mirror of
https://github.com/Yidadaa/ChatGPT-Next-Web.git
synced 2025-08-31 03:09:04 +08:00
Compare commits
4 Commits
feat-multi
...
feat/voice
Author | SHA1 | Date | |
---|---|---|---|
|
52316785d1 | ||
|
e2b15f785a | ||
|
42d04d473e | ||
|
2f53107581 |
@@ -1,97 +1,8 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Node.js dependencies
|
||||
/node_modules
|
||||
/jspm_packages
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.test
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# Next.js build output
|
||||
.next
|
||||
out
|
||||
# docker-compose env files
|
||||
.env
|
||||
|
||||
# Nuxt.js build output
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
.cache/
|
||||
|
||||
|
||||
# Vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
.dynamodb/
|
||||
|
||||
# Temporary folders
|
||||
tmp
|
||||
temp
|
||||
|
||||
# IDE and editor directories
|
||||
.idea
|
||||
.vscode
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS generated files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# secret key
|
||||
*.key
|
||||
*.key.pub
|
||||
*.key.pub
|
@@ -2,7 +2,7 @@
|
||||
# Your openai api key. (required)
|
||||
OPENAI_API_KEY=sk-xxxx
|
||||
|
||||
# Access password, separated by comma. (optional)
|
||||
# Access passsword, separated by comma. (optional)
|
||||
CODE=your-password
|
||||
|
||||
# You can start service behind a proxy
|
||||
|
@@ -216,7 +216,7 @@ If you do not want users to use GPT-4, set this value to 1.
|
||||
|
||||
> Default: Empty
|
||||
|
||||
If you do want users to query balance, set this value to 1.
|
||||
If you do want users to query balance, set this value to 1, or you should set it to 0.
|
||||
|
||||
### `DISABLE_FAST_LINK` (optional)
|
||||
|
||||
|
@@ -1,189 +0,0 @@
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import {
|
||||
ANTHROPIC_BASE_URL,
|
||||
Anthropic,
|
||||
ApiPath,
|
||||
DEFAULT_MODELS,
|
||||
ModelProvider,
|
||||
} from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "../../auth";
|
||||
import { collectModelTable } from "@/app/utils/model";
|
||||
|
||||
const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]);
|
||||
|
||||
async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
console.log("[Anthropic Route] params ", params);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const subpath = params.path.join("/");
|
||||
|
||||
if (!ALLOWD_PATH.has(subpath)) {
|
||||
console.log("[Anthropic Route] forbidden path ", subpath);
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + subpath,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const authResult = auth(req, ModelProvider.Claude);
|
||||
if (authResult.error) {
|
||||
return NextResponse.json(authResult, {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request(req);
|
||||
return response;
|
||||
} catch (e) {
|
||||
console.error("[Anthropic] ", e);
|
||||
return NextResponse.json(prettyObject(e));
|
||||
}
|
||||
}
|
||||
|
||||
export const GET = handle;
|
||||
export const POST = handle;
|
||||
|
||||
export const runtime = "edge";
|
||||
export const preferredRegion = [
|
||||
"arn1",
|
||||
"bom1",
|
||||
"cdg1",
|
||||
"cle1",
|
||||
"cpt1",
|
||||
"dub1",
|
||||
"fra1",
|
||||
"gru1",
|
||||
"hnd1",
|
||||
"iad1",
|
||||
"icn1",
|
||||
"kix1",
|
||||
"lhr1",
|
||||
"pdx1",
|
||||
"sfo1",
|
||||
"sin1",
|
||||
"syd1",
|
||||
];
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
async function request(req: NextRequest) {
|
||||
const controller = new AbortController();
|
||||
|
||||
let authHeaderName = "x-api-key";
|
||||
let authValue =
|
||||
req.headers.get(authHeaderName) ||
|
||||
req.headers.get("Authorization")?.replaceAll("Bearer ", "").trim() ||
|
||||
serverConfig.anthropicApiKey ||
|
||||
"";
|
||||
|
||||
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.Anthropic, "");
|
||||
|
||||
let baseUrl =
|
||||
serverConfig.anthropicUrl || serverConfig.baseUrl || ANTHROPIC_BASE_URL;
|
||||
|
||||
if (!baseUrl.startsWith("http")) {
|
||||
baseUrl = `https://${baseUrl}`;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
console.log("[Proxy] ", path);
|
||||
console.log("[Base Url]", baseUrl);
|
||||
|
||||
const timeoutId = setTimeout(
|
||||
() => {
|
||||
controller.abort();
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
|
||||
const fetchUrl = `${baseUrl}${path}`;
|
||||
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-store",
|
||||
[authHeaderName]: authValue,
|
||||
"anthropic-version":
|
||||
req.headers.get("anthropic-version") ||
|
||||
serverConfig.anthropicApiVersion ||
|
||||
Anthropic.Vision,
|
||||
},
|
||||
method: req.method,
|
||||
body: req.body,
|
||||
redirect: "manual",
|
||||
// @ts-ignore
|
||||
duplex: "half",
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
// #1815 try to refuse some request to some models
|
||||
if (serverConfig.customModels && req.body) {
|
||||
try {
|
||||
const modelTable = collectModelTable(
|
||||
DEFAULT_MODELS,
|
||||
serverConfig.customModels,
|
||||
);
|
||||
const clonedBody = await req.text();
|
||||
fetchOptions.body = clonedBody;
|
||||
|
||||
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
||||
|
||||
// not undefined and is false
|
||||
if (modelTable[jsonBody?.model ?? ""].available === false) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: `you are not allowed to use ${jsonBody?.model} model`,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[Anthropic] filter`, e);
|
||||
}
|
||||
}
|
||||
console.log("[Anthropic request]", fetchOptions.headers, req.method);
|
||||
try {
|
||||
const res = await fetch(fetchUrl, fetchOptions);
|
||||
|
||||
console.log(
|
||||
"[Anthropic response]",
|
||||
res.status,
|
||||
" ",
|
||||
res.headers,
|
||||
res.url,
|
||||
);
|
||||
// to prevent browser prompt for credentials
|
||||
const newHeaders = new Headers(res.headers);
|
||||
newHeaders.delete("www-authenticate");
|
||||
// to disable nginx buffering
|
||||
newHeaders.set("X-Accel-Buffering", "no");
|
||||
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers: newHeaders,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
@@ -57,31 +57,12 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
|
||||
if (!apiKey) {
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
// const systemApiKey =
|
||||
// modelProvider === ModelProvider.GeminiPro
|
||||
// ? serverConfig.googleApiKey
|
||||
// : serverConfig.isAzure
|
||||
// ? serverConfig.azureApiKey
|
||||
// : serverConfig.apiKey;
|
||||
|
||||
let systemApiKey: string | undefined;
|
||||
|
||||
switch (modelProvider) {
|
||||
case ModelProvider.GeminiPro:
|
||||
systemApiKey = serverConfig.googleApiKey;
|
||||
break;
|
||||
case ModelProvider.Claude:
|
||||
systemApiKey = serverConfig.anthropicApiKey;
|
||||
break;
|
||||
case ModelProvider.GPT:
|
||||
default:
|
||||
if (serverConfig.isAzure) {
|
||||
systemApiKey = serverConfig.azureApiKey;
|
||||
} else {
|
||||
systemApiKey = serverConfig.apiKey;
|
||||
}
|
||||
}
|
||||
|
||||
const systemApiKey =
|
||||
modelProvider === ModelProvider.GeminiPro
|
||||
? serverConfig.googleApiKey
|
||||
: serverConfig.isAzure
|
||||
? serverConfig.azureApiKey
|
||||
: serverConfig.apiKey;
|
||||
if (systemApiKey) {
|
||||
console.log("[Auth] use system api key");
|
||||
req.headers.set("Authorization", `Bearer ${systemApiKey}`);
|
||||
|
@@ -43,6 +43,10 @@ export async function requestOpenai(req: NextRequest) {
|
||||
|
||||
console.log("[Proxy] ", path);
|
||||
console.log("[Base Url]", baseUrl);
|
||||
// this fix [Org ID] undefined in server side if not using custom point
|
||||
if (serverConfig.openaiOrgId !== undefined) {
|
||||
console.log("[Org ID]", serverConfig.openaiOrgId);
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(
|
||||
() => {
|
||||
@@ -112,37 +116,18 @@ export async function requestOpenai(req: NextRequest) {
|
||||
try {
|
||||
const res = await fetch(fetchUrl, fetchOptions);
|
||||
|
||||
// Extract the OpenAI-Organization header from the response
|
||||
const openaiOrganizationHeader = res.headers.get("OpenAI-Organization");
|
||||
|
||||
// Check if serverConfig.openaiOrgId is defined and not an empty string
|
||||
if (serverConfig.openaiOrgId && serverConfig.openaiOrgId.trim() !== "") {
|
||||
// If openaiOrganizationHeader is present, log it; otherwise, log that the header is not present
|
||||
console.log("[Org ID]", openaiOrganizationHeader);
|
||||
} else {
|
||||
console.log("[Org ID] is not set up.");
|
||||
}
|
||||
|
||||
// to prevent browser prompt for credentials
|
||||
const newHeaders = new Headers(res.headers);
|
||||
newHeaders.delete("www-authenticate");
|
||||
// to disable nginx buffering
|
||||
newHeaders.set("X-Accel-Buffering", "no");
|
||||
|
||||
|
||||
// Conditionally delete the OpenAI-Organization header from the response if [Org ID] is undefined or empty (not setup in ENV)
|
||||
// Also, this is to prevent the header from being sent to the client
|
||||
if (!serverConfig.openaiOrgId || serverConfig.openaiOrgId.trim() === "") {
|
||||
newHeaders.delete("OpenAI-Organization");
|
||||
}
|
||||
|
||||
// The latest version of the OpenAI API forced the content-encoding to be "br" in json response
|
||||
// So if the streaming is disabled, we need to remove the content-encoding header
|
||||
// Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
|
||||
// The browser will try to decode the response with brotli and fail
|
||||
newHeaders.delete("content-encoding");
|
||||
|
||||
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
|
@@ -12,28 +12,17 @@ async function handle(
|
||||
|
||||
const requestUrl = new URL(req.url);
|
||||
let endpoint = requestUrl.searchParams.get("endpoint");
|
||||
|
||||
// Validate the endpoint to prevent potential SSRF attacks
|
||||
if (!endpoint || !endpoint.startsWith("/")) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "Invalid endpoint",
|
||||
},
|
||||
{
|
||||
status: 400,
|
||||
},
|
||||
);
|
||||
if (!endpoint?.endsWith("/")) {
|
||||
endpoint += "/";
|
||||
}
|
||||
const endpointPath = params.path.join("/");
|
||||
const targetPath = `${endpoint}/${endpointPath}`;
|
||||
|
||||
// only allow MKCOL, GET, PUT
|
||||
if (req.method !== "MKCOL" && req.method !== "GET" && req.method !== "PUT") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + targetPath,
|
||||
msg: "you are not allowed to request " + params.path.join("/"),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
@@ -43,13 +32,13 @@ async function handle(
|
||||
|
||||
// for MKCOL request, only allow request ${folder}
|
||||
if (
|
||||
req.method === "MKCOL" &&
|
||||
!targetPath.endsWith(folder)
|
||||
req.method == "MKCOL" &&
|
||||
!new URL(endpointPath).pathname.endsWith(folder)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + targetPath,
|
||||
msg: "you are not allowed to request " + params.path.join("/"),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
@@ -59,13 +48,13 @@ async function handle(
|
||||
|
||||
// for GET request, only allow request ending with fileName
|
||||
if (
|
||||
req.method === "GET" &&
|
||||
!targetPath.endsWith(fileName)
|
||||
req.method == "GET" &&
|
||||
!new URL(endpointPath).pathname.endsWith(fileName)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + targetPath,
|
||||
msg: "you are not allowed to request " + params.path.join("/"),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
@@ -75,13 +64,13 @@ async function handle(
|
||||
|
||||
// for PUT request, only allow request ending with fileName
|
||||
if (
|
||||
req.method === "PUT" &&
|
||||
!targetPath.endsWith(fileName)
|
||||
req.method == "PUT" &&
|
||||
!new URL(endpointPath).pathname.endsWith(fileName)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
msg: "you are not allowed to request " + targetPath,
|
||||
msg: "you are not allowed to request " + params.path.join("/"),
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
@@ -89,7 +78,7 @@ async function handle(
|
||||
);
|
||||
}
|
||||
|
||||
const targetUrl = `${endpoint}/${endpointPath}`;
|
||||
const targetUrl = `${endpoint + endpointPath}`;
|
||||
|
||||
const method = req.method;
|
||||
const shouldNotHaveBody = ["get", "head"].includes(
|
||||
@@ -101,7 +90,6 @@ async function handle(
|
||||
authorization: req.headers.get("authorization") ?? "",
|
||||
},
|
||||
body: shouldNotHaveBody ? null : req.body,
|
||||
redirect: 'manual',
|
||||
method,
|
||||
// @ts-ignore
|
||||
duplex: "half",
|
||||
|
@@ -8,7 +8,6 @@ import {
|
||||
import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store";
|
||||
import { ChatGPTApi } from "./platforms/openai";
|
||||
import { GeminiProApi } from "./platforms/google";
|
||||
import { ClaudeApi } from "./platforms/anthropic";
|
||||
export const ROLES = ["system", "user", "assistant"] as const;
|
||||
export type MessageRole = (typeof ROLES)[number];
|
||||
|
||||
@@ -95,16 +94,11 @@ export class ClientApi {
|
||||
public llm: LLMApi;
|
||||
|
||||
constructor(provider: ModelProvider = ModelProvider.GPT) {
|
||||
switch (provider) {
|
||||
case ModelProvider.GeminiPro:
|
||||
this.llm = new GeminiProApi();
|
||||
break;
|
||||
case ModelProvider.Claude:
|
||||
this.llm = new ClaudeApi();
|
||||
break;
|
||||
default:
|
||||
this.llm = new ChatGPTApi();
|
||||
if (provider === ModelProvider.GeminiPro) {
|
||||
this.llm = new GeminiProApi();
|
||||
return;
|
||||
}
|
||||
this.llm = new ChatGPTApi();
|
||||
}
|
||||
|
||||
config() {}
|
||||
|
@@ -1,404 +0,0 @@
|
||||
import { ACCESS_CODE_PREFIX, Anthropic, ApiPath } from "@/app/constant";
|
||||
import { ChatOptions, LLMApi, MultimodalContent } from "../api";
|
||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { DEFAULT_API_HOST } from "@/app/constant";
|
||||
import { RequestMessage } from "@/app/typing";
|
||||
import {
|
||||
EventStreamContentType,
|
||||
fetchEventSource,
|
||||
} from "@fortaine/fetch-event-source";
|
||||
|
||||
import Locale from "../../locales";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
||||
|
||||
export type MultiBlockContent = {
|
||||
type: "image" | "text";
|
||||
source?: {
|
||||
type: string;
|
||||
media_type: string;
|
||||
data: string;
|
||||
};
|
||||
text?: string;
|
||||
};
|
||||
|
||||
export type AnthropicMessage = {
|
||||
role: (typeof ClaudeMapper)[keyof typeof ClaudeMapper];
|
||||
content: string | MultiBlockContent[];
|
||||
};
|
||||
|
||||
export interface AnthropicChatRequest {
|
||||
model: string; // The model that will complete your prompt.
|
||||
messages: AnthropicMessage[]; // The prompt that you want Claude to complete.
|
||||
max_tokens: number; // The maximum number of tokens to generate before stopping.
|
||||
stop_sequences?: string[]; // Sequences that will cause the model to stop generating completion text.
|
||||
temperature?: number; // Amount of randomness injected into the response.
|
||||
top_p?: number; // Use nucleus sampling.
|
||||
top_k?: number; // Only sample from the top K options for each subsequent token.
|
||||
metadata?: object; // An object describing metadata about the request.
|
||||
stream?: boolean; // Whether to incrementally stream the response using server-sent events.
|
||||
}
|
||||
|
||||
export interface ChatRequest {
|
||||
model: string; // The model that will complete your prompt.
|
||||
prompt: string; // The prompt that you want Claude to complete.
|
||||
max_tokens_to_sample: number; // The maximum number of tokens to generate before stopping.
|
||||
stop_sequences?: string[]; // Sequences that will cause the model to stop generating completion text.
|
||||
temperature?: number; // Amount of randomness injected into the response.
|
||||
top_p?: number; // Use nucleus sampling.
|
||||
top_k?: number; // Only sample from the top K options for each subsequent token.
|
||||
metadata?: object; // An object describing metadata about the request.
|
||||
stream?: boolean; // Whether to incrementally stream the response using server-sent events.
|
||||
}
|
||||
|
||||
export interface ChatResponse {
|
||||
completion: string;
|
||||
stop_reason: "stop_sequence" | "max_tokens";
|
||||
model: string;
|
||||
}
|
||||
|
||||
export type ChatStreamResponse = ChatResponse & {
|
||||
stop?: string;
|
||||
log_id: string;
|
||||
};
|
||||
|
||||
const ClaudeMapper = {
|
||||
assistant: "assistant",
|
||||
user: "user",
|
||||
system: "user",
|
||||
} as const;
|
||||
|
||||
const keys = ["claude-2, claude-instant-1"];
|
||||
|
||||
export class ClaudeApi implements LLMApi {
|
||||
extractMessage(res: any) {
|
||||
console.log("[Response] claude response: ", res);
|
||||
|
||||
return res?.content?.[0]?.text;
|
||||
}
|
||||
async chat(options: ChatOptions): Promise<void> {
|
||||
const visionModel = isVisionModel(options.config.model);
|
||||
|
||||
const accessStore = useAccessStore.getState();
|
||||
|
||||
const shouldStream = !!options.config.stream;
|
||||
|
||||
const modelConfig = {
|
||||
...useAppConfig.getState().modelConfig,
|
||||
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||
...{
|
||||
model: options.config.model,
|
||||
},
|
||||
};
|
||||
|
||||
const messages = [...options.messages];
|
||||
|
||||
const keys = ["system", "user"];
|
||||
|
||||
// roles must alternate between "user" and "assistant" in claude, so add a fake assistant message between two user messages
|
||||
for (let i = 0; i < messages.length - 1; i++) {
|
||||
const message = messages[i];
|
||||
const nextMessage = messages[i + 1];
|
||||
|
||||
if (keys.includes(message.role) && keys.includes(nextMessage.role)) {
|
||||
messages[i] = [
|
||||
message,
|
||||
{
|
||||
role: "assistant",
|
||||
content: ";",
|
||||
},
|
||||
] as any;
|
||||
}
|
||||
}
|
||||
|
||||
const prompt = messages
|
||||
.flat()
|
||||
.filter((v) => {
|
||||
if (!v.content) return false;
|
||||
if (typeof v.content === "string" && !v.content.trim()) return false;
|
||||
return true;
|
||||
})
|
||||
.map((v) => {
|
||||
const { role, content } = v;
|
||||
const insideRole = ClaudeMapper[role] ?? "user";
|
||||
|
||||
if (!visionModel || typeof content === "string") {
|
||||
return {
|
||||
role: insideRole,
|
||||
content: getMessageTextContent(v),
|
||||
};
|
||||
}
|
||||
return {
|
||||
role: insideRole,
|
||||
content: content
|
||||
.filter((v) => v.image_url || v.text)
|
||||
.map(({ type, text, image_url }) => {
|
||||
if (type === "text") {
|
||||
return {
|
||||
type,
|
||||
text: text!,
|
||||
};
|
||||
}
|
||||
const { url = "" } = image_url || {};
|
||||
const colonIndex = url.indexOf(":");
|
||||
const semicolonIndex = url.indexOf(";");
|
||||
const comma = url.indexOf(",");
|
||||
|
||||
const mimeType = url.slice(colonIndex + 1, semicolonIndex);
|
||||
const encodeType = url.slice(semicolonIndex + 1, comma);
|
||||
const data = url.slice(comma + 1);
|
||||
|
||||
return {
|
||||
type: "image" as const,
|
||||
source: {
|
||||
type: encodeType,
|
||||
media_type: mimeType,
|
||||
data,
|
||||
},
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
const requestBody: AnthropicChatRequest = {
|
||||
messages: prompt,
|
||||
stream: shouldStream,
|
||||
|
||||
model: modelConfig.model,
|
||||
max_tokens: modelConfig.max_tokens,
|
||||
temperature: modelConfig.temperature,
|
||||
top_p: modelConfig.top_p,
|
||||
// top_k: modelConfig.top_k,
|
||||
top_k: 5,
|
||||
};
|
||||
|
||||
const path = this.path(Anthropic.ChatPath);
|
||||
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
|
||||
const payload = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
"x-api-key": accessStore.anthropicApiKey,
|
||||
"anthropic-version": accessStore.anthropicApiVersion,
|
||||
Authorization: getAuthKey(accessStore.anthropicApiKey),
|
||||
},
|
||||
};
|
||||
|
||||
if (shouldStream) {
|
||||
try {
|
||||
const context = {
|
||||
text: "",
|
||||
finished: false,
|
||||
};
|
||||
|
||||
const finish = () => {
|
||||
if (!context.finished) {
|
||||
options.onFinish(context.text);
|
||||
context.finished = true;
|
||||
}
|
||||
};
|
||||
|
||||
controller.signal.onabort = finish;
|
||||
fetchEventSource(path, {
|
||||
...payload,
|
||||
async onopen(res) {
|
||||
const contentType = res.headers.get("content-type");
|
||||
console.log("response content type: ", contentType);
|
||||
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
context.text = await res.clone().text();
|
||||
return finish();
|
||||
}
|
||||
|
||||
if (
|
||||
!res.ok ||
|
||||
!res.headers
|
||||
.get("content-type")
|
||||
?.startsWith(EventStreamContentType) ||
|
||||
res.status !== 200
|
||||
) {
|
||||
const responseTexts = [context.text];
|
||||
let extraInfo = await res.clone().text();
|
||||
try {
|
||||
const resJson = await res.clone().json();
|
||||
extraInfo = prettyObject(resJson);
|
||||
} catch {}
|
||||
|
||||
if (res.status === 401) {
|
||||
responseTexts.push(Locale.Error.Unauthorized);
|
||||
}
|
||||
|
||||
if (extraInfo) {
|
||||
responseTexts.push(extraInfo);
|
||||
}
|
||||
|
||||
context.text = responseTexts.join("\n\n");
|
||||
|
||||
return finish();
|
||||
}
|
||||
},
|
||||
onmessage(msg) {
|
||||
let chunkJson:
|
||||
| undefined
|
||||
| {
|
||||
type: "content_block_delta" | "content_block_stop";
|
||||
delta?: {
|
||||
type: "text_delta";
|
||||
text: string;
|
||||
};
|
||||
index: number;
|
||||
};
|
||||
try {
|
||||
chunkJson = JSON.parse(msg.data);
|
||||
} catch (e) {
|
||||
console.error("[Response] parse error", msg.data);
|
||||
}
|
||||
|
||||
if (!chunkJson || chunkJson.type === "content_block_stop") {
|
||||
return finish();
|
||||
}
|
||||
|
||||
const { delta } = chunkJson;
|
||||
if (delta?.text) {
|
||||
context.text += delta.text;
|
||||
options.onUpdate?.(context.text, delta.text);
|
||||
}
|
||||
},
|
||||
onclose() {
|
||||
finish();
|
||||
},
|
||||
onerror(e) {
|
||||
options.onError?.(e);
|
||||
throw e;
|
||||
},
|
||||
openWhenHidden: true,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("failed to chat", e);
|
||||
options.onError?.(e as Error);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
controller.signal.onabort = () => options.onFinish("");
|
||||
|
||||
const res = await fetch(path, payload);
|
||||
const resJson = await res.json();
|
||||
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
} catch (e) {
|
||||
console.error("failed to chat", e);
|
||||
options.onError?.(e as Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
async usage() {
|
||||
return {
|
||||
used: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
async models() {
|
||||
// const provider = {
|
||||
// id: "anthropic",
|
||||
// providerName: "Anthropic",
|
||||
// providerType: "anthropic",
|
||||
// };
|
||||
|
||||
return [
|
||||
// {
|
||||
// name: "claude-instant-1.2",
|
||||
// available: true,
|
||||
// provider,
|
||||
// },
|
||||
// {
|
||||
// name: "claude-2.0",
|
||||
// available: true,
|
||||
// provider,
|
||||
// },
|
||||
// {
|
||||
// name: "claude-2.1",
|
||||
// available: true,
|
||||
// provider,
|
||||
// },
|
||||
// {
|
||||
// name: "claude-3-opus-20240229",
|
||||
// available: true,
|
||||
// provider,
|
||||
// },
|
||||
// {
|
||||
// name: "claude-3-sonnet-20240229",
|
||||
// available: true,
|
||||
// provider,
|
||||
// },
|
||||
// {
|
||||
// name: "claude-3-haiku-20240307",
|
||||
// available: true,
|
||||
// provider,
|
||||
// },
|
||||
];
|
||||
}
|
||||
path(path: string): string {
|
||||
const accessStore = useAccessStore.getState();
|
||||
|
||||
let baseUrl: string = accessStore.anthropicUrl;
|
||||
|
||||
// if endpoint is empty, use default endpoint
|
||||
if (baseUrl.trim().length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
baseUrl = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/anthropic"
|
||||
: ApiPath.Anthropic;
|
||||
}
|
||||
|
||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) {
|
||||
baseUrl = "https://" + baseUrl;
|
||||
}
|
||||
|
||||
baseUrl = trimEnd(baseUrl, "/");
|
||||
|
||||
return `${baseUrl}/${path}`;
|
||||
}
|
||||
}
|
||||
|
||||
function trimEnd(s: string, end = " ") {
|
||||
if (end.length === 0) return s;
|
||||
|
||||
while (s.endsWith(end)) {
|
||||
s = s.slice(0, -end.length);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
function bearer(value: string) {
|
||||
return `Bearer ${value.trim()}`;
|
||||
}
|
||||
|
||||
function getAuthKey(apiKey = "") {
|
||||
const accessStore = useAccessStore.getState();
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
let authKey = "";
|
||||
|
||||
if (apiKey) {
|
||||
// use user's api key first
|
||||
authKey = bearer(apiKey);
|
||||
} else if (
|
||||
accessStore.enabledAccessControl() &&
|
||||
!isApp &&
|
||||
!!accessStore.accessCode
|
||||
) {
|
||||
// or use access code
|
||||
authKey = bearer(ACCESS_CODE_PREFIX + accessStore.accessCode);
|
||||
}
|
||||
|
||||
return authKey;
|
||||
}
|
@@ -151,9 +151,6 @@ export class ChatGPTApi implements LLMApi {
|
||||
if (finished || controller.signal.aborted) {
|
||||
responseText += remainText;
|
||||
console.log("[Response Animation] finished");
|
||||
if (responseText?.length === 0) {
|
||||
options.onError?.(new Error("empty response from server"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -228,21 +225,19 @@ export class ChatGPTApi implements LLMApi {
|
||||
}
|
||||
const text = msg.data;
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
const choices = json.choices as Array<{ delta: { content: string } }>;
|
||||
const delta = choices[0]?.delta?.content;
|
||||
const textmoderation = json?.prompt_filter_results;
|
||||
|
||||
const json = JSON.parse(text) as {
|
||||
choices: Array<{
|
||||
delta: {
|
||||
content: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
const delta = json.choices[0]?.delta?.content;
|
||||
if (delta) {
|
||||
remainText += delta;
|
||||
}
|
||||
|
||||
if (textmoderation && textmoderation.length > 0 && ServiceProvider.Azure) {
|
||||
const contentFilterResults = textmoderation[0]?.content_filter_results;
|
||||
console.log(`[${ServiceProvider.Azure}] [Text Moderation] flagged categories result:`, contentFilterResults);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Request] parse error", text, msg);
|
||||
console.error("[Request] parse error", text);
|
||||
}
|
||||
},
|
||||
onclose() {
|
||||
|
@@ -20,8 +20,6 @@ export function AuthPage() {
|
||||
accessStore.update((access) => {
|
||||
access.openaiApiKey = "";
|
||||
access.accessCode = "";
|
||||
access.googleApiKey = "";
|
||||
access.anthropicApiKey = "";
|
||||
});
|
||||
}; // Reset access code to empty string
|
||||
|
||||
@@ -77,17 +75,6 @@ export function AuthPage() {
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
className={styles["auth-input"]}
|
||||
type="password"
|
||||
placeholder={Locale.Settings.Access.Anthropic.ApiKey.Placeholder}
|
||||
value={accessStore.anthropicApiKey}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.anthropicApiKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
|
@@ -12,7 +12,7 @@ import {
|
||||
import { useChatStore } from "../store";
|
||||
|
||||
import Locale from "../locales";
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { Path } from "../constant";
|
||||
import { MaskAvatar } from "./mask";
|
||||
import { Mask } from "../store/mask";
|
||||
@@ -40,16 +40,12 @@ export function ChatItem(props: {
|
||||
});
|
||||
}
|
||||
}, [props.selected]);
|
||||
|
||||
const { pathname: currentPath } = useLocation();
|
||||
return (
|
||||
<Draggable draggableId={`${props.id}`} index={props.index}>
|
||||
{(provided) => (
|
||||
<div
|
||||
className={`${styles["chat-item"]} ${
|
||||
props.selected &&
|
||||
(currentPath === Path.Chat || currentPath === Path.Home) &&
|
||||
styles["chat-item-selected"]
|
||||
props.selected && styles["chat-item-selected"]
|
||||
}`}
|
||||
onClick={props.onClick}
|
||||
ref={(ele) => {
|
||||
|
@@ -58,7 +58,7 @@
|
||||
box-shadow: var(--card-shadow);
|
||||
transition: width ease 0.3s;
|
||||
align-items: center;
|
||||
height: 16px;
|
||||
height: 24px;
|
||||
width: var(--icon-width);
|
||||
overflow: hidden;
|
||||
|
||||
@@ -68,7 +68,6 @@
|
||||
|
||||
.text {
|
||||
white-space: nowrap;
|
||||
padding-left: 5px;
|
||||
opacity: 0;
|
||||
transform: translateX(-5px);
|
||||
transition: all ease 0.3s;
|
||||
@@ -610,10 +609,6 @@
|
||||
.chat-input-send {
|
||||
background-color: var(--primary);
|
||||
color: white;
|
||||
|
||||
position: absolute;
|
||||
right: 30px;
|
||||
bottom: 32px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
|
@@ -97,7 +97,7 @@ import { ExportMessageModal } from "./exporter";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { useAllModels } from "../utils/hooks";
|
||||
import { MultimodalContent } from "../client/api";
|
||||
|
||||
import SpeechRecorder from "./chat/speechRecorder";
|
||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||
loading: () => <LoadingIcon />,
|
||||
});
|
||||
@@ -347,7 +347,7 @@ function ChatAction(props: {
|
||||
full: 16,
|
||||
icon: 16,
|
||||
});
|
||||
|
||||
const [isActive, setIsActive] = useState(false);
|
||||
function updateWidth() {
|
||||
if (!iconRef.current || !textRef.current) return;
|
||||
const getWidth = (dom: HTMLDivElement) => dom.getBoundingClientRect().width;
|
||||
@@ -361,25 +361,22 @@ function ChatAction(props: {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles["chat-input-action"]} clickable`}
|
||||
className={`${styles["chat-input-action"]} clickable group`}
|
||||
onClick={() => {
|
||||
props.onClick();
|
||||
setTimeout(updateWidth, 1);
|
||||
}}
|
||||
onMouseEnter={updateWidth}
|
||||
onTouchStart={updateWidth}
|
||||
style={
|
||||
{
|
||||
"--icon-width": `${width.icon}px`,
|
||||
"--full-width": `${width.full}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div ref={iconRef} className={styles["icon"]}>
|
||||
{props.icon}
|
||||
</div>
|
||||
<div className={styles["text"]} ref={textRef}>
|
||||
{props.text}
|
||||
<div className="flex">
|
||||
<div ref={iconRef} className={styles["icon"]}>
|
||||
{props.icon}
|
||||
</div>
|
||||
<div
|
||||
className={`${styles["text"]} transition-all duration-1000 w-0 group-hover:w-[60px]`}
|
||||
ref={textRef}
|
||||
>
|
||||
{props.text}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -424,6 +421,7 @@ export function ChatActions(props: {
|
||||
showPromptModal: () => void;
|
||||
scrollToBottom: () => void;
|
||||
showPromptHints: () => void;
|
||||
setUserInput: (text: string) => void;
|
||||
hitBottom: boolean;
|
||||
uploading: boolean;
|
||||
}) {
|
||||
@@ -1462,6 +1460,7 @@ function _Chat() {
|
||||
scrollToBottom={scrollToBottom}
|
||||
hitBottom={hitBottom}
|
||||
uploading={uploading}
|
||||
setUserInput={setUserInput}
|
||||
showPromptHints={() => {
|
||||
// Click again to close
|
||||
if (promptHints.length > 0) {
|
||||
@@ -1522,13 +1521,19 @@ function _Chat() {
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<IconButton
|
||||
icon={<SendWhiteIcon />}
|
||||
text={Locale.Chat.Send}
|
||||
className={styles["chat-input-send"]}
|
||||
type="primary"
|
||||
onClick={() => doSubmit(userInput)}
|
||||
/>
|
||||
<div className="flex gap-2 absolute left-[30px] bottom-[32px]">
|
||||
<SpeechRecorder textUpdater={setUserInput}></SpeechRecorder>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2 absolute right-[30px] bottom-[32px]">
|
||||
<IconButton
|
||||
icon={<SendWhiteIcon />}
|
||||
text={Locale.Chat.Send}
|
||||
className={styles["chat-input-send"]}
|
||||
type="primary"
|
||||
onClick={() => doSubmit(userInput)}
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
64
app/components/chat/speechRecorder.tsx
Normal file
64
app/components/chat/speechRecorder.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import VoiceIcon from "@/app/icons/voice.svg";
|
||||
import { getLang, formatLang } from "@/app/locales";
|
||||
type SpeechRecognitionType =
|
||||
| typeof window.SpeechRecognition
|
||||
| typeof window.webkitSpeechRecognition;
|
||||
|
||||
export default function SpeechRecorder({
|
||||
textUpdater,
|
||||
onStop,
|
||||
}: {
|
||||
textUpdater: (text: string) => void;
|
||||
onStop?: () => void;
|
||||
}) {
|
||||
const [speechRecognition, setSpeechRecognition] =
|
||||
useState<SpeechRecognitionType | null>(null);
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
useEffect(() => {
|
||||
if ("SpeechRecognition" in window) {
|
||||
setSpeechRecognition(new (window as any).SpeechRecognition());
|
||||
} else if ("webkitSpeechRecognition" in window) {
|
||||
setSpeechRecognition(new (window as any).webkitSpeechRecognition());
|
||||
}
|
||||
}, []);
|
||||
return (
|
||||
<>
|
||||
{speechRecognition && (
|
||||
<div>
|
||||
<button
|
||||
onClick={() => {
|
||||
if (!isRecording && speechRecognition) {
|
||||
speechRecognition.continuous = true;
|
||||
speechRecognition.lang = formatLang(getLang());
|
||||
console.log(speechRecognition.lang);
|
||||
speechRecognition.interimResults = true;
|
||||
speechRecognition.start();
|
||||
speechRecognition.onresult = function (event: any) {
|
||||
console.log(event);
|
||||
var transcript = event.results[0][0].transcript;
|
||||
console.log(transcript);
|
||||
textUpdater(transcript);
|
||||
};
|
||||
setIsRecording(true);
|
||||
} else {
|
||||
speechRecognition.stop();
|
||||
setIsRecording(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isRecording ? (
|
||||
<button className="p-2 rounded-full bg-blue-500 hover:bg-blue-600 ring-4 ring-blue-200 transition animate-pulse">
|
||||
<VoiceIcon fill={"white"} />
|
||||
</button>
|
||||
) : (
|
||||
<button className="p-2 rounded-full bg-zinc-100 hover:bg-zinc-200 transition">
|
||||
<VoiceIcon fill={"#8282A5"} />
|
||||
</button>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@@ -315,8 +315,6 @@ export function PreviewActions(props: {
|
||||
var api: ClientApi;
|
||||
if (config.modelConfig.model.startsWith("gemini")) {
|
||||
api = new ClientApi(ModelProvider.GeminiPro);
|
||||
} else if (config.modelConfig.model.startsWith("claude")) {
|
||||
api = new ClientApi(ModelProvider.Claude);
|
||||
} else {
|
||||
api = new ClientApi(ModelProvider.GPT);
|
||||
}
|
||||
|
@@ -173,8 +173,6 @@ export function useLoadData() {
|
||||
var api: ClientApi;
|
||||
if (config.modelConfig.model.startsWith("gemini")) {
|
||||
api = new ClientApi(ModelProvider.GeminiPro);
|
||||
} else if (config.modelConfig.model.startsWith("claude")) {
|
||||
api = new ClientApi(ModelProvider.Claude);
|
||||
} else {
|
||||
api = new ClientApi(ModelProvider.GPT);
|
||||
}
|
||||
|
@@ -116,27 +116,9 @@ function escapeDollarNumber(text: string) {
|
||||
return escapedText;
|
||||
}
|
||||
|
||||
function escapeBrackets(text: string) {
|
||||
const pattern =
|
||||
/(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
|
||||
return text.replace(
|
||||
pattern,
|
||||
(match, codeBlock, squareBracket, roundBracket) => {
|
||||
if (codeBlock) {
|
||||
return codeBlock;
|
||||
} else if (squareBracket) {
|
||||
return `$$${squareBracket}$$`;
|
||||
} else if (roundBracket) {
|
||||
return `$${roundBracket}$`;
|
||||
}
|
||||
return match;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function _MarkDownContent(props: { content: string }) {
|
||||
const escapedContent = useMemo(
|
||||
() => escapeBrackets(escapeDollarNumber(props.content)),
|
||||
() => escapeDollarNumber(props.content),
|
||||
[props.content],
|
||||
);
|
||||
|
||||
|
@@ -404,16 +404,7 @@ export function MaskPage() {
|
||||
const maskStore = useMaskStore();
|
||||
const chatStore = useChatStore();
|
||||
|
||||
const [filterLang, setFilterLang] = useState<Lang | undefined>(
|
||||
localStorage.getItem("Mask-language") as Lang | undefined,
|
||||
);
|
||||
useEffect(() => {
|
||||
if (filterLang) {
|
||||
localStorage.setItem("Mask-language", filterLang);
|
||||
} else {
|
||||
localStorage.removeItem("Mask-language");
|
||||
}
|
||||
}, [filterLang]);
|
||||
const [filterLang, setFilterLang] = useState<Lang>();
|
||||
|
||||
const allMasks = maskStore
|
||||
.getAll()
|
||||
|
@@ -227,7 +227,7 @@ export function MessageSelector(props: {
|
||||
</div>
|
||||
|
||||
<div className={styles["checkbox"]}>
|
||||
<input type="checkbox" checked={isSelected} readOnly></input>
|
||||
<input type="checkbox" checked={isSelected}></input>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@@ -51,7 +51,6 @@ import Locale, {
|
||||
import { copyToClipboard } from "../utils";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Anthropic,
|
||||
Azure,
|
||||
Google,
|
||||
OPENAI_BASE_URL,
|
||||
@@ -964,7 +963,7 @@ export function Settings() {
|
||||
</Select>
|
||||
</ListItem>
|
||||
|
||||
{accessStore.provider === ServiceProvider.OpenAI && (
|
||||
{accessStore.provider === "OpenAI" ? (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.OpenAI.Endpoint.Title}
|
||||
@@ -1003,8 +1002,7 @@ export function Settings() {
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
{accessStore.provider === ServiceProvider.Azure && (
|
||||
) : accessStore.provider === "Azure" ? (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.Azure.Endpoint.Title}
|
||||
@@ -1063,8 +1061,7 @@ export function Settings() {
|
||||
></input>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
{accessStore.provider === ServiceProvider.Google && (
|
||||
) : accessStore.provider === "Google" ? (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.Google.Endpoint.Title}
|
||||
@@ -1123,70 +1120,7 @@ export function Settings() {
|
||||
></input>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
{accessStore.provider === ServiceProvider.Anthropic && (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.Anthropic.Endpoint.Title}
|
||||
subTitle={
|
||||
Locale.Settings.Access.Anthropic.Endpoint.SubTitle +
|
||||
Anthropic.ExampleEndpoint
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={accessStore.anthropicUrl}
|
||||
placeholder={Anthropic.ExampleEndpoint}
|
||||
onChange={(e) =>
|
||||
accessStore.update(
|
||||
(access) =>
|
||||
(access.anthropicUrl = e.currentTarget.value),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.Anthropic.ApiKey.Title}
|
||||
subTitle={
|
||||
Locale.Settings.Access.Anthropic.ApiKey.SubTitle
|
||||
}
|
||||
>
|
||||
<PasswordInput
|
||||
value={accessStore.anthropicApiKey}
|
||||
type="text"
|
||||
placeholder={
|
||||
Locale.Settings.Access.Anthropic.ApiKey.Placeholder
|
||||
}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) =>
|
||||
(access.anthropicApiKey =
|
||||
e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.Anthropic.ApiVerion.Title}
|
||||
subTitle={
|
||||
Locale.Settings.Access.Anthropic.ApiVerion.SubTitle
|
||||
}
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
value={accessStore.anthropicApiVersion}
|
||||
placeholder={Anthropic.Vision}
|
||||
onChange={(e) =>
|
||||
accessStore.update(
|
||||
(access) =>
|
||||
(access.anthropicApiVersion =
|
||||
e.currentTarget.value),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
) : null}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
@@ -69,7 +69,6 @@ export const getServerSideConfig = () => {
|
||||
|
||||
const isAzure = !!process.env.AZURE_URL;
|
||||
const isGoogle = !!process.env.GOOGLE_API_KEY;
|
||||
const isAnthropic = !!process.env.ANTHROPIC_API_KEY;
|
||||
|
||||
const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
|
||||
const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
|
||||
@@ -93,11 +92,6 @@ export const getServerSideConfig = () => {
|
||||
googleApiKey: process.env.GOOGLE_API_KEY,
|
||||
googleUrl: process.env.GOOGLE_URL,
|
||||
|
||||
isAnthropic,
|
||||
anthropicApiKey: process.env.ANTHROPIC_API_KEY,
|
||||
anthropicApiVersion: process.env.ANTHROPIC_API_VERSION,
|
||||
anthropicUrl: process.env.ANTHROPIC_URL,
|
||||
|
||||
gtmId: process.env.GTM_ID,
|
||||
|
||||
needCode: ACCESS_CODES.size > 0,
|
||||
|
@@ -10,7 +10,6 @@ export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
||||
|
||||
export const DEFAULT_API_HOST = "https://api.nextchat.dev";
|
||||
export const OPENAI_BASE_URL = "https://api.openai.com";
|
||||
export const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
||||
|
||||
export const GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/";
|
||||
|
||||
@@ -26,7 +25,6 @@ export enum Path {
|
||||
export enum ApiPath {
|
||||
Cors = "",
|
||||
OpenAI = "/api/openai",
|
||||
Anthropic = "/api/anthropic",
|
||||
}
|
||||
|
||||
export enum SlotID {
|
||||
@@ -69,22 +67,13 @@ export enum ServiceProvider {
|
||||
OpenAI = "OpenAI",
|
||||
Azure = "Azure",
|
||||
Google = "Google",
|
||||
Anthropic = "Anthropic",
|
||||
}
|
||||
|
||||
export enum ModelProvider {
|
||||
GPT = "GPT",
|
||||
GeminiPro = "GeminiPro",
|
||||
Claude = "Claude",
|
||||
}
|
||||
|
||||
export const Anthropic = {
|
||||
ChatPath: "v1/messages",
|
||||
ChatPath1: "v1/complete",
|
||||
ExampleEndpoint: "https://api.anthropic.com",
|
||||
Vision: "2023-06-01",
|
||||
};
|
||||
|
||||
export const OpenaiPath = {
|
||||
ChatPath: "v1/chat/completions",
|
||||
UsagePath: "dashboard/billing/usage",
|
||||
@@ -300,60 +289,6 @@ export const DEFAULT_MODELS = [
|
||||
providerType: "google",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "claude-instant-1.2",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "anthropic",
|
||||
providerName: "Anthropic",
|
||||
providerType: "anthropic",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "claude-2.0",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "anthropic",
|
||||
providerName: "Anthropic",
|
||||
providerType: "anthropic",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "claude-2.1",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "anthropic",
|
||||
providerName: "Anthropic",
|
||||
providerType: "anthropic",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "claude-3-opus-20240229",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "anthropic",
|
||||
providerName: "Anthropic",
|
||||
providerType: "anthropic",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "claude-3-sonnet-20240229",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "anthropic",
|
||||
providerName: "Anthropic",
|
||||
providerType: "anthropic",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "claude-3-haiku-20240307",
|
||||
available: true,
|
||||
provider: {
|
||||
id: "anthropic",
|
||||
providerName: "Anthropic",
|
||||
providerType: "anthropic",
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const CHAT_PAGE_SIZE = 15;
|
||||
|
11
app/icons/voice.svg
Normal file
11
app/icons/voice.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M5.9375 5.3125C5.9375 3.06884 7.75634 1.25 10 1.25C12.2437 1.25 14.0625 3.06884 14.0625 5.3125V8.75C14.0625 10.9937 12.2437 12.8125 10 12.8125C7.75634 12.8125 5.9375 10.9937 5.9375 8.75V5.3125Z"
|
||||
fill="auto" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd"
|
||||
d="M3.35938 7.8125C3.79085 7.8125 4.14062 8.16228 4.14062 8.59375V8.75C4.14062 11.986 6.76396 14.6094 10 14.6094C13.236 14.6094 15.8594 11.986 15.8594 8.75V8.59375C15.8594 8.16228 16.2092 7.8125 16.6406 7.8125C17.0721 7.8125 17.4219 8.16228 17.4219 8.59375V8.75C17.4219 12.849 14.099 16.1719 10 16.1719C5.90101 16.1719 2.57812 12.849 2.57812 8.75V8.59375C2.57812 8.16228 2.9279 7.8125 3.35938 7.8125Z"
|
||||
fill="auto" />
|
||||
<path
|
||||
d="M9.21875 15.4688C9.21875 15.0373 9.56853 14.6875 10 14.6875C10.4315 14.6875 10.7812 15.0373 10.7812 15.4688V17.9688C10.7812 18.4002 10.4315 18.75 10 18.75C9.56853 18.75 9.21875 18.4002 9.21875 17.9688V15.4688Z"
|
||||
fill="auto" />
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
@@ -13,7 +13,7 @@ const cn = {
|
||||
Auth: {
|
||||
Title: "需要密码",
|
||||
Tips: "管理员开启了密码验证,请在下方填入访问码",
|
||||
SubTips: "或者输入你的 OpenAI, Google API 或 Anthropic API 密钥",
|
||||
SubTips: "或者输入你的 OpenAI 或 Google API 密钥",
|
||||
Input: "在此处填写访问码",
|
||||
Confirm: "确认",
|
||||
Later: "稍后再说",
|
||||
@@ -313,23 +313,6 @@ const cn = {
|
||||
SubTitle: "选择指定的部分版本",
|
||||
},
|
||||
},
|
||||
Anthropic: {
|
||||
ApiKey: {
|
||||
Title: "接口密钥",
|
||||
SubTitle: "使用自定义 Anthropic Key 绕过密码访问限制",
|
||||
Placeholder: "输入您的 Anthropic API 密钥",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "样例:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "接口版本 (claude api version)",
|
||||
SubTitle: "选择一个特定的 API 版本输入",
|
||||
},
|
||||
},
|
||||
Google: {
|
||||
ApiKey: {
|
||||
Title: "API 密钥",
|
||||
|
@@ -15,7 +15,7 @@ const en: LocaleType = {
|
||||
Auth: {
|
||||
Title: "Need Access Code",
|
||||
Tips: "Please enter access code below",
|
||||
SubTips: "Or enter your OpenAI, Google API Key or Anthropic API Key",
|
||||
SubTips: "Or enter your OpenAI or Google API Key",
|
||||
Input: "access code",
|
||||
Confirm: "Confirm",
|
||||
Later: "Later",
|
||||
@@ -316,24 +316,6 @@ const en: LocaleType = {
|
||||
SubTitle: "Check your api version from azure console",
|
||||
},
|
||||
},
|
||||
Anthropic: {
|
||||
ApiKey: {
|
||||
Title: "Anthropic API Key",
|
||||
SubTitle:
|
||||
"Use a custom Anthropic Key to bypass password access restrictions",
|
||||
Placeholder: "Enter your Anthropic API Key",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Endpoint Address",
|
||||
SubTitle: "Example:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "API Version (claude api version)",
|
||||
SubTitle: "Select and input a specific API version",
|
||||
},
|
||||
},
|
||||
CustomModel: {
|
||||
Title: "Custom Models",
|
||||
SubTitle: "Custom model options, seperated by comma",
|
||||
|
@@ -70,6 +70,28 @@ export const ALL_LANG_OPTIONS: Record<Lang, string> = {
|
||||
sk: "Slovensky",
|
||||
};
|
||||
|
||||
const LANG_CODE_MAPPING = {
|
||||
cn: "zh-CN",
|
||||
en: "en-US",
|
||||
tw: "zh-TW",
|
||||
pt: "pt-BR",
|
||||
jp: "ja-JP",
|
||||
ko: "ko-KR",
|
||||
id: "id-ID",
|
||||
fr: "fr-FR",
|
||||
es: "es-ES",
|
||||
it: "it-IT",
|
||||
tr: "tr-TR",
|
||||
de: "de-DE",
|
||||
vi: "vi-VN",
|
||||
ru: "ru-RU",
|
||||
cs: "cs-CZ",
|
||||
no: "nb-NO",
|
||||
ar: "ar-SA",
|
||||
bn: "bn-BD",
|
||||
sk: "sk-SK",
|
||||
};
|
||||
|
||||
const LANG_KEY = "lang";
|
||||
const DEFAULT_LANG = "en";
|
||||
|
||||
@@ -81,6 +103,13 @@ merge(fallbackLang, targetLang);
|
||||
|
||||
export default fallbackLang as LocaleType;
|
||||
|
||||
export const formatLang = (languageCode: string) => {
|
||||
return (
|
||||
LANG_CODE_MAPPING[languageCode as keyof typeof LANG_CODE_MAPPING] ||
|
||||
languageCode
|
||||
);
|
||||
};
|
||||
|
||||
function getItem(key: string) {
|
||||
try {
|
||||
return localStorage.getItem(key);
|
||||
|
@@ -316,23 +316,6 @@ const pt: PartialLocaleType = {
|
||||
SubTitle: "Verifique sua versão API do console Azure",
|
||||
},
|
||||
},
|
||||
Anthropic: {
|
||||
ApiKey: {
|
||||
Title: "Chave API Anthropic",
|
||||
SubTitle: "Verifique sua chave API do console Anthropic",
|
||||
Placeholder: "Chave API Anthropic",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Endpoint Address",
|
||||
SubTitle: "Exemplo: ",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "Versão API (Versão api claude)",
|
||||
SubTitle: "Verifique sua versão API do console Anthropic",
|
||||
},
|
||||
},
|
||||
CustomModel: {
|
||||
Title: "Modelos Personalizados",
|
||||
SubTitle: "Opções de modelo personalizado, separados por vírgula",
|
||||
|
@@ -317,23 +317,6 @@ const sk: PartialLocaleType = {
|
||||
SubTitle: "Skontrolujte svoju verziu API v Azure konzole",
|
||||
},
|
||||
},
|
||||
Anthropic: {
|
||||
ApiKey: {
|
||||
Title: "API kľúč Anthropic",
|
||||
SubTitle: "Skontrolujte svoj API kľúč v Anthropic konzole",
|
||||
Placeholder: "API kľúč Anthropic",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "Adresa koncového bodu",
|
||||
SubTitle: "Príklad:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "Verzia API (claude verzia API)",
|
||||
SubTitle: "Vyberte špecifickú verziu časti",
|
||||
},
|
||||
},
|
||||
CustomModel: {
|
||||
Title: "Vlastné modely",
|
||||
SubTitle: "Možnosti vlastného modelu, oddelené čiarkou",
|
||||
|
@@ -314,23 +314,6 @@ const tw = {
|
||||
SubTitle: "選擇指定的部分版本",
|
||||
},
|
||||
},
|
||||
Anthropic: {
|
||||
ApiKey: {
|
||||
Title: "API 密鑰",
|
||||
SubTitle: "從 Anthropic AI 獲取您的 API 密鑰",
|
||||
Placeholder: "Anthropic API Key",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
Title: "終端地址",
|
||||
SubTitle: "示例:",
|
||||
},
|
||||
|
||||
ApiVerion: {
|
||||
Title: "API 版本 (claude api version)",
|
||||
SubTitle: "選擇一個特定的 API 版本输入",
|
||||
},
|
||||
},
|
||||
Google: {
|
||||
ApiKey: {
|
||||
Title: "API 密鑰",
|
||||
@@ -484,12 +467,12 @@ const tw = {
|
||||
|
||||
type DeepPartial<T> = T extends object
|
||||
? {
|
||||
[P in keyof T]?: DeepPartial<T[P]>;
|
||||
}
|
||||
[P in keyof T]?: DeepPartial<T[P]>;
|
||||
}
|
||||
: T;
|
||||
|
||||
export type LocaleType = typeof tw;
|
||||
export type PartialLocaleType = DeepPartial<typeof tw>;
|
||||
|
||||
export default tw;
|
||||
// Translated by @chunkiuuu, feel free the submit new pr if there are typo/incorrect translations :D
|
||||
// Translated by @chunkiuuu, feel free the submit new pr if there are typo/incorrect translations :D
|
@@ -36,11 +36,6 @@ const DEFAULT_ACCESS_STATE = {
|
||||
googleApiKey: "",
|
||||
googleApiVersion: "v1",
|
||||
|
||||
// anthropic
|
||||
anthropicApiKey: "",
|
||||
anthropicApiVersion: "2023-06-01",
|
||||
anthropicUrl: "",
|
||||
|
||||
// server config
|
||||
needCode: true,
|
||||
hideUserApiKey: false,
|
||||
@@ -72,10 +67,6 @@ export const useAccessStore = createPersistStore(
|
||||
return ensure(get(), ["googleApiKey"]);
|
||||
},
|
||||
|
||||
isValidAnthropic() {
|
||||
return ensure(get(), ["anthropicApiKey"]);
|
||||
},
|
||||
|
||||
isAuthorized() {
|
||||
this.fetch();
|
||||
|
||||
@@ -84,7 +75,6 @@ export const useAccessStore = createPersistStore(
|
||||
this.isValidOpenAI() ||
|
||||
this.isValidAzure() ||
|
||||
this.isValidGoogle() ||
|
||||
this.isValidAnthropic() ||
|
||||
!this.enabledAccessControl() ||
|
||||
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
|
||||
);
|
||||
|
@@ -348,8 +348,6 @@ export const useChatStore = createPersistStore(
|
||||
var api: ClientApi;
|
||||
if (modelConfig.model.startsWith("gemini")) {
|
||||
api = new ClientApi(ModelProvider.GeminiPro);
|
||||
} else if (modelConfig.model.startsWith("claude")) {
|
||||
api = new ClientApi(ModelProvider.Claude);
|
||||
} else {
|
||||
api = new ClientApi(ModelProvider.GPT);
|
||||
}
|
||||
@@ -496,6 +494,7 @@ export const useChatStore = createPersistStore(
|
||||
tokenCount += estimateTokenLength(getMessageTextContent(msg));
|
||||
reversedRecentMessages.push(msg);
|
||||
}
|
||||
|
||||
// concat all messages
|
||||
const recentMessages = [
|
||||
...systemPrompts,
|
||||
@@ -534,8 +533,6 @@ export const useChatStore = createPersistStore(
|
||||
var api: ClientApi;
|
||||
if (modelConfig.model.startsWith("gemini")) {
|
||||
api = new ClientApi(ModelProvider.GeminiPro);
|
||||
} else if (modelConfig.model.startsWith("claude")) {
|
||||
api = new ClientApi(ModelProvider.Claude);
|
||||
} else {
|
||||
api = new ClientApi(ModelProvider.GPT);
|
||||
}
|
||||
@@ -560,7 +557,6 @@ export const useChatStore = createPersistStore(
|
||||
messages: topicMessages,
|
||||
config: {
|
||||
model: getSummarizeModel(session.mask.modelConfig.model),
|
||||
stream: false,
|
||||
},
|
||||
onFinish(message) {
|
||||
get().updateCurrentSession(
|
||||
@@ -604,10 +600,6 @@ export const useChatStore = createPersistStore(
|
||||
historyMsgLength > modelConfig.compressMessageLengthThreshold &&
|
||||
modelConfig.sendMemory
|
||||
) {
|
||||
/** Destruct max_tokens while summarizing
|
||||
* this param is just shit
|
||||
**/
|
||||
const { max_tokens, ...modelcfg } = modelConfig;
|
||||
api.llm.chat({
|
||||
messages: toBeSummarizedMsgs.concat(
|
||||
createMessage({
|
||||
@@ -617,7 +609,7 @@ export const useChatStore = createPersistStore(
|
||||
}),
|
||||
),
|
||||
config: {
|
||||
...modelcfg,
|
||||
...modelConfig,
|
||||
stream: true,
|
||||
model: getSummarizeModel(session.mask.modelConfig.model),
|
||||
},
|
||||
|
@@ -1,3 +1,6 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import "./animation.scss";
|
||||
@import "./window.scss";
|
||||
|
||||
|
@@ -1,9 +1 @@
|
||||
export type Updater<T> = (updater: (value: T) => void) => void;
|
||||
|
||||
export const ROLES = ["system", "user", "assistant"] as const;
|
||||
export type MessageRole = (typeof ROLES)[number];
|
||||
|
||||
export interface RequestMessage {
|
||||
role: MessageRole;
|
||||
content: string;
|
||||
}
|
||||
|
23
app/utils.ts
23
app/utils.ts
@@ -2,17 +2,16 @@ import { useEffect, useState } from "react";
|
||||
import { showToast } from "./components/ui-lib";
|
||||
import Locale from "./locales";
|
||||
import { RequestMessage } from "./client/api";
|
||||
import { DEFAULT_MODELS } from "./constant";
|
||||
|
||||
export function trimTopic(topic: string) {
|
||||
// Fix an issue where double quotes still show in the Indonesian language
|
||||
// This will remove the specified punctuation from the end of the string
|
||||
// and also trim quotes from both the start and end if they exist.
|
||||
return (
|
||||
topic
|
||||
// fix for gemini
|
||||
.replace(/^["“”*]+|["“”*]+$/g, "")
|
||||
.replace(/[,。!?”“"、,.!?*]*$/, "")
|
||||
);
|
||||
return topic
|
||||
// fix for gemini
|
||||
.replace(/^["“”*]+|["“”*]+$/g, "")
|
||||
.replace(/[,。!?”“"、,.!?*]*$/, "");
|
||||
}
|
||||
|
||||
export async function copyToClipboard(text: string) {
|
||||
@@ -58,7 +57,10 @@ export async function downloadAs(text: string, filename: string) {
|
||||
|
||||
if (result !== null) {
|
||||
try {
|
||||
await window.__TAURI__.fs.writeTextFile(result, text);
|
||||
await window.__TAURI__.fs.writeTextFile(
|
||||
result,
|
||||
text
|
||||
);
|
||||
showToast(Locale.Download.Success);
|
||||
} catch (error) {
|
||||
showToast(Locale.Download.Failed);
|
||||
@@ -291,7 +293,10 @@ export function getMessageImages(message: RequestMessage): string[] {
|
||||
|
||||
export function isVisionModel(model: string) {
|
||||
// Note: This is a better way using the TypeScript feature instead of `&&` or `||` (ts v5.5.0-dev.20240314 I've been using)
|
||||
const visionKeywords = ["vision", "claude-3"];
|
||||
const visionKeywords = [
|
||||
"vision",
|
||||
"claude-3",
|
||||
];
|
||||
|
||||
return visionKeywords.some((keyword) => model.includes(keyword));
|
||||
return visionKeywords.some(keyword => model.includes(keyword));
|
||||
}
|
||||
|
@@ -18,15 +18,8 @@ export function createWebDavClient(store: SyncStore) {
|
||||
method: "MKCOL",
|
||||
headers: this.headers(),
|
||||
});
|
||||
const success = [201, 200, 404, 405, 301, 302, 307, 308].includes(
|
||||
res.status,
|
||||
);
|
||||
console.log(
|
||||
`[WebDav] check ${success ? "success" : "failed"}, ${res.status} ${
|
||||
res.statusText
|
||||
}`,
|
||||
);
|
||||
return success;
|
||||
console.log("[WebDav] check", res.status, res.statusText);
|
||||
return [201, 200, 404, 301, 302, 307, 308].includes(res.status);
|
||||
} catch (e) {
|
||||
console.error("[WebDav] failed to check", e);
|
||||
}
|
||||
|
@@ -1,17 +0,0 @@
|
||||
export function omit<T extends object, U extends (keyof T)[]>(
|
||||
obj: T,
|
||||
...keys: U
|
||||
): Omit<T, U[number]> {
|
||||
const ret: any = { ...obj };
|
||||
keys.forEach((key) => delete ret[key]);
|
||||
return ret;
|
||||
}
|
||||
|
||||
export function pick<T extends object, U extends (keyof T)[]>(
|
||||
obj: T,
|
||||
...keys: U
|
||||
): Pick<T, U[number]> {
|
||||
const ret: any = {};
|
||||
keys.forEach((key) => (ret[key] = obj[key]));
|
||||
return ret;
|
||||
}
|
@@ -77,10 +77,6 @@ if (mode !== "export") {
|
||||
source: "/api/proxy/openai/:path*",
|
||||
destination: "https://api.openai.com/:path*",
|
||||
},
|
||||
{
|
||||
source: "/api/proxy/anthropic/:path*",
|
||||
destination: "https://api.anthropic.com/:path*",
|
||||
},
|
||||
{
|
||||
source: "/google-fonts/:path*",
|
||||
destination: "https://fonts.googleapis.com/:path*",
|
||||
|
15
package.json
15
package.json
@@ -22,7 +22,7 @@
|
||||
"@svgr/webpack": "^6.5.1",
|
||||
"@vercel/analytics": "^0.1.11",
|
||||
"@vercel/speed-insights": "^1.0.2",
|
||||
"emoji-picker-react": "^4.9.2",
|
||||
"emoji-picker-react": "^4.5.15",
|
||||
"fuse.js": "^7.0.0",
|
||||
"html-to-image": "^1.11.11",
|
||||
"mermaid": "^10.6.1",
|
||||
@@ -44,25 +44,28 @@
|
||||
"zustand": "^4.3.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "1.5.11",
|
||||
"@types/node": "^20.11.30",
|
||||
"@types/react": "^18.2.70",
|
||||
"@tauri-apps/cli": "1.5.7",
|
||||
"@types/node": "^20.9.0",
|
||||
"@types/react": "^18.2.14",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-katex": "^3.0.0",
|
||||
"@types/spark-md5": "^3.0.4",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.49.0",
|
||||
"eslint-config-next": "13.4.19",
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"husky": "^8.0.0",
|
||||
"lint-staged": "^13.2.2",
|
||||
"postcss": "^8.4.35",
|
||||
"prettier": "^3.0.2",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "5.2.2",
|
||||
"webpack": "^5.88.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"lint-staged/yaml": "^2.2.2"
|
||||
},
|
||||
}
|
||||
"packageManager": "yarn@1.22.19"
|
||||
}
|
||||
|
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
15
tailwind.config.js
Normal file
15
tailwind.config.js
Normal file
@@ -0,0 +1,15 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: [
|
||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
|
||||
// Or if using `src` directory:
|
||||
"./src/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
@@ -23,6 +23,12 @@
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "app/calcTextareaHeight.ts"],
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"app/calcTextareaHeight.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
Reference in New Issue
Block a user