Merge branch 'main' into feat-shortcutkey
@@ -10,6 +10,9 @@ import { handle as alibabaHandler } from "../../alibaba";
|
||||
import { handle as moonshotHandler } from "../../moonshot";
|
||||
import { handle as stabilityHandler } from "../../stability";
|
||||
import { handle as iflytekHandler } from "../../iflytek";
|
||||
import { handle as deepseekHandler } from "../../deepseek";
|
||||
import { handle as xaiHandler } from "../../xai";
|
||||
import { handle as chatglmHandler } from "../../glm";
|
||||
import { handle as proxyHandler } from "../../proxy";
|
||||
|
||||
async function handle(
|
||||
@@ -38,6 +41,12 @@ async function handle(
|
||||
return stabilityHandler(req, { params });
|
||||
case ApiPath.Iflytek:
|
||||
return iflytekHandler(req, { params });
|
||||
case ApiPath.DeepSeek:
|
||||
return deepseekHandler(req, { params });
|
||||
case ApiPath.XAI:
|
||||
return xaiHandler(req, { params });
|
||||
case ApiPath.ChatGLM:
|
||||
return chatglmHandler(req, { params });
|
||||
case ApiPath.OpenAI:
|
||||
return openaiHandler(req, { params });
|
||||
default:
|
||||
|
@@ -8,7 +8,7 @@ import {
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/api/auth";
|
||||
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||
import { isModelNotavailableInServer } from "@/app/utils/model";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
@@ -89,7 +89,7 @@ async function request(req: NextRequest) {
|
||||
|
||||
// not undefined and is false
|
||||
if (
|
||||
isModelAvailableInServer(
|
||||
isModelNotavailableInServer(
|
||||
serverConfig.customModels,
|
||||
jsonBody?.model as string,
|
||||
ServiceProvider.Alibaba as string,
|
||||
|
@@ -9,7 +9,7 @@ import {
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "./auth";
|
||||
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||
import { isModelNotavailableInServer } from "@/app/utils/model";
|
||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||
|
||||
const ALLOWD_PATH = new Set([Anthropic.ChatPath, Anthropic.ChatPath1]);
|
||||
@@ -122,7 +122,7 @@ async function request(req: NextRequest) {
|
||||
|
||||
// not undefined and is false
|
||||
if (
|
||||
isModelAvailableInServer(
|
||||
isModelNotavailableInServer(
|
||||
serverConfig.customModels,
|
||||
jsonBody?.model as string,
|
||||
ServiceProvider.Anthropic as string,
|
||||
|
@@ -92,6 +92,15 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
|
||||
systemApiKey =
|
||||
serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret;
|
||||
break;
|
||||
case ModelProvider.DeepSeek:
|
||||
systemApiKey = serverConfig.deepseekApiKey;
|
||||
break;
|
||||
case ModelProvider.XAI:
|
||||
systemApiKey = serverConfig.xaiApiKey;
|
||||
break;
|
||||
case ModelProvider.ChatGLM:
|
||||
systemApiKey = serverConfig.chatglmApiKey;
|
||||
break;
|
||||
case ModelProvider.GPT:
|
||||
default:
|
||||
if (req.nextUrl.pathname.includes("azure/deployments")) {
|
||||
|
@@ -8,7 +8,7 @@ import {
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/api/auth";
|
||||
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||
import { isModelNotavailableInServer } from "@/app/utils/model";
|
||||
import { getAccessToken } from "@/app/utils/baidu";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
@@ -104,7 +104,7 @@ async function request(req: NextRequest) {
|
||||
|
||||
// not undefined and is false
|
||||
if (
|
||||
isModelAvailableInServer(
|
||||
isModelNotavailableInServer(
|
||||
serverConfig.customModels,
|
||||
jsonBody?.model as string,
|
||||
ServiceProvider.Baidu as string,
|
||||
|
@@ -8,7 +8,7 @@ import {
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/api/auth";
|
||||
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||
import { isModelNotavailableInServer } from "@/app/utils/model";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
@@ -88,7 +88,7 @@ async function request(req: NextRequest) {
|
||||
|
||||
// not undefined and is false
|
||||
if (
|
||||
isModelAvailableInServer(
|
||||
isModelNotavailableInServer(
|
||||
serverConfig.customModels,
|
||||
jsonBody?.model as string,
|
||||
ServiceProvider.ByteDance as string,
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSideConfig } from "../config/server";
|
||||
import { OPENAI_BASE_URL, ServiceProvider } from "../constant";
|
||||
import { isModelAvailableInServer } from "../utils/model";
|
||||
import { cloudflareAIGatewayUrl } from "../utils/cloudflare";
|
||||
import { getModelProvider, isModelNotavailableInServer } from "../utils/model";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
@@ -71,7 +71,7 @@ export async function requestOpenai(req: NextRequest) {
|
||||
.filter((v) => !!v && !v.startsWith("-") && v.includes(modelName))
|
||||
.forEach((m) => {
|
||||
const [fullName, displayName] = m.split("=");
|
||||
const [_, providerName] = fullName.split("@");
|
||||
const [_, providerName] = getModelProvider(fullName);
|
||||
if (providerName === "azure" && !displayName) {
|
||||
const [_, deployId] = (serverConfig?.azureUrl ?? "").split(
|
||||
"deployments/",
|
||||
@@ -118,15 +118,14 @@ export async function requestOpenai(req: NextRequest) {
|
||||
|
||||
// not undefined and is false
|
||||
if (
|
||||
isModelAvailableInServer(
|
||||
isModelNotavailableInServer(
|
||||
serverConfig.customModels,
|
||||
jsonBody?.model as string,
|
||||
ServiceProvider.OpenAI as string,
|
||||
) ||
|
||||
isModelAvailableInServer(
|
||||
serverConfig.customModels,
|
||||
jsonBody?.model as string,
|
||||
ServiceProvider.Azure as string,
|
||||
[
|
||||
ServiceProvider.OpenAI,
|
||||
ServiceProvider.Azure,
|
||||
jsonBody?.model as string, // support provider-unspecified model
|
||||
],
|
||||
)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
|
@@ -14,6 +14,7 @@ const DANGER_CONFIG = {
|
||||
disableFastLink: serverConfig.disableFastLink,
|
||||
customModels: serverConfig.customModels,
|
||||
defaultModel: serverConfig.defaultModel,
|
||||
visionModels: serverConfig.visionModels,
|
||||
};
|
||||
|
||||
declare global {
|
||||
|
128
app/api/deepseek.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import {
|
||||
DEEPSEEK_BASE_URL,
|
||||
ApiPath,
|
||||
ModelProvider,
|
||||
ServiceProvider,
|
||||
} from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/api/auth";
|
||||
import { isModelNotavailableInServer } from "@/app/utils/model";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
export async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
console.log("[DeepSeek Route] params ", params);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const authResult = auth(req, ModelProvider.DeepSeek);
|
||||
if (authResult.error) {
|
||||
return NextResponse.json(authResult, {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request(req);
|
||||
return response;
|
||||
} catch (e) {
|
||||
console.error("[DeepSeek] ", e);
|
||||
return NextResponse.json(prettyObject(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function request(req: NextRequest) {
|
||||
const controller = new AbortController();
|
||||
|
||||
// alibaba use base url or just remove the path
|
||||
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.DeepSeek, "");
|
||||
|
||||
let baseUrl = serverConfig.deepseekUrl || DEEPSEEK_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",
|
||||
Authorization: req.headers.get("Authorization") ?? "",
|
||||
},
|
||||
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 clonedBody = await req.text();
|
||||
fetchOptions.body = clonedBody;
|
||||
|
||||
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
||||
|
||||
// not undefined and is false
|
||||
if (
|
||||
isModelNotavailableInServer(
|
||||
serverConfig.customModels,
|
||||
jsonBody?.model as string,
|
||||
ServiceProvider.DeepSeek as string,
|
||||
)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: `you are not allowed to use ${jsonBody?.model} model`,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[DeepSeek] filter`, e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const res = await fetch(fetchUrl, fetchOptions);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
129
app/api/glm.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import {
|
||||
CHATGLM_BASE_URL,
|
||||
ApiPath,
|
||||
ModelProvider,
|
||||
ServiceProvider,
|
||||
} from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/api/auth";
|
||||
import { isModelNotavailableInServer } from "@/app/utils/model";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
export async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
console.log("[GLM Route] params ", params);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const authResult = auth(req, ModelProvider.ChatGLM);
|
||||
if (authResult.error) {
|
||||
return NextResponse.json(authResult, {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request(req);
|
||||
return response;
|
||||
} catch (e) {
|
||||
console.error("[GLM] ", e);
|
||||
return NextResponse.json(prettyObject(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function request(req: NextRequest) {
|
||||
const controller = new AbortController();
|
||||
|
||||
// alibaba use base url or just remove the path
|
||||
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.ChatGLM, "");
|
||||
|
||||
let baseUrl = serverConfig.chatglmUrl || CHATGLM_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}`;
|
||||
console.log("[Fetch Url] ", fetchUrl);
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: req.headers.get("Authorization") ?? "",
|
||||
},
|
||||
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 clonedBody = await req.text();
|
||||
fetchOptions.body = clonedBody;
|
||||
|
||||
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
||||
|
||||
// not undefined and is false
|
||||
if (
|
||||
isModelNotavailableInServer(
|
||||
serverConfig.customModels,
|
||||
jsonBody?.model as string,
|
||||
ServiceProvider.ChatGLM as string,
|
||||
)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: `you are not allowed to use ${jsonBody?.model} model`,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[GLM] filter`, e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const res = await fetch(fetchUrl, fetchOptions);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
@@ -23,7 +23,8 @@ export async function handle(
|
||||
});
|
||||
}
|
||||
|
||||
const bearToken = req.headers.get("Authorization") ?? "";
|
||||
const bearToken =
|
||||
req.headers.get("x-goog-api-key") || req.headers.get("Authorization") || "";
|
||||
const token = bearToken.trim().replaceAll("Bearer ", "").trim();
|
||||
|
||||
const apiKey = token ? token : serverConfig.googleApiKey;
|
||||
@@ -91,8 +92,8 @@ async function request(req: NextRequest, apiKey: string) {
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
const fetchUrl = `${baseUrl}${path}?key=${apiKey}${
|
||||
req?.nextUrl?.searchParams?.get("alt") === "sse" ? "&alt=sse" : ""
|
||||
const fetchUrl = `${baseUrl}${path}${
|
||||
req?.nextUrl?.searchParams?.get("alt") === "sse" ? "?alt=sse" : ""
|
||||
}`;
|
||||
|
||||
console.log("[Fetch Url] ", fetchUrl);
|
||||
@@ -100,6 +101,9 @@ async function request(req: NextRequest, apiKey: string) {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Cache-Control": "no-store",
|
||||
"x-goog-api-key":
|
||||
req.headers.get("x-goog-api-key") ||
|
||||
(req.headers.get("Authorization") ?? "").replace("Bearer ", ""),
|
||||
},
|
||||
method: req.method,
|
||||
body: req.body,
|
||||
|
@@ -8,7 +8,7 @@ import {
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/api/auth";
|
||||
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||
import { isModelNotavailableInServer } from "@/app/utils/model";
|
||||
// iflytek
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
@@ -89,7 +89,7 @@ async function request(req: NextRequest) {
|
||||
|
||||
// not undefined and is false
|
||||
if (
|
||||
isModelAvailableInServer(
|
||||
isModelNotavailableInServer(
|
||||
serverConfig.customModels,
|
||||
jsonBody?.model as string,
|
||||
ServiceProvider.Iflytek as string,
|
||||
|
@@ -8,7 +8,7 @@ import {
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/api/auth";
|
||||
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||
import { isModelNotavailableInServer } from "@/app/utils/model";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
@@ -88,7 +88,7 @@ async function request(req: NextRequest) {
|
||||
|
||||
// not undefined and is false
|
||||
if (
|
||||
isModelAvailableInServer(
|
||||
isModelNotavailableInServer(
|
||||
serverConfig.customModels,
|
||||
jsonBody?.model as string,
|
||||
ServiceProvider.Moonshot as string,
|
||||
|
@@ -6,7 +6,7 @@ import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "./auth";
|
||||
import { requestOpenai } from "./common";
|
||||
|
||||
const ALLOWD_PATH = new Set(Object.values(OpenaiPath));
|
||||
const ALLOWED_PATH = new Set(Object.values(OpenaiPath));
|
||||
|
||||
function getModels(remoteModelRes: OpenAIListModelResponse) {
|
||||
const config = getServerSideConfig();
|
||||
@@ -14,7 +14,7 @@ function getModels(remoteModelRes: OpenAIListModelResponse) {
|
||||
if (config.disableGPT4) {
|
||||
remoteModelRes.data = remoteModelRes.data.filter(
|
||||
(m) =>
|
||||
!(m.id.startsWith("gpt-4") || m.id.startsWith("chatgpt-4o")) ||
|
||||
!(m.id.startsWith("gpt-4") || m.id.startsWith("chatgpt-4o") || m.id.startsWith("o1")) ||
|
||||
m.id.startsWith("gpt-4o-mini"),
|
||||
);
|
||||
}
|
||||
@@ -34,7 +34,7 @@ export async function handle(
|
||||
|
||||
const subpath = params.path.join("/");
|
||||
|
||||
if (!ALLOWD_PATH.has(subpath)) {
|
||||
if (!ALLOWED_PATH.has(subpath)) {
|
||||
console.log("[OpenAI Route] forbidden path ", subpath);
|
||||
return NextResponse.json(
|
||||
{
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
|
||||
export async function handle(
|
||||
req: NextRequest,
|
||||
@@ -9,6 +10,7 @@ export async function handle(
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
// remove path params from searchParams
|
||||
req.nextUrl.searchParams.delete("path");
|
||||
@@ -31,6 +33,18 @@ export async function handle(
|
||||
return true;
|
||||
}),
|
||||
);
|
||||
// if dalle3 use openai api key
|
||||
const baseUrl = req.headers.get("x-base-url");
|
||||
if (baseUrl?.includes("api.openai.com")) {
|
||||
if (!serverConfig.apiKey) {
|
||||
return NextResponse.json(
|
||||
{ error: "OpenAI API key not configured" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
headers.set("Authorization", `Bearer ${serverConfig.apiKey}`);
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const fetchOptions: RequestInit = {
|
||||
headers,
|
||||
|
@@ -6,7 +6,7 @@ const config = getServerSideConfig();
|
||||
|
||||
const mergedAllowedWebDavEndpoints = [
|
||||
...internalAllowedWebDavEndpoints,
|
||||
...config.allowedWebDevEndpoints,
|
||||
...config.allowedWebDavEndpoints,
|
||||
].filter((domain) => Boolean(domain.trim()));
|
||||
|
||||
const normalizeUrl = (url: string) => {
|
||||
|
128
app/api/xai.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import {
|
||||
XAI_BASE_URL,
|
||||
ApiPath,
|
||||
ModelProvider,
|
||||
ServiceProvider,
|
||||
} from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/api/auth";
|
||||
import { isModelNotavailableInServer } from "@/app/utils/model";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
export async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
console.log("[XAI Route] params ", params);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const authResult = auth(req, ModelProvider.XAI);
|
||||
if (authResult.error) {
|
||||
return NextResponse.json(authResult, {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request(req);
|
||||
return response;
|
||||
} catch (e) {
|
||||
console.error("[XAI] ", e);
|
||||
return NextResponse.json(prettyObject(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function request(req: NextRequest) {
|
||||
const controller = new AbortController();
|
||||
|
||||
// alibaba use base url or just remove the path
|
||||
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.XAI, "");
|
||||
|
||||
let baseUrl = serverConfig.xaiUrl || XAI_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",
|
||||
Authorization: req.headers.get("Authorization") ?? "",
|
||||
},
|
||||
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 clonedBody = await req.text();
|
||||
fetchOptions.body = clonedBody;
|
||||
|
||||
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
||||
|
||||
// not undefined and is false
|
||||
if (
|
||||
isModelNotavailableInServer(
|
||||
serverConfig.customModels,
|
||||
jsonBody?.model as string,
|
||||
ServiceProvider.XAI as string,
|
||||
)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: `you are not allowed to use ${jsonBody?.model} model`,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[XAI] filter`, e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
const res = await fetch(fetchUrl, fetchOptions);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
@@ -20,6 +20,9 @@ import { QwenApi } from "./platforms/alibaba";
|
||||
import { HunyuanApi } from "./platforms/tencent";
|
||||
import { MoonshotApi } from "./platforms/moonshot";
|
||||
import { SparkApi } from "./platforms/iflytek";
|
||||
import { DeepSeekApi } from "./platforms/deepseek";
|
||||
import { XAIApi } from "./platforms/xai";
|
||||
import { ChatGLMApi } from "./platforms/glm";
|
||||
|
||||
export const ROLES = ["system", "user", "assistant"] as const;
|
||||
export type MessageRole = (typeof ROLES)[number];
|
||||
@@ -68,7 +71,7 @@ export interface ChatOptions {
|
||||
config: LLMConfig;
|
||||
|
||||
onUpdate?: (message: string, chunk: string) => void;
|
||||
onFinish: (message: string) => void;
|
||||
onFinish: (message: string, responseRes: Response) => void;
|
||||
onError?: (err: Error) => void;
|
||||
onController?: (controller: AbortController) => void;
|
||||
onBeforeTool?: (tool: ChatMessageTool) => void;
|
||||
@@ -152,6 +155,15 @@ export class ClientApi {
|
||||
case ModelProvider.Iflytek:
|
||||
this.llm = new SparkApi();
|
||||
break;
|
||||
case ModelProvider.DeepSeek:
|
||||
this.llm = new DeepSeekApi();
|
||||
break;
|
||||
case ModelProvider.XAI:
|
||||
this.llm = new XAIApi();
|
||||
break;
|
||||
case ModelProvider.ChatGLM:
|
||||
this.llm = new ChatGLMApi();
|
||||
break;
|
||||
default:
|
||||
this.llm = new ChatGPTApi();
|
||||
}
|
||||
@@ -231,7 +243,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
|
||||
function getConfig() {
|
||||
const modelConfig = chatStore.currentSession().mask.modelConfig;
|
||||
const isGoogle = modelConfig.providerName == ServiceProvider.Google;
|
||||
const isGoogle = modelConfig.providerName === ServiceProvider.Google;
|
||||
const isAzure = modelConfig.providerName === ServiceProvider.Azure;
|
||||
const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic;
|
||||
const isBaidu = modelConfig.providerName == ServiceProvider.Baidu;
|
||||
@@ -239,6 +251,9 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
|
||||
const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot;
|
||||
const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek;
|
||||
const isDeepSeek = modelConfig.providerName === ServiceProvider.DeepSeek;
|
||||
const isXAI = modelConfig.providerName === ServiceProvider.XAI;
|
||||
const isChatGLM = modelConfig.providerName === ServiceProvider.ChatGLM;
|
||||
const isEnabledAccessControl = accessStore.enabledAccessControl();
|
||||
const apiKey = isGoogle
|
||||
? accessStore.googleApiKey
|
||||
@@ -252,6 +267,12 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
? accessStore.alibabaApiKey
|
||||
: isMoonshot
|
||||
? accessStore.moonshotApiKey
|
||||
: isXAI
|
||||
? accessStore.xaiApiKey
|
||||
: isDeepSeek
|
||||
? accessStore.deepseekApiKey
|
||||
: isChatGLM
|
||||
? accessStore.chatglmApiKey
|
||||
: isIflytek
|
||||
? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
|
||||
? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
|
||||
@@ -266,13 +287,22 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
isAlibaba,
|
||||
isMoonshot,
|
||||
isIflytek,
|
||||
isDeepSeek,
|
||||
isXAI,
|
||||
isChatGLM,
|
||||
apiKey,
|
||||
isEnabledAccessControl,
|
||||
};
|
||||
}
|
||||
|
||||
function getAuthHeader(): string {
|
||||
return isAzure ? "api-key" : isAnthropic ? "x-api-key" : "Authorization";
|
||||
return isAzure
|
||||
? "api-key"
|
||||
: isAnthropic
|
||||
? "x-api-key"
|
||||
: isGoogle
|
||||
? "x-goog-api-key"
|
||||
: "Authorization";
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -280,17 +310,25 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
isAzure,
|
||||
isAnthropic,
|
||||
isBaidu,
|
||||
isByteDance,
|
||||
isAlibaba,
|
||||
isMoonshot,
|
||||
isIflytek,
|
||||
isDeepSeek,
|
||||
isXAI,
|
||||
isChatGLM,
|
||||
apiKey,
|
||||
isEnabledAccessControl,
|
||||
} = getConfig();
|
||||
// when using google api in app, not set auth header
|
||||
if (isGoogle && clientConfig?.isApp) return headers;
|
||||
// when using baidu api in app, not set auth header
|
||||
if (isBaidu && clientConfig?.isApp) return headers;
|
||||
|
||||
const authHeader = getAuthHeader();
|
||||
|
||||
const bearerToken = getBearerToken(apiKey, isAzure || isAnthropic);
|
||||
const bearerToken = getBearerToken(
|
||||
apiKey,
|
||||
isAzure || isAnthropic || isGoogle,
|
||||
);
|
||||
|
||||
if (bearerToken) {
|
||||
headers[authHeader] = bearerToken;
|
||||
@@ -321,6 +359,12 @@ export function getClientApi(provider: ServiceProvider): ClientApi {
|
||||
return new ClientApi(ModelProvider.Moonshot);
|
||||
case ServiceProvider.Iflytek:
|
||||
return new ClientApi(ModelProvider.Iflytek);
|
||||
case ServiceProvider.DeepSeek:
|
||||
return new ClientApi(ModelProvider.DeepSeek);
|
||||
case ServiceProvider.XAI:
|
||||
return new ClientApi(ModelProvider.XAI);
|
||||
case ServiceProvider.ChatGLM:
|
||||
return new ClientApi(ModelProvider.ChatGLM);
|
||||
default:
|
||||
return new ClientApi(ModelProvider.GPT);
|
||||
}
|
||||
|
@@ -23,6 +23,7 @@ import {
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
object: string;
|
||||
@@ -142,6 +143,7 @@ export class QwenApi implements LLMApi {
|
||||
let responseText = "";
|
||||
let remainText = "";
|
||||
let finished = false;
|
||||
let responseRes: Response;
|
||||
|
||||
// animate response to make it looks smooth
|
||||
function animateResponseText() {
|
||||
@@ -171,13 +173,14 @@ export class QwenApi implements LLMApi {
|
||||
const finish = () => {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
options.onFinish(responseText + remainText, responseRes);
|
||||
}
|
||||
};
|
||||
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
fetch: fetch as any,
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
@@ -186,6 +189,7 @@ export class QwenApi implements LLMApi {
|
||||
"[Alibaba] request response content type: ",
|
||||
contentType,
|
||||
);
|
||||
responseRes = res;
|
||||
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
@@ -252,7 +256,7 @@ export class QwenApi implements LLMApi {
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
|
@@ -8,11 +8,12 @@ import {
|
||||
ChatMessageTool,
|
||||
} from "@/app/store";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { DEFAULT_API_HOST } from "@/app/constant";
|
||||
import { ANTHROPIC_BASE_URL } from "@/app/constant";
|
||||
import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
||||
import { preProcessImageContent, stream } from "@/app/utils/chat";
|
||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||
import { RequestPayload } from "./openai";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export type MultiBlockContent = {
|
||||
type: "image" | "text";
|
||||
@@ -316,13 +317,14 @@ export class ClaudeApi implements LLMApi {
|
||||
};
|
||||
|
||||
try {
|
||||
controller.signal.onabort = () => options.onFinish("");
|
||||
controller.signal.onabort = () =>
|
||||
options.onFinish("", new Response(null, { status: 400 }));
|
||||
|
||||
const res = await fetch(path, payload);
|
||||
const resJson = await res.json();
|
||||
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
options.onFinish(message, res);
|
||||
} catch (e) {
|
||||
console.error("failed to chat", e);
|
||||
options.onError?.(e as Error);
|
||||
@@ -388,9 +390,7 @@ export class ClaudeApi implements LLMApi {
|
||||
if (baseUrl.trim().length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
baseUrl = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/anthropic"
|
||||
: ApiPath.Anthropic;
|
||||
baseUrl = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic;
|
||||
}
|
||||
|
||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith("/api")) {
|
||||
|
@@ -24,6 +24,7 @@ import {
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
object: string;
|
||||
@@ -161,6 +162,7 @@ export class ErnieApi implements LLMApi {
|
||||
let responseText = "";
|
||||
let remainText = "";
|
||||
let finished = false;
|
||||
let responseRes: Response;
|
||||
|
||||
// animate response to make it looks smooth
|
||||
function animateResponseText() {
|
||||
@@ -190,19 +192,20 @@ export class ErnieApi implements LLMApi {
|
||||
const finish = () => {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
options.onFinish(responseText + remainText, responseRes);
|
||||
}
|
||||
};
|
||||
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
fetch: fetch as any,
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
const contentType = res.headers.get("content-type");
|
||||
console.log("[Baidu] request response content type: ", contentType);
|
||||
|
||||
responseRes = res;
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
return finish();
|
||||
@@ -265,7 +268,7 @@ export class ErnieApi implements LLMApi {
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = resJson?.result;
|
||||
options.onFinish(message);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
|
@@ -23,6 +23,7 @@ import {
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
object: string;
|
||||
@@ -129,6 +130,7 @@ export class DoubaoApi implements LLMApi {
|
||||
let responseText = "";
|
||||
let remainText = "";
|
||||
let finished = false;
|
||||
let responseRes: Response;
|
||||
|
||||
// animate response to make it looks smooth
|
||||
function animateResponseText() {
|
||||
@@ -158,13 +160,14 @@ export class DoubaoApi implements LLMApi {
|
||||
const finish = () => {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
options.onFinish(responseText + remainText, responseRes);
|
||||
}
|
||||
};
|
||||
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
fetch: fetch as any,
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
@@ -173,7 +176,7 @@ export class DoubaoApi implements LLMApi {
|
||||
"[ByteDance] request response content type: ",
|
||||
contentType,
|
||||
);
|
||||
|
||||
responseRes = res;
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
return finish();
|
||||
@@ -239,7 +242,7 @@ export class DoubaoApi implements LLMApi {
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
|
200
app/client/platforms/deepseek.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
"use client";
|
||||
// azure and openai, using same models. so using same LLMApi.
|
||||
import {
|
||||
ApiPath,
|
||||
DEEPSEEK_BASE_URL,
|
||||
DeepSeek,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
} from "@/app/constant";
|
||||
import {
|
||||
useAccessStore,
|
||||
useAppConfig,
|
||||
useChatStore,
|
||||
ChatMessageTool,
|
||||
usePluginStore,
|
||||
} from "@/app/store";
|
||||
import { stream } from "@/app/utils/chat";
|
||||
import {
|
||||
ChatOptions,
|
||||
getHeaders,
|
||||
LLMApi,
|
||||
LLMModel,
|
||||
SpeechOptions,
|
||||
} from "../api";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import { RequestPayload } from "./openai";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export class DeepSeekApi implements LLMApi {
|
||||
private disableListModels = true;
|
||||
|
||||
path(path: string): string {
|
||||
const accessStore = useAccessStore.getState();
|
||||
|
||||
let baseUrl = "";
|
||||
|
||||
if (accessStore.useCustomConfig) {
|
||||
baseUrl = accessStore.deepseekUrl;
|
||||
}
|
||||
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
const apiPath = ApiPath.DeepSeek;
|
||||
baseUrl = isApp ? DEEPSEEK_BASE_URL : apiPath;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||
}
|
||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.DeepSeek)) {
|
||||
baseUrl = "https://" + baseUrl;
|
||||
}
|
||||
|
||||
console.log("[Proxy Endpoint] ", baseUrl, path);
|
||||
|
||||
return [baseUrl, path].join("/");
|
||||
}
|
||||
|
||||
extractMessage(res: any) {
|
||||
return res.choices?.at(0)?.message?.content ?? "";
|
||||
}
|
||||
|
||||
speech(options: SpeechOptions): Promise<ArrayBuffer> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
const messages: ChatOptions["messages"] = [];
|
||||
for (const v of options.messages) {
|
||||
const content = getMessageTextContent(v);
|
||||
messages.push({ role: v.role, content });
|
||||
}
|
||||
|
||||
const modelConfig = {
|
||||
...useAppConfig.getState().modelConfig,
|
||||
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||
...{
|
||||
model: options.config.model,
|
||||
providerName: options.config.providerName,
|
||||
},
|
||||
};
|
||||
|
||||
const requestPayload: RequestPayload = {
|
||||
messages,
|
||||
stream: options.config.stream,
|
||||
model: modelConfig.model,
|
||||
temperature: modelConfig.temperature,
|
||||
presence_penalty: modelConfig.presence_penalty,
|
||||
frequency_penalty: modelConfig.frequency_penalty,
|
||||
top_p: modelConfig.top_p,
|
||||
// max_tokens: Math.max(modelConfig.max_tokens, 1024),
|
||||
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
|
||||
};
|
||||
|
||||
console.log("[Request] openai payload: ", requestPayload);
|
||||
|
||||
const shouldStream = !!options.config.stream;
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
|
||||
try {
|
||||
const chatPath = this.path(DeepSeek.ChatPath);
|
||||
const chatPayload = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(requestPayload),
|
||||
signal: controller.signal,
|
||||
headers: getHeaders(),
|
||||
};
|
||||
|
||||
// make a fetch request
|
||||
const requestTimeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
REQUEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
if (shouldStream) {
|
||||
const [tools, funcs] = usePluginStore
|
||||
.getState()
|
||||
.getAsTools(
|
||||
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||
);
|
||||
return stream(
|
||||
chatPath,
|
||||
requestPayload,
|
||||
getHeaders(),
|
||||
tools as any,
|
||||
funcs,
|
||||
controller,
|
||||
// parseSSE
|
||||
(text: string, runTools: ChatMessageTool[]) => {
|
||||
// console.log("parseSSE", text, runTools);
|
||||
const json = JSON.parse(text);
|
||||
const choices = json.choices as Array<{
|
||||
delta: {
|
||||
content: string;
|
||||
tool_calls: ChatMessageTool[];
|
||||
};
|
||||
}>;
|
||||
const tool_calls = choices[0]?.delta?.tool_calls;
|
||||
if (tool_calls?.length > 0) {
|
||||
const index = tool_calls[0]?.index;
|
||||
const id = tool_calls[0]?.id;
|
||||
const args = tool_calls[0]?.function?.arguments;
|
||||
if (id) {
|
||||
runTools.push({
|
||||
id,
|
||||
type: tool_calls[0]?.type,
|
||||
function: {
|
||||
name: tool_calls[0]?.function?.name as string,
|
||||
arguments: args,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// @ts-ignore
|
||||
runTools[index]["function"]["arguments"] += args;
|
||||
}
|
||||
}
|
||||
return choices[0]?.delta?.content;
|
||||
},
|
||||
// processToolMessage, include tool_calls message and tool call results
|
||||
(
|
||||
requestPayload: RequestPayload,
|
||||
toolCallMessage: any,
|
||||
toolCallResult: any[],
|
||||
) => {
|
||||
// @ts-ignore
|
||||
requestPayload?.messages?.splice(
|
||||
// @ts-ignore
|
||||
requestPayload?.messages?.length,
|
||||
0,
|
||||
toolCallMessage,
|
||||
...toolCallResult,
|
||||
);
|
||||
},
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
const res = await fetch(chatPath, chatPayload);
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
options.onError?.(e as Error);
|
||||
}
|
||||
}
|
||||
async usage() {
|
||||
return {
|
||||
used: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async models(): Promise<LLMModel[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
293
app/client/platforms/glm.ts
Normal file
@@ -0,0 +1,293 @@
|
||||
"use client";
|
||||
import {
|
||||
ApiPath,
|
||||
CHATGLM_BASE_URL,
|
||||
ChatGLM,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
} from "@/app/constant";
|
||||
import {
|
||||
useAccessStore,
|
||||
useAppConfig,
|
||||
useChatStore,
|
||||
ChatMessageTool,
|
||||
usePluginStore,
|
||||
} from "@/app/store";
|
||||
import { stream } from "@/app/utils/chat";
|
||||
import {
|
||||
ChatOptions,
|
||||
getHeaders,
|
||||
LLMApi,
|
||||
LLMModel,
|
||||
SpeechOptions,
|
||||
} from "../api";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
||||
import { RequestPayload } from "./openai";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
import { preProcessImageContent } from "@/app/utils/chat";
|
||||
|
||||
interface BasePayload {
|
||||
model: string;
|
||||
}
|
||||
|
||||
interface ChatPayload extends BasePayload {
|
||||
messages: ChatOptions["messages"];
|
||||
stream?: boolean;
|
||||
temperature?: number;
|
||||
presence_penalty?: number;
|
||||
frequency_penalty?: number;
|
||||
top_p?: number;
|
||||
}
|
||||
|
||||
interface ImageGenerationPayload extends BasePayload {
|
||||
prompt: string;
|
||||
size?: string;
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
interface VideoGenerationPayload extends BasePayload {
|
||||
prompt: string;
|
||||
duration?: number;
|
||||
resolution?: string;
|
||||
user_id?: string;
|
||||
}
|
||||
|
||||
type ModelType = "chat" | "image" | "video";
|
||||
|
||||
export class ChatGLMApi implements LLMApi {
|
||||
private disableListModels = true;
|
||||
|
||||
private getModelType(model: string): ModelType {
|
||||
if (model.startsWith("cogview-")) return "image";
|
||||
if (model.startsWith("cogvideo-")) return "video";
|
||||
return "chat";
|
||||
}
|
||||
|
||||
private getModelPath(type: ModelType): string {
|
||||
switch (type) {
|
||||
case "image":
|
||||
return ChatGLM.ImagePath;
|
||||
case "video":
|
||||
return ChatGLM.VideoPath;
|
||||
default:
|
||||
return ChatGLM.ChatPath;
|
||||
}
|
||||
}
|
||||
|
||||
private createPayload(
|
||||
messages: ChatOptions["messages"],
|
||||
modelConfig: any,
|
||||
options: ChatOptions,
|
||||
): BasePayload {
|
||||
const modelType = this.getModelType(modelConfig.model);
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
const prompt =
|
||||
typeof lastMessage.content === "string"
|
||||
? lastMessage.content
|
||||
: lastMessage.content.map((c) => c.text).join("\n");
|
||||
|
||||
switch (modelType) {
|
||||
case "image":
|
||||
return {
|
||||
model: modelConfig.model,
|
||||
prompt,
|
||||
size: options.config.size,
|
||||
} as ImageGenerationPayload;
|
||||
default:
|
||||
return {
|
||||
messages,
|
||||
stream: options.config.stream,
|
||||
model: modelConfig.model,
|
||||
temperature: modelConfig.temperature,
|
||||
presence_penalty: modelConfig.presence_penalty,
|
||||
frequency_penalty: modelConfig.frequency_penalty,
|
||||
top_p: modelConfig.top_p,
|
||||
} as ChatPayload;
|
||||
}
|
||||
}
|
||||
|
||||
private parseResponse(modelType: ModelType, json: any): string {
|
||||
switch (modelType) {
|
||||
case "image": {
|
||||
const imageUrl = json.data?.[0]?.url;
|
||||
return imageUrl ? `` : "";
|
||||
}
|
||||
case "video": {
|
||||
const videoUrl = json.data?.[0]?.url;
|
||||
return videoUrl ? `<video controls src="${videoUrl}"></video>` : "";
|
||||
}
|
||||
default:
|
||||
return this.extractMessage(json);
|
||||
}
|
||||
}
|
||||
|
||||
path(path: string): string {
|
||||
const accessStore = useAccessStore.getState();
|
||||
let baseUrl = "";
|
||||
|
||||
if (accessStore.useCustomConfig) {
|
||||
baseUrl = accessStore.chatglmUrl;
|
||||
}
|
||||
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
const apiPath = ApiPath.ChatGLM;
|
||||
baseUrl = isApp ? CHATGLM_BASE_URL : apiPath;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||
}
|
||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.ChatGLM)) {
|
||||
baseUrl = "https://" + baseUrl;
|
||||
}
|
||||
|
||||
console.log("[Proxy Endpoint] ", baseUrl, path);
|
||||
return [baseUrl, path].join("/");
|
||||
}
|
||||
|
||||
extractMessage(res: any) {
|
||||
return res.choices?.at(0)?.message?.content ?? "";
|
||||
}
|
||||
|
||||
speech(options: SpeechOptions): Promise<ArrayBuffer> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
const visionModel = isVisionModel(options.config.model);
|
||||
const messages: ChatOptions["messages"] = [];
|
||||
for (const v of options.messages) {
|
||||
const content = visionModel
|
||||
? await preProcessImageContent(v.content)
|
||||
: getMessageTextContent(v);
|
||||
messages.push({ role: v.role, content });
|
||||
}
|
||||
|
||||
const modelConfig = {
|
||||
...useAppConfig.getState().modelConfig,
|
||||
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||
...{
|
||||
model: options.config.model,
|
||||
providerName: options.config.providerName,
|
||||
},
|
||||
};
|
||||
const modelType = this.getModelType(modelConfig.model);
|
||||
const requestPayload = this.createPayload(messages, modelConfig, options);
|
||||
const path = this.path(this.getModelPath(modelType));
|
||||
|
||||
console.log(`[Request] glm ${modelType} payload: `, requestPayload);
|
||||
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
|
||||
try {
|
||||
const chatPayload = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(requestPayload),
|
||||
signal: controller.signal,
|
||||
headers: getHeaders(),
|
||||
};
|
||||
|
||||
const requestTimeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
REQUEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
if (modelType === "image" || modelType === "video") {
|
||||
const res = await fetch(path, chatPayload);
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
const resJson = await res.json();
|
||||
console.log(`[Response] glm ${modelType}:`, resJson);
|
||||
const message = this.parseResponse(modelType, resJson);
|
||||
options.onFinish(message, res);
|
||||
return;
|
||||
}
|
||||
|
||||
const shouldStream = !!options.config.stream;
|
||||
if (shouldStream) {
|
||||
const [tools, funcs] = usePluginStore
|
||||
.getState()
|
||||
.getAsTools(
|
||||
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||
);
|
||||
return stream(
|
||||
path,
|
||||
requestPayload,
|
||||
getHeaders(),
|
||||
tools as any,
|
||||
funcs,
|
||||
controller,
|
||||
// parseSSE
|
||||
(text: string, runTools: ChatMessageTool[]) => {
|
||||
const json = JSON.parse(text);
|
||||
const choices = json.choices as Array<{
|
||||
delta: {
|
||||
content: string;
|
||||
tool_calls: ChatMessageTool[];
|
||||
};
|
||||
}>;
|
||||
const tool_calls = choices[0]?.delta?.tool_calls;
|
||||
if (tool_calls?.length > 0) {
|
||||
const index = tool_calls[0]?.index;
|
||||
const id = tool_calls[0]?.id;
|
||||
const args = tool_calls[0]?.function?.arguments;
|
||||
if (id) {
|
||||
runTools.push({
|
||||
id,
|
||||
type: tool_calls[0]?.type,
|
||||
function: {
|
||||
name: tool_calls[0]?.function?.name as string,
|
||||
arguments: args,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// @ts-ignore
|
||||
runTools[index]["function"]["arguments"] += args;
|
||||
}
|
||||
}
|
||||
return choices[0]?.delta?.content;
|
||||
},
|
||||
// processToolMessage
|
||||
(
|
||||
requestPayload: RequestPayload,
|
||||
toolCallMessage: any,
|
||||
toolCallResult: any[],
|
||||
) => {
|
||||
// @ts-ignore
|
||||
requestPayload?.messages?.splice(
|
||||
// @ts-ignore
|
||||
requestPayload?.messages?.length,
|
||||
0,
|
||||
toolCallMessage,
|
||||
...toolCallResult,
|
||||
);
|
||||
},
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
const res = await fetch(path, chatPayload);
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
options.onError?.(e as Error);
|
||||
}
|
||||
}
|
||||
|
||||
async usage() {
|
||||
return {
|
||||
used: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async models(): Promise<LLMModel[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
@@ -7,24 +7,29 @@ import {
|
||||
LLMUsage,
|
||||
SpeechOptions,
|
||||
} from "../api";
|
||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { DEFAULT_API_HOST } from "@/app/constant";
|
||||
import Locale from "../../locales";
|
||||
import {
|
||||
EventStreamContentType,
|
||||
fetchEventSource,
|
||||
} from "@fortaine/fetch-event-source";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
useAccessStore,
|
||||
useAppConfig,
|
||||
useChatStore,
|
||||
usePluginStore,
|
||||
ChatMessageTool,
|
||||
} from "@/app/store";
|
||||
import { stream } from "@/app/utils/chat";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { GEMINI_BASE_URL } from "@/app/constant";
|
||||
|
||||
import {
|
||||
getMessageTextContent,
|
||||
getMessageImages,
|
||||
isVisionModel,
|
||||
} from "@/app/utils";
|
||||
import { preProcessImageContent } from "@/app/utils/chat";
|
||||
import { nanoid } from "nanoid";
|
||||
import { RequestPayload } from "./openai";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export class GeminiProApi implements LLMApi {
|
||||
path(path: string): string {
|
||||
path(path: string, shouldStream = false): string {
|
||||
const accessStore = useAccessStore.getState();
|
||||
|
||||
let baseUrl = "";
|
||||
@@ -34,7 +39,7 @@ export class GeminiProApi implements LLMApi {
|
||||
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
if (baseUrl.length === 0) {
|
||||
baseUrl = isApp ? DEFAULT_API_HOST + `/api/proxy/google` : ApiPath.Google;
|
||||
baseUrl = isApp ? GEMINI_BASE_URL : ApiPath.Google;
|
||||
}
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||
@@ -46,19 +51,27 @@ export class GeminiProApi implements LLMApi {
|
||||
console.log("[Proxy Endpoint] ", baseUrl, path);
|
||||
|
||||
let chatPath = [baseUrl, path].join("/");
|
||||
|
||||
chatPath += chatPath.includes("?") ? "&alt=sse" : "?alt=sse";
|
||||
// if chatPath.startsWith('http') then add key in query string
|
||||
if (chatPath.startsWith("http") && accessStore.googleApiKey) {
|
||||
chatPath += `&key=${accessStore.googleApiKey}`;
|
||||
if (shouldStream) {
|
||||
chatPath += chatPath.includes("?") ? "&alt=sse" : "?alt=sse";
|
||||
}
|
||||
|
||||
return chatPath;
|
||||
}
|
||||
extractMessage(res: any) {
|
||||
console.log("[Response] gemini-pro response: ", res);
|
||||
|
||||
const getTextFromParts = (parts: any[]) => {
|
||||
if (!Array.isArray(parts)) return "";
|
||||
|
||||
return parts
|
||||
.map((part) => part?.text || "")
|
||||
.filter((text) => text.trim() !== "")
|
||||
.join("\n\n");
|
||||
};
|
||||
|
||||
return (
|
||||
res?.candidates?.at(0)?.content?.parts.at(0)?.text ||
|
||||
getTextFromParts(res?.candidates?.at(0)?.content?.parts) ||
|
||||
getTextFromParts(res?.at(0)?.candidates?.at(0)?.content?.parts) ||
|
||||
res?.error?.message ||
|
||||
""
|
||||
);
|
||||
@@ -165,7 +178,10 @@ export class GeminiProApi implements LLMApi {
|
||||
options.onController?.(controller);
|
||||
try {
|
||||
// https://github.com/google-gemini/cookbook/blob/main/quickstarts/rest/Streaming_REST.ipynb
|
||||
const chatPath = this.path(Google.ChatPath(modelConfig.model));
|
||||
const chatPath = this.path(
|
||||
Google.ChatPath(modelConfig.model),
|
||||
shouldStream,
|
||||
);
|
||||
|
||||
const chatPayload = {
|
||||
method: "POST",
|
||||
@@ -181,114 +197,87 @@ export class GeminiProApi implements LLMApi {
|
||||
);
|
||||
|
||||
if (shouldStream) {
|
||||
let responseText = "";
|
||||
let remainText = "";
|
||||
let finished = false;
|
||||
const [tools, funcs] = usePluginStore
|
||||
.getState()
|
||||
.getAsTools(
|
||||
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||
);
|
||||
return stream(
|
||||
chatPath,
|
||||
requestPayload,
|
||||
getHeaders(),
|
||||
// @ts-ignore
|
||||
tools.length > 0
|
||||
? // @ts-ignore
|
||||
[{ functionDeclarations: tools.map((tool) => tool.function) }]
|
||||
: [],
|
||||
funcs,
|
||||
controller,
|
||||
// parseSSE
|
||||
(text: string, runTools: ChatMessageTool[]) => {
|
||||
// console.log("parseSSE", text, runTools);
|
||||
const chunkJson = JSON.parse(text);
|
||||
|
||||
const finish = () => {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
}
|
||||
};
|
||||
|
||||
// animate response to make it looks smooth
|
||||
function animateResponseText() {
|
||||
if (finished || controller.signal.aborted) {
|
||||
responseText += remainText;
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
if (remainText.length > 0) {
|
||||
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
|
||||
const fetchText = remainText.slice(0, fetchCount);
|
||||
responseText += fetchText;
|
||||
remainText = remainText.slice(fetchCount);
|
||||
options.onUpdate?.(responseText, fetchText);
|
||||
}
|
||||
|
||||
requestAnimationFrame(animateResponseText);
|
||||
}
|
||||
|
||||
// start animaion
|
||||
animateResponseText();
|
||||
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
const contentType = res.headers.get("content-type");
|
||||
console.log(
|
||||
"[Gemini] request response content type: ",
|
||||
contentType,
|
||||
const functionCall = chunkJson?.candidates
|
||||
?.at(0)
|
||||
?.content.parts.at(0)?.functionCall;
|
||||
if (functionCall) {
|
||||
const { name, args } = functionCall;
|
||||
runTools.push({
|
||||
id: nanoid(),
|
||||
type: "function",
|
||||
function: {
|
||||
name,
|
||||
arguments: JSON.stringify(args), // utils.chat call function, using JSON.parse
|
||||
},
|
||||
});
|
||||
}
|
||||
return chunkJson?.candidates
|
||||
?.at(0)
|
||||
?.content.parts?.map((part: { text: string }) => part.text)
|
||||
.join("\n\n");
|
||||
},
|
||||
// processToolMessage, include tool_calls message and tool call results
|
||||
(
|
||||
requestPayload: RequestPayload,
|
||||
toolCallMessage: any,
|
||||
toolCallResult: any[],
|
||||
) => {
|
||||
// @ts-ignore
|
||||
requestPayload?.contents?.splice(
|
||||
// @ts-ignore
|
||||
requestPayload?.contents?.length,
|
||||
0,
|
||||
{
|
||||
role: "model",
|
||||
parts: toolCallMessage.tool_calls.map(
|
||||
(tool: ChatMessageTool) => ({
|
||||
functionCall: {
|
||||
name: tool?.function?.name,
|
||||
args: JSON.parse(tool?.function?.arguments as string),
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
// @ts-ignore
|
||||
...toolCallResult.map((result) => ({
|
||||
role: "function",
|
||||
parts: [
|
||||
{
|
||||
functionResponse: {
|
||||
name: result.name,
|
||||
response: {
|
||||
name: result.name,
|
||||
content: result.content, // TODO just text content...
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
})),
|
||||
);
|
||||
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
return finish();
|
||||
}
|
||||
|
||||
if (
|
||||
!res.ok ||
|
||||
!res.headers
|
||||
.get("content-type")
|
||||
?.startsWith(EventStreamContentType) ||
|
||||
res.status !== 200
|
||||
) {
|
||||
const responseTexts = [responseText];
|
||||
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);
|
||||
}
|
||||
|
||||
responseText = responseTexts.join("\n\n");
|
||||
|
||||
return finish();
|
||||
}
|
||||
},
|
||||
onmessage(msg) {
|
||||
if (msg.data === "[DONE]" || finished) {
|
||||
return finish();
|
||||
}
|
||||
const text = msg.data;
|
||||
try {
|
||||
const json = JSON.parse(text);
|
||||
const delta = apiClient.extractMessage(json);
|
||||
|
||||
if (delta) {
|
||||
remainText += delta;
|
||||
}
|
||||
|
||||
const blockReason = json?.promptFeedback?.blockReason;
|
||||
if (blockReason) {
|
||||
// being blocked
|
||||
console.log(`[Google] [Safety Ratings] result:`, blockReason);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[Request] parse error", text, msg);
|
||||
}
|
||||
},
|
||||
onclose() {
|
||||
finish();
|
||||
},
|
||||
onerror(e) {
|
||||
options.onError?.(e);
|
||||
throw e;
|
||||
},
|
||||
openWhenHidden: true,
|
||||
});
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
const res = await fetch(chatPath, chatPayload);
|
||||
clearTimeout(requestTimeoutId);
|
||||
@@ -303,7 +292,7 @@ export class GeminiProApi implements LLMApi {
|
||||
);
|
||||
}
|
||||
const message = apiClient.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
|
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import {
|
||||
ApiPath,
|
||||
DEFAULT_API_HOST,
|
||||
IFLYTEK_BASE_URL,
|
||||
Iflytek,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
} from "@/app/constant";
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
import { RequestPayload } from "./openai";
|
||||
|
||||
@@ -40,7 +41,7 @@ export class SparkApi implements LLMApi {
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
const apiPath = ApiPath.Iflytek;
|
||||
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
|
||||
baseUrl = isApp ? IFLYTEK_BASE_URL : apiPath;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
@@ -116,6 +117,7 @@ export class SparkApi implements LLMApi {
|
||||
let responseText = "";
|
||||
let remainText = "";
|
||||
let finished = false;
|
||||
let responseRes: Response;
|
||||
|
||||
// Animate response text to make it look smooth
|
||||
function animateResponseText() {
|
||||
@@ -142,19 +144,20 @@ export class SparkApi implements LLMApi {
|
||||
const finish = () => {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
options.onFinish(responseText + remainText, responseRes);
|
||||
}
|
||||
};
|
||||
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
fetch: fetch as any,
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
const contentType = res.headers.get("content-type");
|
||||
console.log("[Spark] request response content type: ", contentType);
|
||||
|
||||
responseRes = res;
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
return finish();
|
||||
@@ -229,7 +232,7 @@ export class SparkApi implements LLMApi {
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
|
@@ -2,7 +2,7 @@
|
||||
// azure and openai, using same models. so using same LLMApi.
|
||||
import {
|
||||
ApiPath,
|
||||
DEFAULT_API_HOST,
|
||||
MOONSHOT_BASE_URL,
|
||||
Moonshot,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
} from "@/app/constant";
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import { RequestPayload } from "./openai";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export class MoonshotApi implements LLMApi {
|
||||
private disableListModels = true;
|
||||
@@ -40,7 +41,7 @@ export class MoonshotApi implements LLMApi {
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
const apiPath = ApiPath.Moonshot;
|
||||
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
|
||||
baseUrl = isApp ? MOONSHOT_BASE_URL : apiPath;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
@@ -179,7 +180,7 @@ export class MoonshotApi implements LLMApi {
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
|
@@ -2,7 +2,7 @@
|
||||
// azure and openai, using same models. so using same LLMApi.
|
||||
import {
|
||||
ApiPath,
|
||||
DEFAULT_API_HOST,
|
||||
OPENAI_BASE_URL,
|
||||
DEFAULT_MODELS,
|
||||
OpenaiPath,
|
||||
Azure,
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
stream,
|
||||
} from "@/app/utils/chat";
|
||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||
import { DalleSize, DalleQuality, DalleStyle } from "@/app/typing";
|
||||
import { ModelSize, DalleQuality, DalleStyle } from "@/app/typing";
|
||||
|
||||
import {
|
||||
ChatOptions,
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
isVisionModel,
|
||||
isDalle3 as _isDalle3,
|
||||
} from "@/app/utils";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
object: string;
|
||||
@@ -64,6 +65,7 @@ export interface RequestPayload {
|
||||
frequency_penalty: number;
|
||||
top_p: number;
|
||||
max_tokens?: number;
|
||||
max_completion_tokens?: number;
|
||||
}
|
||||
|
||||
export interface DalleRequestPayload {
|
||||
@@ -71,7 +73,7 @@ export interface DalleRequestPayload {
|
||||
prompt: string;
|
||||
response_format: "url" | "b64_json";
|
||||
n: number;
|
||||
size: DalleSize;
|
||||
size: ModelSize;
|
||||
quality: DalleQuality;
|
||||
style: DalleStyle;
|
||||
}
|
||||
@@ -98,7 +100,7 @@ export class ChatGPTApi implements LLMApi {
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
const apiPath = isAzure ? ApiPath.Azure : ApiPath.OpenAI;
|
||||
baseUrl = isApp ? DEFAULT_API_HOST + "/proxy" + apiPath : apiPath;
|
||||
baseUrl = isApp ? OPENAI_BASE_URL : apiPath;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
@@ -222,7 +224,7 @@ export class ChatGPTApi implements LLMApi {
|
||||
// O1 not support image, tools (plugin in ChatGPTNextWeb) and system, stream, logprobs, temperature, top_p, n, presence_penalty, frequency_penalty yet.
|
||||
requestPayload = {
|
||||
messages,
|
||||
stream: !isO1 ? options.config.stream : false,
|
||||
stream: options.config.stream,
|
||||
model: modelConfig.model,
|
||||
temperature: !isO1 ? modelConfig.temperature : 1,
|
||||
presence_penalty: !isO1 ? modelConfig.presence_penalty : 0,
|
||||
@@ -232,6 +234,11 @@ export class ChatGPTApi implements LLMApi {
|
||||
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
|
||||
};
|
||||
|
||||
// O1 使用 max_completion_tokens 控制token数 (https://platform.openai.com/docs/guides/reasoning#controlling-costs)
|
||||
if (isO1) {
|
||||
requestPayload["max_completion_tokens"] = modelConfig.max_tokens;
|
||||
}
|
||||
|
||||
// add max_tokens to vision model
|
||||
if (visionModel) {
|
||||
requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
|
||||
@@ -240,7 +247,7 @@ export class ChatGPTApi implements LLMApi {
|
||||
|
||||
console.log("[Request] openai payload: ", requestPayload);
|
||||
|
||||
const shouldStream = !isDalle3 && !!options.config.stream && !isO1;
|
||||
const shouldStream = !isDalle3 && !!options.config.stream;
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
|
||||
@@ -277,6 +284,7 @@ export class ChatGPTApi implements LLMApi {
|
||||
);
|
||||
}
|
||||
if (shouldStream) {
|
||||
let index = -1;
|
||||
const [tools, funcs] = usePluginStore
|
||||
.getState()
|
||||
.getAsTools(
|
||||
@@ -302,10 +310,10 @@ export class ChatGPTApi implements LLMApi {
|
||||
}>;
|
||||
const tool_calls = choices[0]?.delta?.tool_calls;
|
||||
if (tool_calls?.length > 0) {
|
||||
const index = tool_calls[0]?.index;
|
||||
const id = tool_calls[0]?.id;
|
||||
const args = tool_calls[0]?.function?.arguments;
|
||||
if (id) {
|
||||
index += 1;
|
||||
runTools.push({
|
||||
id,
|
||||
type: tool_calls[0]?.type,
|
||||
@@ -327,6 +335,8 @@ export class ChatGPTApi implements LLMApi {
|
||||
toolCallMessage: any,
|
||||
toolCallResult: any[],
|
||||
) => {
|
||||
// reset index value
|
||||
index = -1;
|
||||
// @ts-ignore
|
||||
requestPayload?.messages?.splice(
|
||||
// @ts-ignore
|
||||
@@ -349,7 +359,7 @@ export class ChatGPTApi implements LLMApi {
|
||||
// make a fetch request
|
||||
const requestTimeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
|
||||
isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 4 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
|
||||
);
|
||||
|
||||
const res = await fetch(chatPath, chatPayload);
|
||||
@@ -357,7 +367,7 @@ export class ChatGPTApi implements LLMApi {
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = await this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
|
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { ApiPath, DEFAULT_API_HOST, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||
import { ApiPath, TENCENT_BASE_URL, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store";
|
||||
|
||||
import {
|
||||
@@ -22,6 +22,7 @@ import mapKeys from "lodash-es/mapKeys";
|
||||
import mapValues from "lodash-es/mapValues";
|
||||
import isArray from "lodash-es/isArray";
|
||||
import isObject from "lodash-es/isObject";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
object: string;
|
||||
@@ -70,9 +71,7 @@ export class HunyuanApi implements LLMApi {
|
||||
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
baseUrl = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/tencent"
|
||||
: ApiPath.Tencent;
|
||||
baseUrl = isApp ? TENCENT_BASE_URL : ApiPath.Tencent;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
@@ -143,6 +142,7 @@ export class HunyuanApi implements LLMApi {
|
||||
let responseText = "";
|
||||
let remainText = "";
|
||||
let finished = false;
|
||||
let responseRes: Response;
|
||||
|
||||
// animate response to make it looks smooth
|
||||
function animateResponseText() {
|
||||
@@ -172,13 +172,14 @@ export class HunyuanApi implements LLMApi {
|
||||
const finish = () => {
|
||||
if (!finished) {
|
||||
finished = true;
|
||||
options.onFinish(responseText + remainText);
|
||||
options.onFinish(responseText + remainText, responseRes);
|
||||
}
|
||||
};
|
||||
|
||||
controller.signal.onabort = finish;
|
||||
|
||||
fetchEventSource(chatPath, {
|
||||
fetch: fetch as any,
|
||||
...chatPayload,
|
||||
async onopen(res) {
|
||||
clearTimeout(requestTimeoutId);
|
||||
@@ -187,7 +188,7 @@ export class HunyuanApi implements LLMApi {
|
||||
"[Tencent] request response content type: ",
|
||||
contentType,
|
||||
);
|
||||
|
||||
responseRes = res;
|
||||
if (contentType?.startsWith("text/plain")) {
|
||||
responseText = await res.clone().text();
|
||||
return finish();
|
||||
@@ -253,7 +254,7 @@ export class HunyuanApi implements LLMApi {
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
|
193
app/client/platforms/xai.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
"use client";
|
||||
// azure and openai, using same models. so using same LLMApi.
|
||||
import { ApiPath, XAI_BASE_URL, XAI, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||
import {
|
||||
useAccessStore,
|
||||
useAppConfig,
|
||||
useChatStore,
|
||||
ChatMessageTool,
|
||||
usePluginStore,
|
||||
} from "@/app/store";
|
||||
import { stream } from "@/app/utils/chat";
|
||||
import {
|
||||
ChatOptions,
|
||||
getHeaders,
|
||||
LLMApi,
|
||||
LLMModel,
|
||||
SpeechOptions,
|
||||
} from "../api";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import { RequestPayload } from "./openai";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export class XAIApi implements LLMApi {
|
||||
private disableListModels = true;
|
||||
|
||||
path(path: string): string {
|
||||
const accessStore = useAccessStore.getState();
|
||||
|
||||
let baseUrl = "";
|
||||
|
||||
if (accessStore.useCustomConfig) {
|
||||
baseUrl = accessStore.xaiUrl;
|
||||
}
|
||||
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
const apiPath = ApiPath.XAI;
|
||||
baseUrl = isApp ? XAI_BASE_URL : apiPath;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||
}
|
||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.XAI)) {
|
||||
baseUrl = "https://" + baseUrl;
|
||||
}
|
||||
|
||||
console.log("[Proxy Endpoint] ", baseUrl, path);
|
||||
|
||||
return [baseUrl, path].join("/");
|
||||
}
|
||||
|
||||
extractMessage(res: any) {
|
||||
return res.choices?.at(0)?.message?.content ?? "";
|
||||
}
|
||||
|
||||
speech(options: SpeechOptions): Promise<ArrayBuffer> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
const messages: ChatOptions["messages"] = [];
|
||||
for (const v of options.messages) {
|
||||
const content = getMessageTextContent(v);
|
||||
messages.push({ role: v.role, content });
|
||||
}
|
||||
|
||||
const modelConfig = {
|
||||
...useAppConfig.getState().modelConfig,
|
||||
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||
...{
|
||||
model: options.config.model,
|
||||
providerName: options.config.providerName,
|
||||
},
|
||||
};
|
||||
|
||||
const requestPayload: RequestPayload = {
|
||||
messages,
|
||||
stream: options.config.stream,
|
||||
model: modelConfig.model,
|
||||
temperature: modelConfig.temperature,
|
||||
presence_penalty: modelConfig.presence_penalty,
|
||||
frequency_penalty: modelConfig.frequency_penalty,
|
||||
top_p: modelConfig.top_p,
|
||||
};
|
||||
|
||||
console.log("[Request] xai payload: ", requestPayload);
|
||||
|
||||
const shouldStream = !!options.config.stream;
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
|
||||
try {
|
||||
const chatPath = this.path(XAI.ChatPath);
|
||||
const chatPayload = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(requestPayload),
|
||||
signal: controller.signal,
|
||||
headers: getHeaders(),
|
||||
};
|
||||
|
||||
// make a fetch request
|
||||
const requestTimeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
REQUEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
if (shouldStream) {
|
||||
const [tools, funcs] = usePluginStore
|
||||
.getState()
|
||||
.getAsTools(
|
||||
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||
);
|
||||
return stream(
|
||||
chatPath,
|
||||
requestPayload,
|
||||
getHeaders(),
|
||||
tools as any,
|
||||
funcs,
|
||||
controller,
|
||||
// parseSSE
|
||||
(text: string, runTools: ChatMessageTool[]) => {
|
||||
// console.log("parseSSE", text, runTools);
|
||||
const json = JSON.parse(text);
|
||||
const choices = json.choices as Array<{
|
||||
delta: {
|
||||
content: string;
|
||||
tool_calls: ChatMessageTool[];
|
||||
};
|
||||
}>;
|
||||
const tool_calls = choices[0]?.delta?.tool_calls;
|
||||
if (tool_calls?.length > 0) {
|
||||
const index = tool_calls[0]?.index;
|
||||
const id = tool_calls[0]?.id;
|
||||
const args = tool_calls[0]?.function?.arguments;
|
||||
if (id) {
|
||||
runTools.push({
|
||||
id,
|
||||
type: tool_calls[0]?.type,
|
||||
function: {
|
||||
name: tool_calls[0]?.function?.name as string,
|
||||
arguments: args,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// @ts-ignore
|
||||
runTools[index]["function"]["arguments"] += args;
|
||||
}
|
||||
}
|
||||
return choices[0]?.delta?.content;
|
||||
},
|
||||
// processToolMessage, include tool_calls message and tool call results
|
||||
(
|
||||
requestPayload: RequestPayload,
|
||||
toolCallMessage: any,
|
||||
toolCallResult: any[],
|
||||
) => {
|
||||
// @ts-ignore
|
||||
requestPayload?.messages?.splice(
|
||||
// @ts-ignore
|
||||
requestPayload?.messages?.length,
|
||||
0,
|
||||
toolCallMessage,
|
||||
...toolCallResult,
|
||||
);
|
||||
},
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
const res = await fetch(chatPath, chatPayload);
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message, res);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
options.onError?.(e as Error);
|
||||
}
|
||||
}
|
||||
async usage() {
|
||||
return {
|
||||
used: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async models(): Promise<LLMModel[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
@@ -1,12 +1,70 @@
|
||||
.auth-page {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
justify-content: flex-start;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
.top-banner {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 12px 64px;
|
||||
box-sizing: border-box;
|
||||
background: var(--second);
|
||||
.top-banner-inner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
line-height: 150%;
|
||||
span {
|
||||
gap: 8px;
|
||||
a {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
margin-left: 8px;
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
.top-banner-close {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 48px;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.top-banner {
|
||||
padding: 12px 24px 12px 12px;
|
||||
.top-banner-close {
|
||||
right: 10px;
|
||||
}
|
||||
.top-banner-inner {
|
||||
.top-banner-logo {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.auth-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
animation: slide-in-from-top ease 0.3s;
|
||||
}
|
||||
|
||||
.auth-logo {
|
||||
margin-top: 10vh;
|
||||
transform: scale(1.4);
|
||||
}
|
||||
|
||||
@@ -14,6 +72,7 @@
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
line-height: 2;
|
||||
margin-bottom: 1vh;
|
||||
}
|
||||
|
||||
.auth-tips {
|
||||
@@ -24,6 +83,10 @@
|
||||
margin: 3vh 0;
|
||||
}
|
||||
|
||||
.auth-input-second {
|
||||
margin: 0 0 3vh 0;
|
||||
}
|
||||
|
||||
.auth-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@@ -1,21 +1,37 @@
|
||||
import styles from "./auth.module.scss";
|
||||
import { IconButton } from "./button";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { Path } from "../constant";
|
||||
import { Path, SAAS_CHAT_URL } from "../constant";
|
||||
import { useAccessStore } from "../store";
|
||||
import Locale from "../locales";
|
||||
|
||||
import Delete from "../icons/close.svg";
|
||||
import Arrow from "../icons/arrow.svg";
|
||||
import Logo from "../icons/logo.svg";
|
||||
import { useMobileScreen } from "@/app/utils";
|
||||
import BotIcon from "../icons/bot.svg";
|
||||
import { useEffect } from "react";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { PasswordInput } from "./ui-lib";
|
||||
import LeftIcon from "@/app/icons/left.svg";
|
||||
import { safeLocalStorage } from "@/app/utils";
|
||||
import {
|
||||
trackSettingsPageGuideToCPaymentClick,
|
||||
trackAuthorizationPageButtonToCPaymentClick,
|
||||
} from "../utils/auth-settings-events";
|
||||
import clsx from "clsx";
|
||||
|
||||
const storage = safeLocalStorage();
|
||||
|
||||
export function AuthPage() {
|
||||
const navigate = useNavigate();
|
||||
const accessStore = useAccessStore();
|
||||
|
||||
const goHome = () => navigate(Path.Home);
|
||||
const goChat = () => navigate(Path.Chat);
|
||||
const goSaas = () => {
|
||||
trackAuthorizationPageButtonToCPaymentClick();
|
||||
window.location.href = SAAS_CHAT_URL;
|
||||
};
|
||||
|
||||
const resetAccessCode = () => {
|
||||
accessStore.update((access) => {
|
||||
access.openaiApiKey = "";
|
||||
@@ -32,43 +48,58 @@ export function AuthPage() {
|
||||
|
||||
return (
|
||||
<div className={styles["auth-page"]}>
|
||||
<div className={`no-dark ${styles["auth-logo"]}`}>
|
||||
<TopBanner></TopBanner>
|
||||
<div className={styles["auth-header"]}>
|
||||
<IconButton
|
||||
icon={<LeftIcon />}
|
||||
text={Locale.Auth.Return}
|
||||
onClick={() => navigate(Path.Home)}
|
||||
></IconButton>
|
||||
</div>
|
||||
<div className={clsx("no-dark", styles["auth-logo"])}>
|
||||
<BotIcon />
|
||||
</div>
|
||||
|
||||
<div className={styles["auth-title"]}>{Locale.Auth.Title}</div>
|
||||
<div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div>
|
||||
|
||||
<input
|
||||
className={styles["auth-input"]}
|
||||
type="password"
|
||||
placeholder={Locale.Auth.Input}
|
||||
<PasswordInput
|
||||
style={{ marginTop: "3vh", marginBottom: "3vh" }}
|
||||
aria={Locale.Settings.ShowPassword}
|
||||
aria-label={Locale.Auth.Input}
|
||||
value={accessStore.accessCode}
|
||||
type="text"
|
||||
placeholder={Locale.Auth.Input}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.accessCode = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{!accessStore.hideUserApiKey ? (
|
||||
<>
|
||||
<div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
|
||||
<input
|
||||
className={styles["auth-input"]}
|
||||
type="password"
|
||||
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
|
||||
<PasswordInput
|
||||
style={{ marginTop: "3vh", marginBottom: "3vh" }}
|
||||
aria={Locale.Settings.ShowPassword}
|
||||
aria-label={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
|
||||
value={accessStore.openaiApiKey}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.openaiApiKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
className={styles["auth-input"]}
|
||||
type="password"
|
||||
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
|
||||
<PasswordInput
|
||||
style={{ marginTop: "3vh", marginBottom: "3vh" }}
|
||||
aria={Locale.Settings.ShowPassword}
|
||||
aria-label={Locale.Settings.Access.Google.ApiKey.Placeholder}
|
||||
value={accessStore.googleApiKey}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.googleApiKey = e.currentTarget.value),
|
||||
@@ -85,13 +116,74 @@ export function AuthPage() {
|
||||
onClick={goChat}
|
||||
/>
|
||||
<IconButton
|
||||
text={Locale.Auth.Later}
|
||||
text={Locale.Auth.SaasTips}
|
||||
onClick={() => {
|
||||
resetAccessCode();
|
||||
goHome();
|
||||
goSaas();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TopBanner() {
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
const isMobile = useMobileScreen();
|
||||
useEffect(() => {
|
||||
// 检查 localStorage 中是否有标记
|
||||
const bannerDismissed = storage.getItem("bannerDismissed");
|
||||
// 如果标记不存在,存储默认值并显示横幅
|
||||
if (!bannerDismissed) {
|
||||
storage.setItem("bannerDismissed", "false");
|
||||
setIsVisible(true); // 显示横幅
|
||||
} else if (bannerDismissed === "true") {
|
||||
// 如果标记为 "true",则隐藏横幅
|
||||
setIsVisible(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
setIsHovered(true);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
setIsHovered(false);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setIsVisible(false);
|
||||
storage.setItem("bannerDismissed", "true");
|
||||
};
|
||||
|
||||
if (!isVisible) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={styles["top-banner"]}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<div className={clsx(styles["top-banner-inner"], "no-dark")}>
|
||||
<Logo className={styles["top-banner-logo"]}></Logo>
|
||||
<span>
|
||||
{Locale.Auth.TopTips}
|
||||
<a
|
||||
href={SAAS_CHAT_URL}
|
||||
rel="stylesheet"
|
||||
onClick={() => {
|
||||
trackSettingsPageGuideToCPaymentClick();
|
||||
}}
|
||||
>
|
||||
{Locale.Settings.Access.SaasStart.ChatNow}
|
||||
<Arrow style={{ marginLeft: "4px" }} />
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
{(isHovered || isMobile) && (
|
||||
<Delete className={styles["top-banner-close"]} onClick={handleClose} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -5,7 +5,6 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
|
@@ -2,6 +2,7 @@ import * as React from "react";
|
||||
|
||||
import styles from "./button.module.scss";
|
||||
import { CSSProperties } from "react";
|
||||
import clsx from "clsx";
|
||||
|
||||
export type ButtonType = "primary" | "danger" | null;
|
||||
|
||||
@@ -22,12 +23,16 @@ export function IconButton(props: {
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
className={
|
||||
styles["icon-button"] +
|
||||
` ${props.bordered && styles.border} ${props.shadow && styles.shadow} ${
|
||||
props.className ?? ""
|
||||
} clickable ${styles[props.type ?? ""]}`
|
||||
}
|
||||
className={clsx(
|
||||
"clickable",
|
||||
styles["icon-button"],
|
||||
{
|
||||
[styles.border]: props.bordered,
|
||||
[styles.shadow]: props.shadow,
|
||||
},
|
||||
styles[props.type ?? ""],
|
||||
props.className,
|
||||
)}
|
||||
onClick={props.onClick}
|
||||
title={props.title}
|
||||
disabled={props.disabled}
|
||||
@@ -40,10 +45,9 @@ export function IconButton(props: {
|
||||
{props.icon && (
|
||||
<div
|
||||
aria-label={props.text || props.title}
|
||||
className={
|
||||
styles["icon-button-icon"] +
|
||||
` ${props.type === "primary" && "no-dark"}`
|
||||
}
|
||||
className={clsx(styles["icon-button-icon"], {
|
||||
"no-dark": props.type === "primary",
|
||||
})}
|
||||
>
|
||||
{props.icon}
|
||||
</div>
|
||||
|
@@ -18,6 +18,7 @@ import { Mask } from "../store/mask";
|
||||
import { useRef, useEffect } from "react";
|
||||
import { showConfirm } from "./ui-lib";
|
||||
import { useMobileScreen } from "../utils";
|
||||
import clsx from "clsx";
|
||||
|
||||
export function ChatItem(props: {
|
||||
onClick?: () => void;
|
||||
@@ -45,11 +46,11 @@ export function ChatItem(props: {
|
||||
<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"]
|
||||
}`}
|
||||
className={clsx(styles["chat-item"], {
|
||||
[styles["chat-item-selected"]]:
|
||||
props.selected &&
|
||||
(currentPath === Path.Chat || currentPath === Path.Home),
|
||||
})}
|
||||
onClick={props.onClick}
|
||||
ref={(ele) => {
|
||||
draggableRef.current = ele;
|
||||
@@ -63,7 +64,7 @@ export function ChatItem(props: {
|
||||
>
|
||||
{props.narrow ? (
|
||||
<div className={styles["chat-item-narrow"]}>
|
||||
<div className={styles["chat-item-avatar"] + " no-dark"}>
|
||||
<div className={clsx(styles["chat-item-avatar"], "no-dark")}>
|
||||
<MaskAvatar
|
||||
avatar={props.mask.avatar}
|
||||
model={props.mask.modelConfig.model}
|
||||
|
@@ -45,6 +45,14 @@
|
||||
.chat-input-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 5px;
|
||||
|
||||
&-end {
|
||||
display: flex;
|
||||
margin-left: auto;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.chat-input-action {
|
||||
display: inline-flex;
|
||||
@@ -62,10 +70,6 @@
|
||||
width: var(--icon-width);
|
||||
overflow: hidden;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.text {
|
||||
white-space: nowrap;
|
||||
padding-left: 5px;
|
||||
@@ -231,10 +235,12 @@
|
||||
|
||||
animation: slide-in ease 0.3s;
|
||||
|
||||
$linear: linear-gradient(to right,
|
||||
rgba(0, 0, 0, 0),
|
||||
rgba(0, 0, 0, 1),
|
||||
rgba(0, 0, 0, 0));
|
||||
$linear: linear-gradient(
|
||||
to right,
|
||||
rgba(0, 0, 0, 0),
|
||||
rgba(0, 0, 0, 1),
|
||||
rgba(0, 0, 0, 0)
|
||||
);
|
||||
mask-image: $linear;
|
||||
|
||||
@mixin show {
|
||||
@@ -373,7 +379,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-user>.chat-message-container {
|
||||
.chat-message-user > .chat-message-container {
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
@@ -443,6 +449,25 @@
|
||||
transition: all ease 0.3s;
|
||||
}
|
||||
|
||||
.chat-message-audio {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
border-radius: 10px;
|
||||
background-color: rgba(0, 0, 0, 0.05);
|
||||
border: var(--border-in-light);
|
||||
position: relative;
|
||||
transition: all ease 0.3s;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
user-select: text;
|
||||
word-break: break-word;
|
||||
box-sizing: border-box;
|
||||
audio {
|
||||
height: 30px; /* 调整高度 */
|
||||
}
|
||||
}
|
||||
|
||||
.chat-message-item-image {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
@@ -471,23 +496,27 @@
|
||||
border: rgba($color: #888, $alpha: 0.2) 1px solid;
|
||||
}
|
||||
|
||||
|
||||
@media only screen and (max-width: 600px) {
|
||||
$calc-image-width: calc(100vw/3*2/var(--image-count));
|
||||
$calc-image-width: calc(100vw / 3 * 2 / var(--image-count));
|
||||
|
||||
.chat-message-item-image-multi {
|
||||
width: $calc-image-width;
|
||||
height: $calc-image-width;
|
||||
}
|
||||
|
||||
|
||||
.chat-message-item-image {
|
||||
max-width: calc(100vw/3*2);
|
||||
max-width: calc(100vw / 3 * 2);
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 600px) {
|
||||
$max-image-width: calc(calc(1200px - var(--sidebar-width))/3*2/var(--image-count));
|
||||
$image-width: calc(calc(var(--window-width) - var(--sidebar-width))/3*2/var(--image-count));
|
||||
$max-image-width: calc(
|
||||
calc(1200px - var(--sidebar-width)) / 3 * 2 / var(--image-count)
|
||||
);
|
||||
$image-width: calc(
|
||||
calc(var(--window-width) - var(--sidebar-width)) / 3 * 2 /
|
||||
var(--image-count)
|
||||
);
|
||||
|
||||
.chat-message-item-image-multi {
|
||||
width: $image-width;
|
||||
@@ -497,7 +526,7 @@
|
||||
}
|
||||
|
||||
.chat-message-item-image {
|
||||
max-width: calc(calc(1200px - var(--sidebar-width))/3*2);
|
||||
max-width: calc(calc(1200px - var(--sidebar-width)) / 3 * 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -515,7 +544,7 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.chat-message-user>.chat-message-container>.chat-message-item {
|
||||
.chat-message-user > .chat-message-container > .chat-message-item {
|
||||
background-color: var(--second);
|
||||
|
||||
&:hover {
|
||||
@@ -626,7 +655,8 @@
|
||||
min-height: 68px;
|
||||
}
|
||||
|
||||
.chat-input:focus {}
|
||||
.chat-input:focus {
|
||||
}
|
||||
|
||||
.chat-input-send {
|
||||
background-color: var(--primary);
|
||||
@@ -693,4 +723,31 @@
|
||||
.shortcut-key span {
|
||||
font-size: 12px;
|
||||
color: var(--black);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-main {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.chat-body-container {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
.chat-side-panel {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--white);
|
||||
overflow: hidden;
|
||||
z-index: 10;
|
||||
transform: translateX(100%);
|
||||
transition: all ease 0.3s;
|
||||
&-show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -37,7 +37,8 @@ export function Avatar(props: { model?: ModelType; avatar?: string }) {
|
||||
return (
|
||||
<div className="no-dark">
|
||||
{props.model?.startsWith("gpt-4") ||
|
||||
props.model?.startsWith("chatgpt-4o") ? (
|
||||
props.model?.startsWith("chatgpt-4o") ||
|
||||
props.model?.startsWith("o1") ? (
|
||||
<BlackBotIcon className="user-avatar" />
|
||||
) : (
|
||||
<BotIcon className="user-avatar" />
|
||||
|
@@ -40,6 +40,7 @@ import { EXPORT_MESSAGE_CLASS_NAME } from "../constant";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { type ClientApi, getClientApi } from "../client/api";
|
||||
import { getMessageTextContent } from "../utils";
|
||||
import clsx from "clsx";
|
||||
|
||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||
loading: () => <LoadingIcon />,
|
||||
@@ -118,9 +119,10 @@ function Steps<
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className={`${styles["step"]} ${
|
||||
styles[i <= props.index ? "step-finished" : ""]
|
||||
} ${i === props.index && styles["step-current"]} clickable`}
|
||||
className={clsx("clickable", styles["step"], {
|
||||
[styles["step-finished"]]: i <= props.index,
|
||||
[styles["step-current"]]: i === props.index,
|
||||
})}
|
||||
onClick={() => {
|
||||
props.onStepChange?.(i);
|
||||
}}
|
||||
@@ -525,11 +527,11 @@ export function ImagePreviewer(props: {
|
||||
messages={props.messages}
|
||||
/>
|
||||
<div
|
||||
className={`${styles["preview-body"]} ${styles["default-theme"]}`}
|
||||
className={clsx(styles["preview-body"], styles["default-theme"])}
|
||||
ref={previewRef}
|
||||
>
|
||||
<div className={styles["chat-info"]}>
|
||||
<div className={styles["logo"] + " no-dark"}>
|
||||
<div className={clsx(styles["logo"], "no-dark")}>
|
||||
<NextImage
|
||||
src={ChatGptIcon.src}
|
||||
alt="logo"
|
||||
@@ -570,7 +572,7 @@ export function ImagePreviewer(props: {
|
||||
{props.messages.map((m, i) => {
|
||||
return (
|
||||
<div
|
||||
className={styles["message"] + " " + styles["message-" + m.role]}
|
||||
className={clsx(styles["message"], styles["message-" + m.role])}
|
||||
key={i}
|
||||
>
|
||||
<div className={styles["avatar"]}>
|
||||
|
@@ -140,6 +140,9 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
&-narrow {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
|
@@ -3,7 +3,6 @@
|
||||
require("../polyfill");
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import styles from "./home.module.scss";
|
||||
|
||||
import BotIcon from "../icons/bot.svg";
|
||||
@@ -29,10 +28,11 @@ import { AuthPage } from "./auth";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { type ClientApi, getClientApi } from "../client/api";
|
||||
import { useAccessStore } from "../store";
|
||||
import clsx from "clsx";
|
||||
|
||||
export function Loading(props: { noLogo?: boolean }) {
|
||||
return (
|
||||
<div className={styles["loading-content"] + " no-dark"}>
|
||||
<div className={clsx("no-dark", styles["loading-content"])}>
|
||||
{!props.noLogo && <BotIcon />}
|
||||
<LoadingIcon />
|
||||
</div>
|
||||
@@ -179,7 +179,11 @@ function Screen() {
|
||||
if (isSdNew) return <Sd />;
|
||||
return (
|
||||
<>
|
||||
<SideBar className={isHome ? styles["sidebar-show"] : ""} />
|
||||
<SideBar
|
||||
className={clsx({
|
||||
[styles["sidebar-show"]]: isHome,
|
||||
})}
|
||||
/>
|
||||
<WindowContent>
|
||||
<Routes>
|
||||
<Route path={Path.Home} element={<Chat />} />
|
||||
@@ -197,9 +201,10 @@ function Screen() {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.container} ${
|
||||
shouldTightBorder ? styles["tight-container"] : styles.container
|
||||
} ${getLang() === "ar" ? styles["rtl-screen"] : ""}`}
|
||||
className={clsx(styles.container, {
|
||||
[styles["tight-container"]]: shouldTightBorder,
|
||||
[styles["rtl-screen"]]: getLang() === "ar",
|
||||
})}
|
||||
>
|
||||
{renderContent()}
|
||||
</div>
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import * as React from "react";
|
||||
import styles from "./input-range.module.scss";
|
||||
import clsx from "clsx";
|
||||
|
||||
interface InputRangeProps {
|
||||
onChange: React.ChangeEventHandler<HTMLInputElement>;
|
||||
@@ -23,7 +24,7 @@ export function InputRange({
|
||||
aria,
|
||||
}: InputRangeProps) {
|
||||
return (
|
||||
<div className={styles["input-range"] + ` ${className ?? ""}`}>
|
||||
<div className={clsx(styles["input-range"], className)}>
|
||||
{title || value}
|
||||
<input
|
||||
aria-label={aria}
|
||||
|
@@ -22,6 +22,9 @@ import {
|
||||
import { useChatStore } from "../store";
|
||||
import { IconButton } from "./button";
|
||||
|
||||
import { useAppConfig } from "../store/config";
|
||||
import clsx from "clsx";
|
||||
|
||||
export function Mermaid(props: { code: string }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
@@ -55,7 +58,7 @@ export function Mermaid(props: { code: string }) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="no-dark mermaid"
|
||||
className={clsx("no-dark", "mermaid")}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
overflow: "auto",
|
||||
@@ -87,12 +90,18 @@ export function PreCode(props: { children: any }) {
|
||||
const refText = ref.current.querySelector("code")?.innerText;
|
||||
if (htmlDom) {
|
||||
setHtmlCode((htmlDom as HTMLElement).innerText);
|
||||
} else if (refText?.startsWith("<!DOCTYPE")) {
|
||||
} else if (
|
||||
refText?.startsWith("<!DOCTYPE") ||
|
||||
refText?.startsWith("<svg") ||
|
||||
refText?.startsWith("<?xml")
|
||||
) {
|
||||
setHtmlCode(refText);
|
||||
}
|
||||
}, 600);
|
||||
|
||||
const enableArtifacts = session.mask?.enableArtifacts !== false;
|
||||
const config = useAppConfig();
|
||||
const enableArtifacts =
|
||||
session.mask?.enableArtifacts !== false && config.enableArtifacts;
|
||||
|
||||
//Wrap the paragraph for plain-text
|
||||
useEffect(() => {
|
||||
@@ -128,8 +137,9 @@ export function PreCode(props: { children: any }) {
|
||||
className="copy-code-button"
|
||||
onClick={() => {
|
||||
if (ref.current) {
|
||||
const code = ref.current.innerText;
|
||||
copyToClipboard(code);
|
||||
copyToClipboard(
|
||||
ref.current.querySelector("code")?.innerText ?? "",
|
||||
);
|
||||
}
|
||||
}}
|
||||
></span>
|
||||
@@ -164,6 +174,12 @@ export function PreCode(props: { children: any }) {
|
||||
}
|
||||
|
||||
function CustomCode(props: { children: any; className?: string }) {
|
||||
const chatStore = useChatStore();
|
||||
const session = chatStore.currentSession();
|
||||
const config = useAppConfig();
|
||||
const enableCodeFold =
|
||||
session.mask?.enableCodeFold !== false && config.enableCodeFold;
|
||||
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const [showToggle, setShowToggle] = useState(false);
|
||||
@@ -179,46 +195,39 @@ function CustomCode(props: { children: any; className?: string }) {
|
||||
const toggleCollapsed = () => {
|
||||
setCollapsed((collapsed) => !collapsed);
|
||||
};
|
||||
const renderShowMoreButton = () => {
|
||||
if (showToggle && enableCodeFold && collapsed) {
|
||||
return (
|
||||
<div
|
||||
className={clsx("show-hide-button", {
|
||||
collapsed,
|
||||
expanded: !collapsed,
|
||||
})}
|
||||
>
|
||||
<button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<code
|
||||
className={props?.className}
|
||||
className={clsx(props?.className)}
|
||||
ref={ref}
|
||||
style={{
|
||||
maxHeight: collapsed ? "400px" : "none",
|
||||
maxHeight: enableCodeFold && collapsed ? "400px" : "none",
|
||||
overflowY: "hidden",
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</code>
|
||||
{showToggle && collapsed && (
|
||||
<div
|
||||
className={`show-hide-button ${collapsed ? "collapsed" : "expanded"}`}
|
||||
>
|
||||
<button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderShowMoreButton()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function escapeDollarNumber(text: string) {
|
||||
let escapedText = "";
|
||||
|
||||
for (let i = 0; i < text.length; i += 1) {
|
||||
let char = text[i];
|
||||
const nextChar = text[i + 1] || " ";
|
||||
|
||||
if (char === "$" && nextChar >= "0" && nextChar <= "9") {
|
||||
char = "\\$";
|
||||
}
|
||||
|
||||
escapedText += char;
|
||||
}
|
||||
|
||||
return escapedText;
|
||||
}
|
||||
|
||||
function escapeBrackets(text: string) {
|
||||
const pattern =
|
||||
/(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
|
||||
@@ -239,6 +248,10 @@ function escapeBrackets(text: string) {
|
||||
|
||||
function tryWrapHtmlCode(text: string) {
|
||||
// try add wrap html code (fixed: html codeblock include 2 newline)
|
||||
// ignore embed codeblock
|
||||
if (text.includes("```")) {
|
||||
return text;
|
||||
}
|
||||
return text
|
||||
.replace(
|
||||
/([`]*?)(\w*?)([\n\r]*?)(<!DOCTYPE html>)/g,
|
||||
@@ -247,7 +260,7 @@ function tryWrapHtmlCode(text: string) {
|
||||
},
|
||||
)
|
||||
.replace(
|
||||
/(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*?)([`]*?)([\n\r]*?)/g,
|
||||
/(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*)([`]*)([\n\r]*?)/g,
|
||||
(match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
|
||||
return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match;
|
||||
},
|
||||
@@ -256,7 +269,7 @@ function tryWrapHtmlCode(text: string) {
|
||||
|
||||
function _MarkDownContent(props: { content: string }) {
|
||||
const escapedContent = useMemo(() => {
|
||||
return tryWrapHtmlCode(escapeBrackets(escapeDollarNumber(props.content)));
|
||||
return tryWrapHtmlCode(escapeBrackets(props.content));
|
||||
}, [props.content]);
|
||||
|
||||
return (
|
||||
@@ -278,6 +291,20 @@ function _MarkDownContent(props: { content: string }) {
|
||||
p: (pProps) => <p {...pProps} dir="auto" />,
|
||||
a: (aProps) => {
|
||||
const href = aProps.href || "";
|
||||
if (/\.(aac|mp3|opus|wav)$/.test(href)) {
|
||||
return (
|
||||
<figure>
|
||||
<audio controls src={href}></audio>
|
||||
</figure>
|
||||
);
|
||||
}
|
||||
if (/\.(3gp|3g2|webm|ogv|mpeg|mp4|avi)$/.test(href)) {
|
||||
return (
|
||||
<video controls width="99.9%">
|
||||
<source src={href} />
|
||||
</video>
|
||||
);
|
||||
}
|
||||
const isInternal = /^\/#/i.test(href);
|
||||
const target = isInternal ? "_self" : aProps.target ?? "_blank";
|
||||
return <a {...aProps} target={target} />;
|
||||
|
@@ -55,6 +55,7 @@ import {
|
||||
OnDragEndResponder,
|
||||
} from "@hello-pangea/dnd";
|
||||
import { getMessageTextContent } from "../utils";
|
||||
import clsx from "clsx";
|
||||
|
||||
// drag and drop helper function
|
||||
function reorder<T>(list: T[], startIndex: number, endIndex: number): T[] {
|
||||
@@ -166,21 +167,40 @@ export function MaskConfig(props: {
|
||||
></input>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={Locale.Mask.Config.Artifacts.Title}
|
||||
subTitle={Locale.Mask.Config.Artifacts.SubTitle}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Mask.Config.Artifacts.Title}
|
||||
type="checkbox"
|
||||
checked={props.mask.enableArtifacts !== false}
|
||||
onChange={(e) => {
|
||||
props.updateMask((mask) => {
|
||||
mask.enableArtifacts = e.currentTarget.checked;
|
||||
});
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
{globalConfig.enableArtifacts && (
|
||||
<ListItem
|
||||
title={Locale.Mask.Config.Artifacts.Title}
|
||||
subTitle={Locale.Mask.Config.Artifacts.SubTitle}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Mask.Config.Artifacts.Title}
|
||||
type="checkbox"
|
||||
checked={props.mask.enableArtifacts !== false}
|
||||
onChange={(e) => {
|
||||
props.updateMask((mask) => {
|
||||
mask.enableArtifacts = e.currentTarget.checked;
|
||||
});
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
)}
|
||||
{globalConfig.enableCodeFold && (
|
||||
<ListItem
|
||||
title={Locale.Mask.Config.CodeFold.Title}
|
||||
subTitle={Locale.Mask.Config.CodeFold.SubTitle}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Mask.Config.CodeFold.Title}
|
||||
type="checkbox"
|
||||
checked={props.mask.enableCodeFold !== false}
|
||||
onChange={(e) => {
|
||||
props.updateMask((mask) => {
|
||||
mask.enableCodeFold = e.currentTarget.checked;
|
||||
});
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
{!props.shouldSyncFromGlobal ? (
|
||||
<ListItem
|
||||
@@ -569,7 +589,7 @@ export function MaskPage() {
|
||||
</div>
|
||||
<div className={styles["mask-title"]}>
|
||||
<div className={styles["mask-name"]}>{m.name}</div>
|
||||
<div className={styles["mask-info"] + " one-line"}>
|
||||
<div className={clsx(styles["mask-info"], "one-line")}>
|
||||
{`${Locale.Mask.Item.Info(m.context.length)} / ${
|
||||
ALL_LANG_OPTIONS[m.lang]
|
||||
} / ${m.modelConfig.model}`}
|
||||
|
@@ -8,6 +8,7 @@ import Locale from "../locales";
|
||||
|
||||
import styles from "./message-selector.module.scss";
|
||||
import { getMessageTextContent } from "../utils";
|
||||
import clsx from "clsx";
|
||||
|
||||
function useShiftRange() {
|
||||
const [startIndex, setStartIndex] = useState<number>();
|
||||
@@ -71,6 +72,7 @@ export function MessageSelector(props: {
|
||||
defaultSelectAll?: boolean;
|
||||
onSelected?: (messages: ChatMessage[]) => void;
|
||||
}) {
|
||||
const LATEST_COUNT = 4;
|
||||
const chatStore = useChatStore();
|
||||
const session = chatStore.currentSession();
|
||||
const isValid = (m: ChatMessage) => m.content && !m.isError && !m.streaming;
|
||||
@@ -141,15 +143,13 @@ export function MessageSelector(props: {
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [startIndex, endIndex]);
|
||||
|
||||
const LATEST_COUNT = 4;
|
||||
|
||||
return (
|
||||
<div className={styles["message-selector"]}>
|
||||
<div className={styles["message-filter"]}>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={Locale.Select.Search}
|
||||
className={styles["filter-item"] + " " + styles["search-bar"]}
|
||||
className={clsx(styles["filter-item"], styles["search-bar"])}
|
||||
value={searchInput}
|
||||
onInput={(e) => {
|
||||
setSearchInput(e.currentTarget.value);
|
||||
@@ -196,9 +196,9 @@ export function MessageSelector(props: {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles["message"]} ${
|
||||
props.selection.has(m.id!) && styles["message-selected"]
|
||||
}`}
|
||||
className={clsx(styles["message"], {
|
||||
[styles["message-selected"]]: props.selection.has(m.id!),
|
||||
})}
|
||||
key={i}
|
||||
onClick={() => {
|
||||
props.updateSelection((selection) => {
|
||||
@@ -221,7 +221,7 @@ export function MessageSelector(props: {
|
||||
<div className={styles["date"]}>
|
||||
{new Date(m.date).toLocaleString()}
|
||||
</div>
|
||||
<div className={`${styles["content"]} one-line`}>
|
||||
<div className={clsx(styles["content"], "one-line")}>
|
||||
{getMessageTextContent(m)}
|
||||
</div>
|
||||
</div>
|
||||
|
7
app/components/model-config.module.scss
Normal file
@@ -0,0 +1,7 @@
|
||||
.select-compress-model {
|
||||
width: 60%;
|
||||
select {
|
||||
max-width: 100%;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
@@ -6,6 +6,8 @@ import { InputRange } from "./input-range";
|
||||
import { ListItem, Select } from "./ui-lib";
|
||||
import { useAllModels } from "../utils/hooks";
|
||||
import { groupBy } from "lodash-es";
|
||||
import styles from "./model-config.module.scss";
|
||||
import { getModelProvider } from "../utils/model";
|
||||
|
||||
export function ModelConfigList(props: {
|
||||
modelConfig: ModelConfig;
|
||||
@@ -27,7 +29,9 @@ export function ModelConfigList(props: {
|
||||
value={value}
|
||||
align="left"
|
||||
onChange={(e) => {
|
||||
const [model, providerName] = e.currentTarget.value.split("@");
|
||||
const [model, providerName] = getModelProvider(
|
||||
e.currentTarget.value,
|
||||
);
|
||||
props.updateConfig((config) => {
|
||||
config.model = ModalConfigValidator.model(model);
|
||||
config.providerName = providerName as ServiceProvider;
|
||||
@@ -242,10 +246,13 @@ export function ModelConfigList(props: {
|
||||
subTitle={Locale.Settings.CompressModel.SubTitle}
|
||||
>
|
||||
<Select
|
||||
className={styles["select-compress-model"]}
|
||||
aria-label={Locale.Settings.CompressModel.Title}
|
||||
value={compressModelValue}
|
||||
onChange={(e) => {
|
||||
const [model, providerName] = e.currentTarget.value.split("@");
|
||||
const [model, providerName] = getModelProvider(
|
||||
e.currentTarget.value,
|
||||
);
|
||||
props.updateConfig((config) => {
|
||||
config.compressModel = ModalConfigValidator.model(model);
|
||||
config.compressProviderName = providerName as ServiceProvider;
|
||||
|
@@ -16,6 +16,7 @@ import { MaskAvatar } from "./mask";
|
||||
import { useCommand } from "../command";
|
||||
import { showConfirm } from "./ui-lib";
|
||||
import { BUILTIN_MASK_STORE } from "../masks";
|
||||
import clsx from "clsx";
|
||||
|
||||
function MaskItem(props: { mask: Mask; onClick?: () => void }) {
|
||||
return (
|
||||
@@ -24,7 +25,9 @@ function MaskItem(props: { mask: Mask; onClick?: () => void }) {
|
||||
avatar={props.mask.avatar}
|
||||
model={props.mask.modelConfig.model}
|
||||
/>
|
||||
<div className={styles["mask-name"] + " one-line"}>{props.mask.name}</div>
|
||||
<div className={clsx(styles["mask-name"], "one-line")}>
|
||||
{props.mask.name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -10,7 +10,29 @@
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
min-width: 300px;
|
||||
min-width: 280px;
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-schema {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-direction: row;
|
||||
|
||||
input {
|
||||
margin-right: 20px;
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
margin-right: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
|
||||
button {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -12,7 +12,6 @@ import EditIcon from "../icons/edit.svg";
|
||||
import AddIcon from "../icons/add.svg";
|
||||
import CloseIcon from "../icons/close.svg";
|
||||
import DeleteIcon from "../icons/delete.svg";
|
||||
import EyeIcon from "../icons/eye.svg";
|
||||
import ConfirmIcon from "../icons/confirm.svg";
|
||||
import ReloadIcon from "../icons/reload.svg";
|
||||
import GithubIcon from "../icons/github.svg";
|
||||
@@ -29,7 +28,7 @@ import {
|
||||
import Locale from "../locales";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useState } from "react";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import clsx from "clsx";
|
||||
|
||||
export function PluginPage() {
|
||||
const navigate = useNavigate();
|
||||
@@ -201,7 +200,7 @@ export function PluginPage() {
|
||||
<div className={styles["mask-name"]}>
|
||||
{m.title}@<small>{m.version}</small>
|
||||
</div>
|
||||
<div className={styles["mask-info"] + " one-line"}>
|
||||
<div className={clsx(styles["mask-info"], "one-line")}>
|
||||
{Locale.Plugin.Item.Info(
|
||||
FunctionToolService.add(m).length,
|
||||
)}
|
||||
@@ -209,19 +208,11 @@ export function PluginPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles["mask-actions"]}>
|
||||
{m.builtin ? (
|
||||
<IconButton
|
||||
icon={<EyeIcon />}
|
||||
text={Locale.Plugin.Item.View}
|
||||
onClick={() => setEditingPluginId(m.id)}
|
||||
/>
|
||||
) : (
|
||||
<IconButton
|
||||
icon={<EditIcon />}
|
||||
text={Locale.Plugin.Item.Edit}
|
||||
onClick={() => setEditingPluginId(m.id)}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
icon={<EditIcon />}
|
||||
text={Locale.Plugin.Item.Edit}
|
||||
onClick={() => setEditingPluginId(m.id)}
|
||||
/>
|
||||
{!m.builtin && (
|
||||
<IconButton
|
||||
icon={<DeleteIcon />}
|
||||
@@ -325,30 +316,13 @@ export function PluginPage() {
|
||||
></PasswordInput>
|
||||
</ListItem>
|
||||
)}
|
||||
{!getClientConfig()?.isApp && (
|
||||
<ListItem
|
||||
title={Locale.Plugin.Auth.Proxy}
|
||||
subTitle={Locale.Plugin.Auth.ProxyDescription}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={editingPlugin?.usingProxy}
|
||||
style={{ minWidth: 16 }}
|
||||
onChange={(e) => {
|
||||
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
|
||||
plugin.usingProxy = e.currentTarget.checked;
|
||||
});
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
<List>
|
||||
<ListItem title={Locale.Plugin.EditModal.Content}>
|
||||
<div style={{ display: "flex", justifyContent: "flex-end" }}>
|
||||
<div className={pluginStyles["plugin-schema"]}>
|
||||
<input
|
||||
type="text"
|
||||
style={{ minWidth: 200, marginRight: 20 }}
|
||||
style={{ minWidth: 200 }}
|
||||
onInput={(e) => setLoadUrl(e.currentTarget.value)}
|
||||
></input>
|
||||
<IconButton
|
||||
@@ -362,7 +336,10 @@ export function PluginPage() {
|
||||
<ListItem
|
||||
subTitle={
|
||||
<div
|
||||
className={`markdown-body ${pluginStyles["plugin-content"]}`}
|
||||
className={clsx(
|
||||
"markdown-body",
|
||||
pluginStyles["plugin-content"],
|
||||
)}
|
||||
dir="auto"
|
||||
>
|
||||
<pre>
|
||||
|
1
app/components/realtime-chat/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./realtime-chat";
|
74
app/components/realtime-chat/realtime-chat.module.scss
Normal file
@@ -0,0 +1,74 @@
|
||||
.realtime-chat {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 20px;
|
||||
box-sizing: border-box;
|
||||
.circle-mic {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(to bottom right, #a0d8ef, #f0f8ff);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.icon-center {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.bottom-icons {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
bottom: 20px;
|
||||
box-sizing: border-box;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.icon-left,
|
||||
.icon-right {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
font-size: 36px;
|
||||
background: var(--second);
|
||||
border-radius: 50%;
|
||||
padding: 2px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
&.mobile {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.pulse {
|
||||
animation: pulse 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
359
app/components/realtime-chat/realtime-chat.tsx
Normal file
@@ -0,0 +1,359 @@
|
||||
import VoiceIcon from "@/app/icons/voice.svg";
|
||||
import VoiceOffIcon from "@/app/icons/voice-off.svg";
|
||||
import PowerIcon from "@/app/icons/power.svg";
|
||||
|
||||
import styles from "./realtime-chat.module.scss";
|
||||
import clsx from "clsx";
|
||||
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
|
||||
import { useChatStore, createMessage, useAppConfig } from "@/app/store";
|
||||
|
||||
import { IconButton } from "@/app/components/button";
|
||||
|
||||
import {
|
||||
Modality,
|
||||
RTClient,
|
||||
RTInputAudioItem,
|
||||
RTResponse,
|
||||
TurnDetection,
|
||||
} from "rt-client";
|
||||
import { AudioHandler } from "@/app/lib/audio";
|
||||
import { uploadImage } from "@/app/utils/chat";
|
||||
import { VoicePrint } from "@/app/components/voice-print";
|
||||
|
||||
interface RealtimeChatProps {
|
||||
onClose?: () => void;
|
||||
onStartVoice?: () => void;
|
||||
onPausedVoice?: () => void;
|
||||
}
|
||||
|
||||
export function RealtimeChat({
|
||||
onClose,
|
||||
onStartVoice,
|
||||
onPausedVoice,
|
||||
}: RealtimeChatProps) {
|
||||
const chatStore = useChatStore();
|
||||
const session = chatStore.currentSession();
|
||||
const config = useAppConfig();
|
||||
const [status, setStatus] = useState("");
|
||||
const [isRecording, setIsRecording] = useState(false);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [modality, setModality] = useState("audio");
|
||||
const [useVAD, setUseVAD] = useState(true);
|
||||
const [frequencies, setFrequencies] = useState<Uint8Array | undefined>();
|
||||
|
||||
const clientRef = useRef<RTClient | null>(null);
|
||||
const audioHandlerRef = useRef<AudioHandler | null>(null);
|
||||
const initRef = useRef(false);
|
||||
|
||||
const temperature = config.realtimeConfig.temperature;
|
||||
const apiKey = config.realtimeConfig.apiKey;
|
||||
const model = config.realtimeConfig.model;
|
||||
const azure = config.realtimeConfig.provider === "Azure";
|
||||
const azureEndpoint = config.realtimeConfig.azure.endpoint;
|
||||
const azureDeployment = config.realtimeConfig.azure.deployment;
|
||||
const voice = config.realtimeConfig.voice;
|
||||
|
||||
const handleConnect = async () => {
|
||||
if (isConnecting) return;
|
||||
if (!isConnected) {
|
||||
try {
|
||||
setIsConnecting(true);
|
||||
clientRef.current = azure
|
||||
? new RTClient(
|
||||
new URL(azureEndpoint),
|
||||
{ key: apiKey },
|
||||
{ deployment: azureDeployment },
|
||||
)
|
||||
: new RTClient({ key: apiKey }, { model });
|
||||
const modalities: Modality[] =
|
||||
modality === "audio" ? ["text", "audio"] : ["text"];
|
||||
const turnDetection: TurnDetection = useVAD
|
||||
? { type: "server_vad" }
|
||||
: null;
|
||||
await clientRef.current.configure({
|
||||
instructions: "",
|
||||
voice,
|
||||
input_audio_transcription: { model: "whisper-1" },
|
||||
turn_detection: turnDetection,
|
||||
tools: [],
|
||||
temperature,
|
||||
modalities,
|
||||
});
|
||||
startResponseListener();
|
||||
|
||||
setIsConnected(true);
|
||||
// TODO
|
||||
// try {
|
||||
// const recentMessages = chatStore.getMessagesWithMemory();
|
||||
// for (const message of recentMessages) {
|
||||
// const { role, content } = message;
|
||||
// if (typeof content === "string") {
|
||||
// await clientRef.current.sendItem({
|
||||
// type: "message",
|
||||
// role: role as any,
|
||||
// content: [
|
||||
// {
|
||||
// type: (role === "assistant" ? "text" : "input_text") as any,
|
||||
// text: content as string,
|
||||
// },
|
||||
// ],
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// // await clientRef.current.generateResponse();
|
||||
// } catch (error) {
|
||||
// console.error("Set message failed:", error);
|
||||
// }
|
||||
} catch (error) {
|
||||
console.error("Connection failed:", error);
|
||||
setStatus("Connection failed");
|
||||
} finally {
|
||||
setIsConnecting(false);
|
||||
}
|
||||
} else {
|
||||
await disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
const disconnect = async () => {
|
||||
if (clientRef.current) {
|
||||
try {
|
||||
await clientRef.current.close();
|
||||
clientRef.current = null;
|
||||
setIsConnected(false);
|
||||
} catch (error) {
|
||||
console.error("Disconnect failed:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const startResponseListener = async () => {
|
||||
if (!clientRef.current) return;
|
||||
|
||||
try {
|
||||
for await (const serverEvent of clientRef.current.events()) {
|
||||
if (serverEvent.type === "response") {
|
||||
await handleResponse(serverEvent);
|
||||
} else if (serverEvent.type === "input_audio") {
|
||||
await handleInputAudio(serverEvent);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (clientRef.current) {
|
||||
console.error("Response iteration error:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleResponse = async (response: RTResponse) => {
|
||||
for await (const item of response) {
|
||||
if (item.type === "message" && item.role === "assistant") {
|
||||
const botMessage = createMessage({
|
||||
role: item.role,
|
||||
content: "",
|
||||
});
|
||||
// add bot message first
|
||||
chatStore.updateTargetSession(session, (session) => {
|
||||
session.messages = session.messages.concat([botMessage]);
|
||||
});
|
||||
let hasAudio = false;
|
||||
for await (const content of item) {
|
||||
if (content.type === "text") {
|
||||
for await (const text of content.textChunks()) {
|
||||
botMessage.content += text;
|
||||
}
|
||||
} else if (content.type === "audio") {
|
||||
const textTask = async () => {
|
||||
for await (const text of content.transcriptChunks()) {
|
||||
botMessage.content += text;
|
||||
}
|
||||
};
|
||||
const audioTask = async () => {
|
||||
audioHandlerRef.current?.startStreamingPlayback();
|
||||
for await (const audio of content.audioChunks()) {
|
||||
hasAudio = true;
|
||||
audioHandlerRef.current?.playChunk(audio);
|
||||
}
|
||||
};
|
||||
await Promise.all([textTask(), audioTask()]);
|
||||
}
|
||||
// update message.content
|
||||
chatStore.updateTargetSession(session, (session) => {
|
||||
session.messages = session.messages.concat();
|
||||
});
|
||||
}
|
||||
if (hasAudio) {
|
||||
// upload audio get audio_url
|
||||
const blob = audioHandlerRef.current?.savePlayFile();
|
||||
uploadImage(blob!).then((audio_url) => {
|
||||
botMessage.audio_url = audio_url;
|
||||
// update text and audio_url
|
||||
chatStore.updateTargetSession(session, (session) => {
|
||||
session.messages = session.messages.concat();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleInputAudio = async (item: RTInputAudioItem) => {
|
||||
await item.waitForCompletion();
|
||||
if (item.transcription) {
|
||||
const userMessage = createMessage({
|
||||
role: "user",
|
||||
content: item.transcription,
|
||||
});
|
||||
chatStore.updateTargetSession(session, (session) => {
|
||||
session.messages = session.messages.concat([userMessage]);
|
||||
});
|
||||
// save input audio_url, and update session
|
||||
const { audioStartMillis, audioEndMillis } = item;
|
||||
// upload audio get audio_url
|
||||
const blob = audioHandlerRef.current?.saveRecordFile(
|
||||
audioStartMillis,
|
||||
audioEndMillis,
|
||||
);
|
||||
uploadImage(blob!).then((audio_url) => {
|
||||
userMessage.audio_url = audio_url;
|
||||
chatStore.updateTargetSession(session, (session) => {
|
||||
session.messages = session.messages.concat();
|
||||
});
|
||||
});
|
||||
}
|
||||
// stop streaming play after get input audio.
|
||||
audioHandlerRef.current?.stopStreamingPlayback();
|
||||
};
|
||||
|
||||
const toggleRecording = async () => {
|
||||
if (!isRecording && clientRef.current) {
|
||||
try {
|
||||
if (!audioHandlerRef.current) {
|
||||
audioHandlerRef.current = new AudioHandler();
|
||||
await audioHandlerRef.current.initialize();
|
||||
}
|
||||
await audioHandlerRef.current.startRecording(async (chunk) => {
|
||||
await clientRef.current?.sendAudio(chunk);
|
||||
});
|
||||
setIsRecording(true);
|
||||
} catch (error) {
|
||||
console.error("Failed to start recording:", error);
|
||||
}
|
||||
} else if (audioHandlerRef.current) {
|
||||
try {
|
||||
audioHandlerRef.current.stopRecording();
|
||||
if (!useVAD) {
|
||||
const inputAudio = await clientRef.current?.commitAudio();
|
||||
await handleInputAudio(inputAudio!);
|
||||
await clientRef.current?.generateResponse();
|
||||
}
|
||||
setIsRecording(false);
|
||||
} catch (error) {
|
||||
console.error("Failed to stop recording:", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// 防止重复初始化
|
||||
if (initRef.current) return;
|
||||
initRef.current = true;
|
||||
|
||||
const initAudioHandler = async () => {
|
||||
const handler = new AudioHandler();
|
||||
await handler.initialize();
|
||||
audioHandlerRef.current = handler;
|
||||
await handleConnect();
|
||||
await toggleRecording();
|
||||
};
|
||||
|
||||
initAudioHandler().catch((error) => {
|
||||
setStatus(error);
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
return () => {
|
||||
if (isRecording) {
|
||||
toggleRecording();
|
||||
}
|
||||
audioHandlerRef.current?.close().catch(console.error);
|
||||
disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
let animationFrameId: number;
|
||||
|
||||
if (isConnected && isRecording) {
|
||||
const animationFrame = () => {
|
||||
if (audioHandlerRef.current) {
|
||||
const freqData = audioHandlerRef.current.getByteFrequencyData();
|
||||
setFrequencies(freqData);
|
||||
}
|
||||
animationFrameId = requestAnimationFrame(animationFrame);
|
||||
};
|
||||
|
||||
animationFrameId = requestAnimationFrame(animationFrame);
|
||||
} else {
|
||||
setFrequencies(undefined);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId);
|
||||
}
|
||||
};
|
||||
}, [isConnected, isRecording]);
|
||||
|
||||
// update session params
|
||||
useEffect(() => {
|
||||
clientRef.current?.configure({ voice });
|
||||
}, [voice]);
|
||||
useEffect(() => {
|
||||
clientRef.current?.configure({ temperature });
|
||||
}, [temperature]);
|
||||
|
||||
const handleClose = async () => {
|
||||
onClose?.();
|
||||
if (isRecording) {
|
||||
await toggleRecording();
|
||||
}
|
||||
disconnect().catch(console.error);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles["realtime-chat"]}>
|
||||
<div
|
||||
className={clsx(styles["circle-mic"], {
|
||||
[styles["pulse"]]: isRecording,
|
||||
})}
|
||||
>
|
||||
<VoicePrint frequencies={frequencies} isActive={isRecording} />
|
||||
</div>
|
||||
|
||||
<div className={styles["bottom-icons"]}>
|
||||
<div>
|
||||
<IconButton
|
||||
icon={isRecording ? <VoiceIcon /> : <VoiceOffIcon />}
|
||||
onClick={toggleRecording}
|
||||
disabled={!isConnected}
|
||||
shadow
|
||||
bordered
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["icon-center"]}>{status}</div>
|
||||
<div>
|
||||
<IconButton
|
||||
icon={<PowerIcon />}
|
||||
onClick={handleClose}
|
||||
shadow
|
||||
bordered
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
173
app/components/realtime-chat/realtime-config.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
import { RealtimeConfig } from "@/app/store";
|
||||
|
||||
import Locale from "@/app/locales";
|
||||
import { ListItem, Select, PasswordInput } from "@/app/components/ui-lib";
|
||||
|
||||
import { InputRange } from "@/app/components/input-range";
|
||||
import { Voice } from "rt-client";
|
||||
import { ServiceProvider } from "@/app/constant";
|
||||
|
||||
const providers = [ServiceProvider.OpenAI, ServiceProvider.Azure];
|
||||
|
||||
const models = ["gpt-4o-realtime-preview-2024-10-01"];
|
||||
|
||||
const voice = ["alloy", "shimmer", "echo"];
|
||||
|
||||
export function RealtimeConfigList(props: {
|
||||
realtimeConfig: RealtimeConfig;
|
||||
updateConfig: (updater: (config: RealtimeConfig) => void) => void;
|
||||
}) {
|
||||
const azureConfigComponent = props.realtimeConfig.provider ===
|
||||
ServiceProvider.Azure && (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.Realtime.Azure.Endpoint.Title}
|
||||
subTitle={Locale.Settings.Realtime.Azure.Endpoint.SubTitle}
|
||||
>
|
||||
<input
|
||||
value={props.realtimeConfig?.azure?.endpoint}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Realtime.Azure.Endpoint.Title}
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) => (config.azure.endpoint = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Realtime.Azure.Deployment.Title}
|
||||
subTitle={Locale.Settings.Realtime.Azure.Deployment.SubTitle}
|
||||
>
|
||||
<input
|
||||
value={props.realtimeConfig?.azure?.deployment}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Realtime.Azure.Deployment.Title}
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) => (config.azure.deployment = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.Realtime.Enable.Title}
|
||||
subTitle={Locale.Settings.Realtime.Enable.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={props.realtimeConfig.enable}
|
||||
onChange={(e) =>
|
||||
props.updateConfig(
|
||||
(config) => (config.enable = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
|
||||
{props.realtimeConfig.enable && (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.Realtime.Provider.Title}
|
||||
subTitle={Locale.Settings.Realtime.Provider.SubTitle}
|
||||
>
|
||||
<Select
|
||||
aria-label={Locale.Settings.Realtime.Provider.Title}
|
||||
value={props.realtimeConfig.provider}
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.provider = e.target.value as ServiceProvider),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{providers.map((v, i) => (
|
||||
<option value={v} key={i}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Realtime.Model.Title}
|
||||
subTitle={Locale.Settings.Realtime.Model.SubTitle}
|
||||
>
|
||||
<Select
|
||||
aria-label={Locale.Settings.Realtime.Model.Title}
|
||||
value={props.realtimeConfig.model}
|
||||
onChange={(e) => {
|
||||
props.updateConfig((config) => (config.model = e.target.value));
|
||||
}}
|
||||
>
|
||||
{models.map((v, i) => (
|
||||
<option value={v} key={i}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Realtime.ApiKey.Title}
|
||||
subTitle={Locale.Settings.Realtime.ApiKey.SubTitle}
|
||||
>
|
||||
<PasswordInput
|
||||
aria={Locale.Settings.ShowPassword}
|
||||
aria-label={Locale.Settings.Realtime.ApiKey.Title}
|
||||
value={props.realtimeConfig.apiKey}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Realtime.ApiKey.Placeholder}
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) => (config.apiKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
{azureConfigComponent}
|
||||
<ListItem
|
||||
title={Locale.Settings.TTS.Voice.Title}
|
||||
subTitle={Locale.Settings.TTS.Voice.SubTitle}
|
||||
>
|
||||
<Select
|
||||
value={props.realtimeConfig.voice}
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) => (config.voice = e.currentTarget.value as Voice),
|
||||
);
|
||||
}}
|
||||
>
|
||||
{voice.map((v, i) => (
|
||||
<option value={v} key={i}>
|
||||
{v}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Realtime.Temperature.Title}
|
||||
subTitle={Locale.Settings.Realtime.Temperature.SubTitle}
|
||||
>
|
||||
<InputRange
|
||||
aria={Locale.Settings.Temperature.Title}
|
||||
value={props.realtimeConfig?.temperature?.toFixed(1)}
|
||||
min="0.6"
|
||||
max="1"
|
||||
step="0.1"
|
||||
onChange={(e) => {
|
||||
props.updateConfig(
|
||||
(config) =>
|
||||
(config.temperature = e.currentTarget.valueAsNumber),
|
||||
);
|
||||
}}
|
||||
></InputRange>
|
||||
</ListItem>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
@@ -4,6 +4,7 @@ import { Select } from "@/app/components/ui-lib";
|
||||
import { IconButton } from "@/app/components/button";
|
||||
import Locale from "@/app/locales";
|
||||
import { useSdStore } from "@/app/store/sd";
|
||||
import clsx from "clsx";
|
||||
|
||||
export const params = [
|
||||
{
|
||||
@@ -136,7 +137,7 @@ export function ControlParamItem(props: {
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={styles["ctrl-param-item"] + ` ${props.className || ""}`}>
|
||||
<div className={clsx(styles["ctrl-param-item"], props.className)}>
|
||||
<div className={styles["ctrl-param-item-header"]}>
|
||||
<div className={styles["ctrl-param-item-title"]}>
|
||||
<div>
|
||||
|
@@ -36,6 +36,7 @@ import { removeImage } from "@/app/utils/chat";
|
||||
import { SideBar } from "./sd-sidebar";
|
||||
import { WindowContent } from "@/app/components/home";
|
||||
import { params } from "./sd-panel";
|
||||
import clsx from "clsx";
|
||||
|
||||
function getSdTaskStatus(item: any) {
|
||||
let s: string;
|
||||
@@ -104,7 +105,7 @@ export function Sd() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SideBar className={isSd ? homeStyles["sidebar-show"] : ""} />
|
||||
<SideBar className={clsx({ [homeStyles["sidebar-show"]]: isSd })} />
|
||||
<WindowContent>
|
||||
<div className={chatStyles.chat} key={"1"}>
|
||||
<div className="window-header" data-tauri-drag-region>
|
||||
@@ -121,7 +122,10 @@ export function Sd() {
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={`window-header-title ${chatStyles["chat-body-title"]}`}
|
||||
className={clsx(
|
||||
"window-header-title",
|
||||
chatStyles["chat-body-title"],
|
||||
)}
|
||||
>
|
||||
<div className={`window-header-main-title`}>Stability AI</div>
|
||||
<div className="window-header-sub-title">
|
||||
|
@@ -72,3 +72,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subtitle-button {
|
||||
button {
|
||||
overflow:visible ;
|
||||
}
|
||||
}
|
||||
|
@@ -9,6 +9,7 @@ import CopyIcon from "../icons/copy.svg";
|
||||
import ClearIcon from "../icons/clear.svg";
|
||||
import LoadingIcon from "../icons/three-dots.svg";
|
||||
import EditIcon from "../icons/edit.svg";
|
||||
import FireIcon from "../icons/fire.svg";
|
||||
import EyeIcon from "../icons/eye.svg";
|
||||
import DownloadIcon from "../icons/download.svg";
|
||||
import UploadIcon from "../icons/upload.svg";
|
||||
@@ -18,7 +19,7 @@ import ConfirmIcon from "../icons/confirm.svg";
|
||||
import ConnectionIcon from "../icons/connection.svg";
|
||||
import CloudSuccessIcon from "../icons/cloud-success.svg";
|
||||
import CloudFailIcon from "../icons/cloud-fail.svg";
|
||||
|
||||
import { trackSettingsPageGuideToCPaymentClick } from "../utils/auth-settings-events";
|
||||
import {
|
||||
Input,
|
||||
List,
|
||||
@@ -48,7 +49,7 @@ import Locale, {
|
||||
changeLang,
|
||||
getLang,
|
||||
} from "../locales";
|
||||
import { copyToClipboard } from "../utils";
|
||||
import { copyToClipboard, clientUpdate, semverCompare } from "../utils";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Anthropic,
|
||||
@@ -58,6 +59,7 @@ import {
|
||||
ByteDance,
|
||||
Alibaba,
|
||||
Moonshot,
|
||||
XAI,
|
||||
Google,
|
||||
GoogleSafetySettingsThreshold,
|
||||
OPENAI_BASE_URL,
|
||||
@@ -69,6 +71,9 @@ import {
|
||||
UPDATE_URL,
|
||||
Stability,
|
||||
Iflytek,
|
||||
SAAS_CHAT_URL,
|
||||
ChatGLM,
|
||||
DeepSeek,
|
||||
} from "../constant";
|
||||
import { Prompt, SearchService, usePromptStore } from "../store/prompt";
|
||||
import { ErrorBoundary } from "./error";
|
||||
@@ -81,6 +86,7 @@ import { nanoid } from "nanoid";
|
||||
import { useMaskStore } from "../store/mask";
|
||||
import { ProviderType } from "../utils/cloud";
|
||||
import { TTSConfigList } from "./tts-config";
|
||||
import { RealtimeConfigList } from "./realtime-chat/realtime-config";
|
||||
|
||||
function EditPromptModal(props: { id: string; onClose: () => void }) {
|
||||
const promptStore = usePromptStore();
|
||||
@@ -583,7 +589,7 @@ export function Settings() {
|
||||
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
||||
const currentVersion = updateStore.formatVersion(updateStore.version);
|
||||
const remoteId = updateStore.formatVersion(updateStore.remoteVersion);
|
||||
const hasNewVersion = currentVersion !== remoteId;
|
||||
const hasNewVersion = semverCompare(currentVersion, remoteId) === -1;
|
||||
const updateUrl = getClientConfig()?.isApp ? RELEASE_URL : UPDATE_URL;
|
||||
|
||||
function checkUpdate(force = false) {
|
||||
@@ -686,6 +692,31 @@ export function Settings() {
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
const saasStartComponent = (
|
||||
<ListItem
|
||||
className={styles["subtitle-button"]}
|
||||
title={
|
||||
Locale.Settings.Access.SaasStart.Title +
|
||||
`${Locale.Settings.Access.SaasStart.Label}`
|
||||
}
|
||||
subTitle={Locale.Settings.Access.SaasStart.SubTitle}
|
||||
>
|
||||
<IconButton
|
||||
aria={
|
||||
Locale.Settings.Access.SaasStart.Title +
|
||||
Locale.Settings.Access.SaasStart.ChatNow
|
||||
}
|
||||
icon={<FireIcon />}
|
||||
type={"primary"}
|
||||
text={Locale.Settings.Access.SaasStart.ChatNow}
|
||||
onClick={() => {
|
||||
trackSettingsPageGuideToCPaymentClick();
|
||||
window.location.href = SAAS_CHAT_URL;
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
const useCustomConfigComponent = // Conditionally render the following ListItem based on clientConfig.isApp
|
||||
!clientConfig?.isApp && ( // only show if isApp is false
|
||||
<ListItem
|
||||
@@ -1167,6 +1198,127 @@ export function Settings() {
|
||||
</>
|
||||
);
|
||||
|
||||
const deepseekConfigComponent = accessStore.provider ===
|
||||
ServiceProvider.DeepSeek && (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.DeepSeek.Endpoint.Title}
|
||||
subTitle={
|
||||
Locale.Settings.Access.DeepSeek.Endpoint.SubTitle +
|
||||
DeepSeek.ExampleEndpoint
|
||||
}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Settings.Access.DeepSeek.Endpoint.Title}
|
||||
type="text"
|
||||
value={accessStore.deepseekUrl}
|
||||
placeholder={DeepSeek.ExampleEndpoint}
|
||||
onChange={(e) =>
|
||||
accessStore.update(
|
||||
(access) => (access.deepseekUrl = e.currentTarget.value),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.DeepSeek.ApiKey.Title}
|
||||
subTitle={Locale.Settings.Access.DeepSeek.ApiKey.SubTitle}
|
||||
>
|
||||
<PasswordInput
|
||||
aria-label={Locale.Settings.Access.DeepSeek.ApiKey.Title}
|
||||
value={accessStore.deepseekApiKey}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Access.DeepSeek.ApiKey.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.deepseekApiKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
|
||||
const XAIConfigComponent = accessStore.provider === ServiceProvider.XAI && (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.XAI.Endpoint.Title}
|
||||
subTitle={
|
||||
Locale.Settings.Access.XAI.Endpoint.SubTitle + XAI.ExampleEndpoint
|
||||
}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Settings.Access.XAI.Endpoint.Title}
|
||||
type="text"
|
||||
value={accessStore.xaiUrl}
|
||||
placeholder={XAI.ExampleEndpoint}
|
||||
onChange={(e) =>
|
||||
accessStore.update(
|
||||
(access) => (access.xaiUrl = e.currentTarget.value),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.XAI.ApiKey.Title}
|
||||
subTitle={Locale.Settings.Access.XAI.ApiKey.SubTitle}
|
||||
>
|
||||
<PasswordInput
|
||||
aria-label={Locale.Settings.Access.XAI.ApiKey.Title}
|
||||
value={accessStore.xaiApiKey}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Access.XAI.ApiKey.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.xaiApiKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
|
||||
const chatglmConfigComponent = accessStore.provider ===
|
||||
ServiceProvider.ChatGLM && (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.ChatGLM.Endpoint.Title}
|
||||
subTitle={
|
||||
Locale.Settings.Access.ChatGLM.Endpoint.SubTitle +
|
||||
ChatGLM.ExampleEndpoint
|
||||
}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Settings.Access.ChatGLM.Endpoint.Title}
|
||||
type="text"
|
||||
value={accessStore.chatglmUrl}
|
||||
placeholder={ChatGLM.ExampleEndpoint}
|
||||
onChange={(e) =>
|
||||
accessStore.update(
|
||||
(access) => (access.chatglmUrl = e.currentTarget.value),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.ChatGLM.ApiKey.Title}
|
||||
subTitle={Locale.Settings.Access.ChatGLM.ApiKey.SubTitle}
|
||||
>
|
||||
<PasswordInput
|
||||
aria-label={Locale.Settings.Access.ChatGLM.ApiKey.Title}
|
||||
value={accessStore.chatglmApiKey}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Access.ChatGLM.ApiKey.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.chatglmApiKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
|
||||
const stabilityConfigComponent = accessStore.provider ===
|
||||
ServiceProvider.Stability && (
|
||||
<>
|
||||
@@ -1330,9 +1482,17 @@ export function Settings() {
|
||||
{checkingUpdate ? (
|
||||
<LoadingIcon />
|
||||
) : hasNewVersion ? (
|
||||
<Link href={updateUrl} target="_blank" className="link">
|
||||
{Locale.Settings.Update.GoToUpdate}
|
||||
</Link>
|
||||
clientConfig?.isApp ? (
|
||||
<IconButton
|
||||
icon={<ResetIcon></ResetIcon>}
|
||||
text={Locale.Settings.Update.GoToUpdate}
|
||||
onClick={() => clientUpdate()}
|
||||
/>
|
||||
) : (
|
||||
<Link href={updateUrl} target="_blank" className="link">
|
||||
{Locale.Settings.Update.GoToUpdate}
|
||||
</Link>
|
||||
)
|
||||
) : (
|
||||
<IconButton
|
||||
icon={<ResetIcon></ResetIcon>}
|
||||
@@ -1465,6 +1625,39 @@ export function Settings() {
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={Locale.Mask.Config.Artifacts.Title}
|
||||
subTitle={Locale.Mask.Config.Artifacts.SubTitle}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Mask.Config.Artifacts.Title}
|
||||
type="checkbox"
|
||||
checked={config.enableArtifacts}
|
||||
onChange={(e) =>
|
||||
updateConfig(
|
||||
(config) =>
|
||||
(config.enableArtifacts = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Mask.Config.CodeFold.Title}
|
||||
subTitle={Locale.Mask.Config.CodeFold.SubTitle}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Mask.Config.CodeFold.Title}
|
||||
type="checkbox"
|
||||
checked={config.enableCodeFold}
|
||||
data-testid="enable-code-fold-checkbox"
|
||||
onChange={(e) =>
|
||||
updateConfig(
|
||||
(config) => (config.enableCodeFold = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<SyncItems />
|
||||
@@ -1541,6 +1734,7 @@ export function Settings() {
|
||||
</List>
|
||||
|
||||
<List id={SlotID.CustomModel}>
|
||||
{saasStartComponent}
|
||||
{accessCodeComponent}
|
||||
|
||||
{!accessStore.hideUserApiKey && (
|
||||
@@ -1581,8 +1775,11 @@ export function Settings() {
|
||||
{alibabaConfigComponent}
|
||||
{tencentConfigComponent}
|
||||
{moonshotConfigComponent}
|
||||
{deepseekConfigComponent}
|
||||
{stabilityConfigComponent}
|
||||
{lflytekConfigComponent}
|
||||
{XAIConfigComponent}
|
||||
{chatglmConfigComponent}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@@ -1617,9 +1814,11 @@ export function Settings() {
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.CustomModel.Title}
|
||||
subTitle={Locale.Settings.Access.CustomModel.SubTitle}
|
||||
vertical={true}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Settings.Access.CustomModel.Title}
|
||||
style={{ width: "100%", maxWidth: "unset", textAlign: "left" }}
|
||||
type="text"
|
||||
value={config.customModels}
|
||||
placeholder="model1,model2,model3"
|
||||
@@ -1646,7 +1845,18 @@ export function Settings() {
|
||||
{shouldShowPromptModal && (
|
||||
<UserPromptModal onClose={() => setShowPromptModal(false)} />
|
||||
)}
|
||||
|
||||
<List>
|
||||
<RealtimeConfigList
|
||||
realtimeConfig={config.realtimeConfig}
|
||||
updateConfig={(updater) => {
|
||||
const realtimeConfig = { ...config.realtimeConfig };
|
||||
updater(realtimeConfig);
|
||||
config.update(
|
||||
(config) => (config.realtimeConfig = realtimeConfig),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</List>
|
||||
<List>
|
||||
<TTSConfigList
|
||||
ttsConfig={config.ttsConfig}
|
||||
|
@@ -22,7 +22,6 @@ import {
|
||||
MIN_SIDEBAR_WIDTH,
|
||||
NARROW_SIDEBAR_WIDTH,
|
||||
Path,
|
||||
PLUGINS,
|
||||
REPO_URL,
|
||||
} from "../constant";
|
||||
|
||||
@@ -30,6 +29,13 @@ import { Link, useNavigate } from "react-router-dom";
|
||||
import { isIOS, useMobileScreen } from "../utils";
|
||||
import dynamic from "next/dynamic";
|
||||
import { showConfirm, Selector } from "./ui-lib";
|
||||
import clsx from "clsx";
|
||||
|
||||
const DISCOVERY = [
|
||||
{ name: Locale.Plugin.Name, path: Path.Plugins },
|
||||
{ name: "Stable Diffusion", path: Path.Sd },
|
||||
{ name: Locale.SearchChat.Page.Title, path: Path.SearchChat },
|
||||
];
|
||||
|
||||
const ChatList = dynamic(async () => (await import("./chat-list")).ChatList, {
|
||||
loading: () => null,
|
||||
@@ -141,9 +147,9 @@ export function SideBarContainer(props: {
|
||||
const { children, className, onDragStart, shouldNarrow } = props;
|
||||
return (
|
||||
<div
|
||||
className={`${styles.sidebar} ${className} ${
|
||||
shouldNarrow && styles["narrow-sidebar"]
|
||||
}`}
|
||||
className={clsx(styles.sidebar, className, {
|
||||
[styles["narrow-sidebar"]]: shouldNarrow,
|
||||
})}
|
||||
style={{
|
||||
// #3016 disable transition on ios mobile screen
|
||||
transition: isMobileScreen && isIOSMobile ? "none" : undefined,
|
||||
@@ -165,18 +171,24 @@ export function SideBarHeader(props: {
|
||||
subTitle?: string | React.ReactNode;
|
||||
logo?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
shouldNarrow?: boolean;
|
||||
}) {
|
||||
const { title, subTitle, logo, children } = props;
|
||||
const { title, subTitle, logo, children, shouldNarrow } = props;
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={styles["sidebar-header"]} data-tauri-drag-region>
|
||||
<div
|
||||
className={clsx(styles["sidebar-header"], {
|
||||
[styles["sidebar-header-narrow"]]: shouldNarrow,
|
||||
})}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div className={styles["sidebar-title-container"]}>
|
||||
<div className={styles["sidebar-title"]} data-tauri-drag-region>
|
||||
{title}
|
||||
</div>
|
||||
<div className={styles["sidebar-sub-title"]}>{subTitle}</div>
|
||||
</div>
|
||||
<div className={styles["sidebar-logo"] + " no-dark"}>{logo}</div>
|
||||
<div className={clsx(styles["sidebar-logo"], "no-dark")}>{logo}</div>
|
||||
</div>
|
||||
{children}
|
||||
</Fragment>
|
||||
@@ -212,7 +224,7 @@ export function SideBarTail(props: {
|
||||
export function SideBar(props: { className?: string }) {
|
||||
useHotKey();
|
||||
const { onDragStart, shouldNarrow } = useDragSideBar();
|
||||
const [showPluginSelector, setShowPluginSelector] = useState(false);
|
||||
const [showDiscoverySelector, setshowDiscoverySelector] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const config = useAppConfig();
|
||||
const chatStore = useChatStore();
|
||||
@@ -227,6 +239,7 @@ export function SideBar(props: { className?: string }) {
|
||||
title="NextChat"
|
||||
subTitle="Build your own AI assistant."
|
||||
logo={<ChatGptIcon />}
|
||||
shouldNarrow={shouldNarrow}
|
||||
>
|
||||
<div className={styles["sidebar-header-bar"]}>
|
||||
<IconButton
|
||||
@@ -246,21 +259,21 @@ export function SideBar(props: { className?: string }) {
|
||||
icon={<DiscoveryIcon />}
|
||||
text={shouldNarrow ? undefined : Locale.Discovery.Name}
|
||||
className={styles["sidebar-bar-button"]}
|
||||
onClick={() => setShowPluginSelector(true)}
|
||||
onClick={() => setshowDiscoverySelector(true)}
|
||||
shadow
|
||||
/>
|
||||
</div>
|
||||
{showPluginSelector && (
|
||||
{showDiscoverySelector && (
|
||||
<Selector
|
||||
items={[
|
||||
...PLUGINS.map((item) => {
|
||||
...DISCOVERY.map((item) => {
|
||||
return {
|
||||
title: item.name,
|
||||
value: item.path,
|
||||
};
|
||||
}),
|
||||
]}
|
||||
onClose={() => setShowPluginSelector(false)}
|
||||
onClose={() => setshowDiscoverySelector(false)}
|
||||
onSelection={(s) => {
|
||||
navigate(s[0], { state: { fromHome: true } });
|
||||
}}
|
||||
@@ -279,7 +292,7 @@ export function SideBar(props: { className?: string }) {
|
||||
<SideBarTail
|
||||
primaryAction={
|
||||
<>
|
||||
<div className={styles["sidebar-action"] + " " + styles.mobile}>
|
||||
<div className={clsx(styles["sidebar-action"], styles.mobile)}>
|
||||
<IconButton
|
||||
icon={<DeleteIcon />}
|
||||
onClick={async () => {
|
||||
|
@@ -62,14 +62,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical{
|
||||
&.vertical {
|
||||
flex-direction: column;
|
||||
align-items: start;
|
||||
.list-header{
|
||||
.list-item-title{
|
||||
.list-header {
|
||||
.list-item-title {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.list-item-sub-title{
|
||||
.list-item-sub-title {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
}
|
||||
@@ -310,7 +310,7 @@
|
||||
justify-content: center;
|
||||
z-index: 999;
|
||||
|
||||
.selector-item-disabled{
|
||||
.selector-item-disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@@ -336,3 +336,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -23,6 +23,7 @@ import React, {
|
||||
useRef,
|
||||
} from "react";
|
||||
import { IconButton } from "./button";
|
||||
import clsx from "clsx";
|
||||
|
||||
export function Popover(props: {
|
||||
children: JSX.Element;
|
||||
@@ -45,7 +46,7 @@ export function Popover(props: {
|
||||
|
||||
export function Card(props: { children: JSX.Element[]; className?: string }) {
|
||||
return (
|
||||
<div className={styles.card + " " + props.className}>{props.children}</div>
|
||||
<div className={clsx(styles.card, props.className)}>{props.children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,11 +61,13 @@ export function ListItem(props: {
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
styles["list-item"] +
|
||||
` ${props.vertical ? styles["vertical"] : ""} ` +
|
||||
` ${props.className || ""}`
|
||||
}
|
||||
className={clsx(
|
||||
styles["list-item"],
|
||||
{
|
||||
[styles["vertical"]]: props.vertical,
|
||||
},
|
||||
props.className,
|
||||
)}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<div className={styles["list-header"]}>
|
||||
@@ -135,9 +138,9 @@ export function Modal(props: ModalProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
styles["modal-container"] + ` ${isMax && styles["modal-container-max"]}`
|
||||
}
|
||||
className={clsx(styles["modal-container"], {
|
||||
[styles["modal-container-max"]]: isMax,
|
||||
})}
|
||||
>
|
||||
<div className={styles["modal-header"]}>
|
||||
<div className={styles["modal-title"]}>{props.title}</div>
|
||||
@@ -260,7 +263,7 @@ export function Input(props: InputProps) {
|
||||
return (
|
||||
<textarea
|
||||
{...props}
|
||||
className={`${styles["input"]} ${props.className}`}
|
||||
className={clsx(styles["input"], props.className)}
|
||||
></textarea>
|
||||
);
|
||||
}
|
||||
@@ -301,9 +304,13 @@ export function Select(
|
||||
const { className, children, align, ...otherProps } = props;
|
||||
return (
|
||||
<div
|
||||
className={`${styles["select-with-icon"]} ${
|
||||
align === "left" ? styles["left-align-option"] : ""
|
||||
} ${className}`}
|
||||
className={clsx(
|
||||
styles["select-with-icon"],
|
||||
{
|
||||
[styles["left-align-option"]]: align === "left",
|
||||
},
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<select className={styles["select-with-icon-select"]} {...otherProps}>
|
||||
{children}
|
||||
@@ -509,9 +516,9 @@ export function Selector<T>(props: {
|
||||
const selected = selectedValues.includes(item.value);
|
||||
return (
|
||||
<ListItem
|
||||
className={`${styles["selector-item"]} ${
|
||||
item.disable && styles["selector-item-disabled"]
|
||||
}`}
|
||||
className={clsx(styles["selector-item"], {
|
||||
[styles["selector-item-disabled"]]: item.disable,
|
||||
})}
|
||||
key={i}
|
||||
title={item.title}
|
||||
subTitle={item.subTitle}
|
||||
|
1
app/components/voice-print/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./voice-print";
|
11
app/components/voice-print/voice-print.module.scss
Normal file
@@ -0,0 +1,11 @@
|
||||
.voice-print {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
margin: 20px 0;
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
filter: brightness(1.2); // 增加整体亮度
|
||||
}
|
||||
}
|
180
app/components/voice-print/voice-print.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import { useEffect, useRef, useCallback } from "react";
|
||||
import styles from "./voice-print.module.scss";
|
||||
|
||||
interface VoicePrintProps {
|
||||
frequencies?: Uint8Array;
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
export function VoicePrint({ frequencies, isActive }: VoicePrintProps) {
|
||||
// Canvas引用,用于获取绘图上下文
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
// 存储历史频率数据,用于平滑处理
|
||||
const historyRef = useRef<number[][]>([]);
|
||||
// 控制保留的历史数据帧数,影响平滑度
|
||||
const historyLengthRef = useRef(10);
|
||||
// 存储动画帧ID,用于清理
|
||||
const animationFrameRef = useRef<number>();
|
||||
|
||||
/**
|
||||
* 更新频率历史数据
|
||||
* 使用FIFO队列维护固定长度的历史记录
|
||||
*/
|
||||
const updateHistory = useCallback((freqArray: number[]) => {
|
||||
historyRef.current.push(freqArray);
|
||||
if (historyRef.current.length > historyLengthRef.current) {
|
||||
historyRef.current.shift();
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
/**
|
||||
* 处理高DPI屏幕显示
|
||||
* 根据设备像素比例调整canvas实际渲染分辨率
|
||||
*/
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
canvas.width = canvas.offsetWidth * dpr;
|
||||
canvas.height = canvas.offsetHeight * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
/**
|
||||
* 主要绘制函数
|
||||
* 使用requestAnimationFrame实现平滑动画
|
||||
* 包含以下步骤:
|
||||
* 1. 清空画布
|
||||
* 2. 更新历史数据
|
||||
* 3. 计算波形点
|
||||
* 4. 绘制上下对称的声纹
|
||||
*/
|
||||
const draw = () => {
|
||||
// 清空画布
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (!frequencies || !isActive) {
|
||||
historyRef.current = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const freqArray = Array.from(frequencies);
|
||||
updateHistory(freqArray);
|
||||
|
||||
// 绘制声纹
|
||||
const points: [number, number][] = [];
|
||||
const centerY = canvas.height / 2;
|
||||
const width = canvas.width;
|
||||
const sliceWidth = width / (frequencies.length - 1);
|
||||
|
||||
// 绘制主波形
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, centerY);
|
||||
|
||||
/**
|
||||
* 声纹绘制算法:
|
||||
* 1. 使用历史数据平均值实现平滑过渡
|
||||
* 2. 通过正弦函数添加自然波动
|
||||
* 3. 使用贝塞尔曲线连接点,使曲线更平滑
|
||||
* 4. 绘制对称部分形成完整声纹
|
||||
*/
|
||||
for (let i = 0; i < frequencies.length; i++) {
|
||||
const x = i * sliceWidth;
|
||||
let avgFrequency = frequencies[i];
|
||||
|
||||
/**
|
||||
* 波形平滑处理:
|
||||
* 1. 收集历史数据中对应位置的频率值
|
||||
* 2. 计算当前值与历史值的加权平均
|
||||
* 3. 根据平均值计算实际显示高度
|
||||
*/
|
||||
if (historyRef.current.length > 0) {
|
||||
const historicalValues = historyRef.current.map((h) => h[i] || 0);
|
||||
avgFrequency =
|
||||
(avgFrequency + historicalValues.reduce((a, b) => a + b, 0)) /
|
||||
(historyRef.current.length + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* 波形变换:
|
||||
* 1. 归一化频率值到0-1范围
|
||||
* 2. 添加时间相关的正弦变换
|
||||
* 3. 使用贝塞尔曲线平滑连接点
|
||||
*/
|
||||
const normalized = avgFrequency / 255.0;
|
||||
const height = normalized * (canvas.height / 2);
|
||||
const y = centerY + height * Math.sin(i * 0.2 + Date.now() * 0.002);
|
||||
|
||||
points.push([x, y]);
|
||||
|
||||
if (i === 0) {
|
||||
ctx.moveTo(x, y);
|
||||
} else {
|
||||
// 使用贝塞尔曲线使波形更平滑
|
||||
const prevPoint = points[i - 1];
|
||||
const midX = (prevPoint[0] + x) / 2;
|
||||
ctx.quadraticCurveTo(
|
||||
prevPoint[0],
|
||||
prevPoint[1],
|
||||
midX,
|
||||
(prevPoint[1] + y) / 2,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制对称的下半部分
|
||||
for (let i = points.length - 1; i >= 0; i--) {
|
||||
const [x, y] = points[i];
|
||||
const symmetricY = centerY - (y - centerY);
|
||||
if (i === points.length - 1) {
|
||||
ctx.lineTo(x, symmetricY);
|
||||
} else {
|
||||
const nextPoint = points[i + 1];
|
||||
const midX = (nextPoint[0] + x) / 2;
|
||||
ctx.quadraticCurveTo(
|
||||
nextPoint[0],
|
||||
centerY - (nextPoint[1] - centerY),
|
||||
midX,
|
||||
centerY - ((nextPoint[1] + y) / 2 - centerY),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ctx.closePath();
|
||||
|
||||
/**
|
||||
* 渐变效果:
|
||||
* 从左到右应用三色渐变,带透明度
|
||||
* 使用蓝色系配色提升视觉效果
|
||||
*/
|
||||
const gradient = ctx.createLinearGradient(0, 0, canvas.width, 0);
|
||||
gradient.addColorStop(0, "rgba(100, 180, 255, 0.95)");
|
||||
gradient.addColorStop(0.5, "rgba(140, 200, 255, 0.9)");
|
||||
gradient.addColorStop(1, "rgba(180, 220, 255, 0.95)");
|
||||
|
||||
ctx.fillStyle = gradient;
|
||||
ctx.fill();
|
||||
|
||||
animationFrameRef.current = requestAnimationFrame(draw);
|
||||
};
|
||||
|
||||
// 启动动画循环
|
||||
draw();
|
||||
|
||||
// 清理函数:在组件卸载时取消动画
|
||||
return () => {
|
||||
if (animationFrameRef.current) {
|
||||
cancelAnimationFrame(animationFrameRef.current);
|
||||
}
|
||||
};
|
||||
}, [frequencies, isActive, updateHistory]);
|
||||
|
||||
return (
|
||||
<div className={styles["voice-print"]}>
|
||||
<canvas ref={canvasRef} />
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -1,5 +1,6 @@
|
||||
import md5 from "spark-md5";
|
||||
import { DEFAULT_MODELS, DEFAULT_GA_ID } from "../constant";
|
||||
import { isGPT4Model } from "../utils/model";
|
||||
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
@@ -22,6 +23,7 @@ declare global {
|
||||
DISABLE_FAST_LINK?: string; // disallow parse settings from url or not
|
||||
CUSTOM_MODELS?: string; // to control custom models
|
||||
DEFAULT_MODEL?: string; // to control default model in every new chat window
|
||||
VISION_MODELS?: string; // to control vision models
|
||||
|
||||
// stability only
|
||||
STABILITY_URL?: string;
|
||||
@@ -71,6 +73,17 @@ declare global {
|
||||
IFLYTEK_API_KEY?: string;
|
||||
IFLYTEK_API_SECRET?: string;
|
||||
|
||||
DEEPSEEK_URL?: string;
|
||||
DEEPSEEK_API_KEY?: string;
|
||||
|
||||
// xai only
|
||||
XAI_URL?: string;
|
||||
XAI_API_KEY?: string;
|
||||
|
||||
// chatglm only
|
||||
CHATGLM_URL?: string;
|
||||
CHATGLM_API_KEY?: string;
|
||||
|
||||
// custom template for preprocessing user input
|
||||
DEFAULT_INPUT_TEMPLATE?: string;
|
||||
}
|
||||
@@ -116,22 +129,16 @@ export const getServerSideConfig = () => {
|
||||
const disableGPT4 = !!process.env.DISABLE_GPT4;
|
||||
let customModels = process.env.CUSTOM_MODELS ?? "";
|
||||
let defaultModel = process.env.DEFAULT_MODEL ?? "";
|
||||
let visionModels = process.env.VISION_MODELS ?? "";
|
||||
|
||||
if (disableGPT4) {
|
||||
if (customModels) customModels += ",";
|
||||
customModels += DEFAULT_MODELS.filter(
|
||||
(m) =>
|
||||
(m.name.startsWith("gpt-4") || m.name.startsWith("chatgpt-4o")) &&
|
||||
!m.name.startsWith("gpt-4o-mini"),
|
||||
)
|
||||
customModels += DEFAULT_MODELS.filter((m) => isGPT4Model(m.name))
|
||||
.map((m) => "-" + m.name)
|
||||
.join(",");
|
||||
if (
|
||||
(defaultModel.startsWith("gpt-4") ||
|
||||
defaultModel.startsWith("chatgpt-4o")) &&
|
||||
!defaultModel.startsWith("gpt-4o-mini")
|
||||
)
|
||||
if (defaultModel && isGPT4Model(defaultModel)) {
|
||||
defaultModel = "";
|
||||
}
|
||||
}
|
||||
|
||||
const isStability = !!process.env.STABILITY_API_KEY;
|
||||
@@ -146,6 +153,9 @@ export const getServerSideConfig = () => {
|
||||
const isAlibaba = !!process.env.ALIBABA_API_KEY;
|
||||
const isMoonshot = !!process.env.MOONSHOT_API_KEY;
|
||||
const isIflytek = !!process.env.IFLYTEK_API_KEY;
|
||||
const isDeepSeek = !!process.env.DEEPSEEK_API_KEY;
|
||||
const isXAI = !!process.env.XAI_API_KEY;
|
||||
const isChatGLM = !!process.env.CHATGLM_API_KEY;
|
||||
// const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
|
||||
// const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
|
||||
// const randomIndex = Math.floor(Math.random() * apiKeys.length);
|
||||
@@ -154,8 +164,8 @@ export const getServerSideConfig = () => {
|
||||
// `[Server Config] using ${randomIndex + 1} of ${apiKeys.length} api key`,
|
||||
// );
|
||||
|
||||
const allowedWebDevEndpoints = (
|
||||
process.env.WHITE_WEBDEV_ENDPOINTS ?? ""
|
||||
const allowedWebDavEndpoints = (
|
||||
process.env.WHITE_WEBDAV_ENDPOINTS ?? ""
|
||||
).split(",");
|
||||
|
||||
return {
|
||||
@@ -208,6 +218,18 @@ export const getServerSideConfig = () => {
|
||||
iflytekApiKey: process.env.IFLYTEK_API_KEY,
|
||||
iflytekApiSecret: process.env.IFLYTEK_API_SECRET,
|
||||
|
||||
isDeepSeek,
|
||||
deepseekUrl: process.env.DEEPSEEK_URL,
|
||||
deepseekApiKey: getApiKey(process.env.DEEPSEEK_API_KEY),
|
||||
|
||||
isXAI,
|
||||
xaiUrl: process.env.XAI_URL,
|
||||
xaiApiKey: getApiKey(process.env.XAI_API_KEY),
|
||||
|
||||
isChatGLM,
|
||||
chatglmUrl: process.env.CHATGLM_URL,
|
||||
chatglmApiKey: getApiKey(process.env.CHATGLM_API_KEY),
|
||||
|
||||
cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
||||
cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID,
|
||||
cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY),
|
||||
@@ -229,6 +251,7 @@ export const getServerSideConfig = () => {
|
||||
disableFastLink: !!process.env.DISABLE_FAST_LINK,
|
||||
customModels,
|
||||
defaultModel,
|
||||
allowedWebDevEndpoints,
|
||||
visionModels,
|
||||
allowedWebDavEndpoints,
|
||||
};
|
||||
};
|
||||
|
141
app/constant.ts
@@ -11,7 +11,6 @@ export const RUNTIME_CONFIG_DOM = "danger-runtime-config";
|
||||
|
||||
export const STABILITY_BASE_URL = "https://api.stability.ai";
|
||||
|
||||
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";
|
||||
|
||||
@@ -29,6 +28,12 @@ export const TENCENT_BASE_URL = "https://hunyuan.tencentcloudapi.com";
|
||||
export const MOONSHOT_BASE_URL = "https://api.moonshot.cn";
|
||||
export const IFLYTEK_BASE_URL = "https://spark-api-open.xf-yun.com";
|
||||
|
||||
export const DEEPSEEK_BASE_URL = "https://api.deepseek.com";
|
||||
|
||||
export const XAI_BASE_URL = "https://api.x.ai";
|
||||
|
||||
export const CHATGLM_BASE_URL = "https://open.bigmodel.cn";
|
||||
|
||||
export const CACHE_URL_PREFIX = "/api/cache";
|
||||
export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
|
||||
|
||||
@@ -60,6 +65,9 @@ export enum ApiPath {
|
||||
Iflytek = "/api/iflytek",
|
||||
Stability = "/api/stability",
|
||||
Artifacts = "/api/artifacts",
|
||||
XAI = "/api/xai",
|
||||
ChatGLM = "/api/chatglm",
|
||||
DeepSeek = "/api/deepseek",
|
||||
}
|
||||
|
||||
export enum SlotID {
|
||||
@@ -112,6 +120,9 @@ export enum ServiceProvider {
|
||||
Moonshot = "Moonshot",
|
||||
Stability = "Stability",
|
||||
Iflytek = "Iflytek",
|
||||
XAI = "XAI",
|
||||
ChatGLM = "ChatGLM",
|
||||
DeepSeek = "DeepSeek",
|
||||
}
|
||||
|
||||
// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
|
||||
@@ -134,6 +145,9 @@ export enum ModelProvider {
|
||||
Hunyuan = "Hunyuan",
|
||||
Moonshot = "Moonshot",
|
||||
Iflytek = "Iflytek",
|
||||
XAI = "XAI",
|
||||
ChatGLM = "ChatGLM",
|
||||
DeepSeek = "DeepSeek",
|
||||
}
|
||||
|
||||
export const Stability = {
|
||||
@@ -216,6 +230,23 @@ export const Iflytek = {
|
||||
ChatPath: "v1/chat/completions",
|
||||
};
|
||||
|
||||
export const DeepSeek = {
|
||||
ExampleEndpoint: DEEPSEEK_BASE_URL,
|
||||
ChatPath: "chat/completions",
|
||||
};
|
||||
|
||||
export const XAI = {
|
||||
ExampleEndpoint: XAI_BASE_URL,
|
||||
ChatPath: "v1/chat/completions",
|
||||
};
|
||||
|
||||
export const ChatGLM = {
|
||||
ExampleEndpoint: CHATGLM_BASE_URL,
|
||||
ChatPath: "api/paas/v4/chat/completions",
|
||||
ImagePath: "api/paas/v4/images/generations",
|
||||
VideoPath: "api/paas/v4/videos/generations",
|
||||
};
|
||||
|
||||
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
|
||||
// export const DEFAULT_SYSTEM_TEMPLATE = `
|
||||
// You are ChatGPT, a large language model trained by {{ServiceProvider}}.
|
||||
@@ -245,6 +276,7 @@ export const KnowledgeCutOffDate: Record<string, string> = {
|
||||
"gpt-4o": "2023-10",
|
||||
"gpt-4o-2024-05-13": "2023-10",
|
||||
"gpt-4o-2024-08-06": "2023-10",
|
||||
"gpt-4o-2024-11-20": "2023-10",
|
||||
"chatgpt-4o-latest": "2023-10",
|
||||
"gpt-4o-mini": "2023-10",
|
||||
"gpt-4o-mini-2024-07-18": "2023-10",
|
||||
@@ -255,6 +287,8 @@ export const KnowledgeCutOffDate: Record<string, string> = {
|
||||
// it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously.
|
||||
"gemini-pro": "2023-12",
|
||||
"gemini-pro-vision": "2023-12",
|
||||
"deepseek-chat": "2024-07",
|
||||
"deepseek-coder": "2024-07",
|
||||
};
|
||||
|
||||
export const DEFAULT_TTS_ENGINE = "OpenAI-TTS";
|
||||
@@ -271,6 +305,23 @@ export const DEFAULT_TTS_VOICES = [
|
||||
"shimmer",
|
||||
];
|
||||
|
||||
export const VISION_MODEL_REGEXES = [
|
||||
/vision/,
|
||||
/gpt-4o/,
|
||||
/claude-3/,
|
||||
/gemini-1\.5/,
|
||||
/gemini-exp/,
|
||||
/gemini-2\.0/,
|
||||
/learnlm/,
|
||||
/qwen-vl/,
|
||||
/qwen2-vl/,
|
||||
/gpt-4-turbo(?!.*preview)/, // Matches "gpt-4-turbo" but not "gpt-4-turbo-preview"
|
||||
/^dall-e-3$/, // Matches exactly "dall-e-3"
|
||||
/glm-4v/,
|
||||
];
|
||||
|
||||
export const EXCLUDE_VISION_MODEL_REGEXES = [/claude-3-5-haiku-20241022/];
|
||||
|
||||
const openaiModels = [
|
||||
"gpt-3.5-turbo",
|
||||
"gpt-3.5-turbo-1106",
|
||||
@@ -284,6 +335,7 @@ const openaiModels = [
|
||||
"gpt-4o",
|
||||
"gpt-4o-2024-05-13",
|
||||
"gpt-4o-2024-08-06",
|
||||
"gpt-4o-2024-11-20",
|
||||
"chatgpt-4o-latest",
|
||||
"gpt-4o-mini",
|
||||
"gpt-4o-mini-2024-07-18",
|
||||
@@ -296,10 +348,23 @@ const openaiModels = [
|
||||
];
|
||||
|
||||
const googleModels = [
|
||||
"gemini-1.0-pro",
|
||||
"gemini-1.0-pro", // Deprecated on 2/15/2025
|
||||
"gemini-1.5-pro-latest",
|
||||
"gemini-1.5-pro",
|
||||
"gemini-1.5-pro-002",
|
||||
"gemini-1.5-pro-exp-0827",
|
||||
"gemini-1.5-flash-latest",
|
||||
"gemini-pro-vision",
|
||||
"gemini-1.5-flash-8b-latest",
|
||||
"gemini-1.5-flash",
|
||||
"gemini-1.5-flash-8b",
|
||||
"gemini-1.5-flash-002",
|
||||
"gemini-1.5-flash-exp-0827",
|
||||
"learnlm-1.5-pro-experimental",
|
||||
"gemini-exp-1114",
|
||||
"gemini-exp-1121",
|
||||
"gemini-exp-1206",
|
||||
"gemini-2.0-flash-exp",
|
||||
"gemini-2.0-flash-thinking-exp-1219",
|
||||
];
|
||||
|
||||
const anthropicModels = [
|
||||
@@ -308,8 +373,13 @@ const anthropicModels = [
|
||||
"claude-2.1",
|
||||
"claude-3-sonnet-20240229",
|
||||
"claude-3-opus-20240229",
|
||||
"claude-3-opus-latest",
|
||||
"claude-3-haiku-20240307",
|
||||
"claude-3-5-haiku-20241022",
|
||||
"claude-3-5-haiku-latest",
|
||||
"claude-3-5-sonnet-20240620",
|
||||
"claude-3-5-sonnet-20241022",
|
||||
"claude-3-5-sonnet-latest",
|
||||
];
|
||||
|
||||
const baiduModels = [
|
||||
@@ -365,6 +435,30 @@ const iflytekModels = [
|
||||
"4.0Ultra",
|
||||
];
|
||||
|
||||
const deepseekModels = ["deepseek-chat", "deepseek-coder"];
|
||||
|
||||
const xAIModes = ["grok-beta"];
|
||||
|
||||
const chatglmModels = [
|
||||
"glm-4-plus",
|
||||
"glm-4-0520",
|
||||
"glm-4",
|
||||
"glm-4-air",
|
||||
"glm-4-airx",
|
||||
"glm-4-long",
|
||||
"glm-4-flashx",
|
||||
"glm-4-flash",
|
||||
"glm-4v-plus",
|
||||
"glm-4v",
|
||||
"glm-4v-flash", // free
|
||||
"cogview-3-plus",
|
||||
"cogview-3",
|
||||
"cogview-3-flash", // free
|
||||
// 目前无法适配轮询任务
|
||||
// "cogvideox",
|
||||
// "cogvideox-flash", // free
|
||||
];
|
||||
|
||||
let seq = 1000; // 内置的模型序号生成器从1000开始
|
||||
export const DEFAULT_MODELS = [
|
||||
...openaiModels.map((name) => ({
|
||||
@@ -477,6 +571,39 @@ export const DEFAULT_MODELS = [
|
||||
sorted: 10,
|
||||
},
|
||||
})),
|
||||
...xAIModes.map((name) => ({
|
||||
name,
|
||||
available: true,
|
||||
sorted: seq++,
|
||||
provider: {
|
||||
id: "xai",
|
||||
providerName: "XAI",
|
||||
providerType: "xai",
|
||||
sorted: 11,
|
||||
},
|
||||
})),
|
||||
...chatglmModels.map((name) => ({
|
||||
name,
|
||||
available: true,
|
||||
sorted: seq++,
|
||||
provider: {
|
||||
id: "chatglm",
|
||||
providerName: "ChatGLM",
|
||||
providerType: "chatglm",
|
||||
sorted: 12,
|
||||
},
|
||||
})),
|
||||
...deepseekModels.map((name) => ({
|
||||
name,
|
||||
available: true,
|
||||
sorted: seq++,
|
||||
provider: {
|
||||
id: "deepseek",
|
||||
providerName: "DeepSeek",
|
||||
providerType: "deepseek",
|
||||
sorted: 13,
|
||||
},
|
||||
})),
|
||||
] as const;
|
||||
|
||||
export const CHAT_PAGE_SIZE = 15;
|
||||
@@ -496,8 +623,6 @@ export const internalAllowedWebDavEndpoints = [
|
||||
];
|
||||
|
||||
export const DEFAULT_GA_ID = "G-89WN60ZK2E";
|
||||
export const PLUGINS = [
|
||||
{ name: "Plugins", path: Path.Plugins },
|
||||
{ name: "Stable Diffusion", path: Path.Sd },
|
||||
{ name: "Search Chat", path: Path.SearchChat },
|
||||
];
|
||||
|
||||
export const SAAS_CHAT_URL = "https://nextchat.dev/chat";
|
||||
export const SAAS_CHAT_UTM_URL = "https://nextchat.dev/chat?utm=github";
|
||||
|
7
app/global.d.ts
vendored
@@ -26,6 +26,13 @@ declare interface Window {
|
||||
isPermissionGranted(): Promise<boolean>;
|
||||
sendNotification(options: string | Options): void;
|
||||
};
|
||||
updater: {
|
||||
checkUpdate(): Promise<UpdateResult>;
|
||||
installUpdate(): Promise<void>;
|
||||
onUpdaterEvent(
|
||||
handler: (status: UpdateStatusResult) => void,
|
||||
): Promise<UnlistenFn>;
|
||||
};
|
||||
http: {
|
||||
fetch<T>(
|
||||
url: string,
|
||||
|
1
app/icons/arrow.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg class="icon--SJP_d" width="16" height="16" fill="none" viewBox="0 0 16 16" style="min-width: 16px; min-height: 16px;"><g><path data-follow-fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" d="M5.248 14.444a.625.625 0 0 1-.005-.884l5.068-5.12a.625.625 0 0 0 0-.88L5.243 2.44a.625.625 0 1 1 .889-.88l5.067 5.121c.723.73.723 1.907 0 2.638l-5.067 5.12a.625.625 0 0 1-.884.005Z" fill="currentColor"></path></g></svg>
|
After Width: | Height: | Size: 426 B |
1
app/icons/fire.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M12.832 21.801c3.126-.626 7.168-2.875 7.168-8.69c0-5.291-3.873-8.815-6.658-10.434c-.619-.36-1.342.113-1.342.828v1.828c0 1.442-.606 4.074-2.29 5.169c-.86.559-1.79-.278-1.894-1.298l-.086-.838c-.1-.974-1.092-1.565-1.87-.971C4.461 8.46 3 10.33 3 13.11C3 20.221 8.289 22 10.933 22q.232 0 .484-.015C10.111 21.874 8 21.064 8 18.444c0-2.05 1.495-3.435 2.631-4.11c.306-.18.663.055.663.41v.59c0 .45.175 1.155.59 1.637c.47.546 1.159-.026 1.214-.744c.018-.226.246-.37.442-.256c.641.375 1.46 1.175 1.46 2.473c0 2.048-1.129 2.99-2.168 3.357"/></svg>
|
After Width: | Height: | Size: 648 B |
11
app/icons/headphone.svg
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="16" height="16" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4 28C4 26.8954 4.89543 26 6 26H10V38H6C4.89543 38 4 37.1046 4 36V28Z" fill="none" />
|
||||
<path d="M38 26H42C43.1046 26 44 26.8954 44 28V36C44 37.1046 43.1046 38 42 38H38V26Z"
|
||||
fill="none" />
|
||||
<path
|
||||
d="M10 36V24C10 16.268 16.268 10 24 10C31.732 10 38 16.268 38 24V36M10 26H6C4.89543 26 4 26.8954 4 28V36C4 37.1046 4.89543 38 6 38H10V26ZM38 26H42C43.1046 26 44 26.8954 44 28V36C44 37.1046 43.1046 38 42 38H38V26Z"
|
||||
stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M16 32H20L22 26L26 38L28 32H32" stroke="#333" stroke-width="4" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
After Width: | Height: | Size: 808 B |
19
app/icons/logo.svg
Normal file
@@ -0,0 +1,19 @@
|
||||
<svg width="38.73" height="42" viewBox="0 0 221 240" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="160.697" y="38.125" width="65.007" height="145.932" rx="32.503" transform="rotate(21.987 160.697 38.125)" fill="url(#logo_svg__a)"></rect>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="m48.642 79.125-25.92 71.213c-6.139 16.869 2.558 35.52 19.427 41.66 16.868 6.14 35.52-2.558 41.66-19.426L94.23 143.94l-36.658-37.439a32.42 32.42 0 0 1-9.244-23.497c.033-1.326.14-2.62.314-3.879Z" fill="url(#logo_svg__b)"></path>
|
||||
<path d="M172.578 132.787a32.765 32.765 0 0 1 8.981 24.238c-1.458 28.748-36.622 41.778-56.46 20.92l-67.644-71.122a32.763 32.763 0 0 1-8.981-24.238c1.457-28.748 36.622-41.778 56.46-20.92l67.644 71.122Z" fill="url(#logo_svg__c)" fill-opacity="0.96"></path>
|
||||
<defs>
|
||||
<linearGradient id="logo_svg__a" x1="215.063" y1="59.628" x2="160.714" y2="157.96" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#3EADFE"></stop>
|
||||
<stop offset="1" stop-color="#2A7AFF"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="logo_svg__b" x1="105.376" y1="84.416" x2="19.745" y2="131.163" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#01B3FF"></stop>
|
||||
<stop offset="1" stop-color="#59ECFA"></stop>
|
||||
</linearGradient>
|
||||
<linearGradient id="logo_svg__c" x1="102.734" y1="136.396" x2="192.577" y2="155.859" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#023BFF" stop-opacity="0.82"></stop>
|
||||
<stop offset="0.88" stop-color="#2D86FF" stop-opacity="0.76"></stop>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.5 KiB |
7
app/icons/power.svg
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M14.5 8C13.8406 8.37652 13.2062 8.79103 12.6 9.24051C11.5625 10.0097 10.6074 10.8814 9.75 11.8402C6.79377 15.1463 5 19.4891 5 24.2455C5 34.6033 13.5066 43 24 43C34.4934 43 43 34.6033 43 24.2455C43 19.4891 41.2062 15.1463 38.25 11.8402C37.3926 10.8814 36.4375 10.0097 35.4 9.24051C34.7938 8.79103 34.1594 8.37652 33.5 8"
|
||||
stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M24 4V24" stroke="#333" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
After Width: | Height: | Size: 675 B |
13
app/icons/voice-off.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M31 24V11C31 7.13401 27.866 4 24 4C20.134 4 17 7.13401 17 11V24C17 27.866 20.134 31 24 31C27.866 31 31 27.866 31 24Z"
|
||||
stroke="#d0021b" stroke-width="4" stroke-linejoin="round" />
|
||||
<path
|
||||
d="M9 23C9 31.2843 15.7157 38 24 38C25.7532 38 27.4361 37.6992 29 37.1465M39 23C39 25.1333 38.5547 27.1626 37.7519 29"
|
||||
stroke="#d0021b" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M24 38V44" stroke="#d0021b" stroke-width="4" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M42 42L6 6" stroke="#d0021b" stroke-width="4" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
After Width: | Height: | Size: 811 B |
9
app/icons/voice.svg
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="24" height="24" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="17" y="4" width="14" height="27" rx="7" fill="none" stroke="#333" stroke-width="4"
|
||||
stroke-linejoin="round" />
|
||||
<path d="M9 23C9 31.2843 15.7157 38 24 38C32.2843 38 39 31.2843 39 23" stroke="#333"
|
||||
stroke-width="4" stroke-linecap="round" stroke-linejoin="round" />
|
||||
<path d="M24 38V44" stroke="#333" stroke-width="4" stroke-linecap="round"
|
||||
stroke-linejoin="round" />
|
||||
</svg>
|
After Width: | Height: | Size: 549 B |
200
app/lib/audio.ts
Normal file
@@ -0,0 +1,200 @@
|
||||
export class AudioHandler {
|
||||
private context: AudioContext;
|
||||
private mergeNode: ChannelMergerNode;
|
||||
private analyserData: Uint8Array;
|
||||
public analyser: AnalyserNode;
|
||||
private workletNode: AudioWorkletNode | null = null;
|
||||
private stream: MediaStream | null = null;
|
||||
private source: MediaStreamAudioSourceNode | null = null;
|
||||
private recordBuffer: Int16Array[] = [];
|
||||
private readonly sampleRate = 24000;
|
||||
|
||||
private nextPlayTime: number = 0;
|
||||
private isPlaying: boolean = false;
|
||||
private playbackQueue: AudioBufferSourceNode[] = [];
|
||||
private playBuffer: Int16Array[] = [];
|
||||
|
||||
constructor() {
|
||||
this.context = new AudioContext({ sampleRate: this.sampleRate });
|
||||
// using ChannelMergerNode to get merged audio data, and then get analyser data.
|
||||
this.mergeNode = new ChannelMergerNode(this.context, { numberOfInputs: 2 });
|
||||
this.analyser = new AnalyserNode(this.context, { fftSize: 256 });
|
||||
this.analyserData = new Uint8Array(this.analyser.frequencyBinCount);
|
||||
this.mergeNode.connect(this.analyser);
|
||||
}
|
||||
|
||||
getByteFrequencyData() {
|
||||
this.analyser.getByteFrequencyData(this.analyserData);
|
||||
return this.analyserData;
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
await this.context.audioWorklet.addModule("/audio-processor.js");
|
||||
}
|
||||
|
||||
async startRecording(onChunk: (chunk: Uint8Array) => void) {
|
||||
try {
|
||||
if (!this.workletNode) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
this.stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
channelCount: 1,
|
||||
sampleRate: this.sampleRate,
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true,
|
||||
},
|
||||
});
|
||||
|
||||
await this.context.resume();
|
||||
this.source = this.context.createMediaStreamSource(this.stream);
|
||||
this.workletNode = new AudioWorkletNode(
|
||||
this.context,
|
||||
"audio-recorder-processor",
|
||||
);
|
||||
|
||||
this.workletNode.port.onmessage = (event) => {
|
||||
if (event.data.eventType === "audio") {
|
||||
const float32Data = event.data.audioData;
|
||||
const int16Data = new Int16Array(float32Data.length);
|
||||
|
||||
for (let i = 0; i < float32Data.length; i++) {
|
||||
const s = Math.max(-1, Math.min(1, float32Data[i]));
|
||||
int16Data[i] = s < 0 ? s * 0x8000 : s * 0x7fff;
|
||||
}
|
||||
|
||||
const uint8Data = new Uint8Array(int16Data.buffer);
|
||||
onChunk(uint8Data);
|
||||
// save recordBuffer
|
||||
// @ts-ignore
|
||||
this.recordBuffer.push.apply(this.recordBuffer, int16Data);
|
||||
}
|
||||
};
|
||||
|
||||
this.source.connect(this.workletNode);
|
||||
this.source.connect(this.mergeNode, 0, 0);
|
||||
this.workletNode.connect(this.context.destination);
|
||||
|
||||
this.workletNode.port.postMessage({ command: "START_RECORDING" });
|
||||
} catch (error) {
|
||||
console.error("Error starting recording:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
stopRecording() {
|
||||
if (!this.workletNode || !this.source || !this.stream) {
|
||||
throw new Error("Recording not started");
|
||||
}
|
||||
|
||||
this.workletNode.port.postMessage({ command: "STOP_RECORDING" });
|
||||
|
||||
this.workletNode.disconnect();
|
||||
this.source.disconnect();
|
||||
this.stream.getTracks().forEach((track) => track.stop());
|
||||
}
|
||||
startStreamingPlayback() {
|
||||
this.isPlaying = true;
|
||||
this.nextPlayTime = this.context.currentTime;
|
||||
}
|
||||
|
||||
stopStreamingPlayback() {
|
||||
this.isPlaying = false;
|
||||
this.playbackQueue.forEach((source) => source.stop());
|
||||
this.playbackQueue = [];
|
||||
this.playBuffer = [];
|
||||
}
|
||||
|
||||
playChunk(chunk: Uint8Array) {
|
||||
if (!this.isPlaying) return;
|
||||
|
||||
const int16Data = new Int16Array(chunk.buffer);
|
||||
// @ts-ignore
|
||||
this.playBuffer.push.apply(this.playBuffer, int16Data); // save playBuffer
|
||||
|
||||
const float32Data = new Float32Array(int16Data.length);
|
||||
for (let i = 0; i < int16Data.length; i++) {
|
||||
float32Data[i] = int16Data[i] / (int16Data[i] < 0 ? 0x8000 : 0x7fff);
|
||||
}
|
||||
|
||||
const audioBuffer = this.context.createBuffer(
|
||||
1,
|
||||
float32Data.length,
|
||||
this.sampleRate,
|
||||
);
|
||||
audioBuffer.getChannelData(0).set(float32Data);
|
||||
|
||||
const source = this.context.createBufferSource();
|
||||
source.buffer = audioBuffer;
|
||||
source.connect(this.context.destination);
|
||||
source.connect(this.mergeNode, 0, 1);
|
||||
|
||||
const chunkDuration = audioBuffer.length / this.sampleRate;
|
||||
|
||||
source.start(this.nextPlayTime);
|
||||
|
||||
this.playbackQueue.push(source);
|
||||
source.onended = () => {
|
||||
const index = this.playbackQueue.indexOf(source);
|
||||
if (index > -1) {
|
||||
this.playbackQueue.splice(index, 1);
|
||||
}
|
||||
};
|
||||
|
||||
this.nextPlayTime += chunkDuration;
|
||||
|
||||
if (this.nextPlayTime < this.context.currentTime) {
|
||||
this.nextPlayTime = this.context.currentTime;
|
||||
}
|
||||
}
|
||||
_saveData(data: Int16Array, bytesPerSample = 16): Blob {
|
||||
const headerLength = 44;
|
||||
const numberOfChannels = 1;
|
||||
const byteLength = data.buffer.byteLength;
|
||||
const header = new Uint8Array(headerLength);
|
||||
const view = new DataView(header.buffer);
|
||||
view.setUint32(0, 1380533830, false); // RIFF identifier 'RIFF'
|
||||
view.setUint32(4, 36 + byteLength, true); // file length minus RIFF identifier length and file description length
|
||||
view.setUint32(8, 1463899717, false); // RIFF type 'WAVE'
|
||||
view.setUint32(12, 1718449184, false); // format chunk identifier 'fmt '
|
||||
view.setUint32(16, 16, true); // format chunk length
|
||||
view.setUint16(20, 1, true); // sample format (raw)
|
||||
view.setUint16(22, numberOfChannels, true); // channel count
|
||||
view.setUint32(24, this.sampleRate, true); // sample rate
|
||||
view.setUint32(28, this.sampleRate * 4, true); // byte rate (sample rate * block align)
|
||||
view.setUint16(32, numberOfChannels * 2, true); // block align (channel count * bytes per sample)
|
||||
view.setUint16(34, bytesPerSample, true); // bits per sample
|
||||
view.setUint32(36, 1684108385, false); // data chunk identifier 'data'
|
||||
view.setUint32(40, byteLength, true); // data chunk length
|
||||
|
||||
// using data.buffer, so no need to setUint16 to view.
|
||||
return new Blob([view, data.buffer], { type: "audio/mpeg" });
|
||||
}
|
||||
savePlayFile() {
|
||||
// @ts-ignore
|
||||
return this._saveData(new Int16Array(this.playBuffer));
|
||||
}
|
||||
saveRecordFile(
|
||||
audioStartMillis: number | undefined,
|
||||
audioEndMillis: number | undefined,
|
||||
) {
|
||||
const startIndex = audioStartMillis
|
||||
? Math.floor((audioStartMillis * this.sampleRate) / 1000)
|
||||
: 0;
|
||||
const endIndex = audioEndMillis
|
||||
? Math.floor((audioEndMillis * this.sampleRate) / 1000)
|
||||
: this.recordBuffer.length;
|
||||
return this._saveData(
|
||||
// @ts-ignore
|
||||
new Int16Array(this.recordBuffer.slice(startIndex, endIndex)),
|
||||
);
|
||||
}
|
||||
async close() {
|
||||
this.recordBuffer = [];
|
||||
this.workletNode?.disconnect();
|
||||
this.source?.disconnect();
|
||||
this.stream?.getTracks().forEach((track) => track.stop());
|
||||
await this.context.close();
|
||||
}
|
||||
}
|
@@ -1,15 +1,21 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const ar: PartialLocaleType = {
|
||||
WIP: "قريبًا...",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "تم اكتشاف مفتاح API غير صالح، يرجى الذهاب إلى [الإعدادات](/#/settings) للتحقق من صحة مفتاح API."
|
||||
: "كلمة المرور غير صحيحة أو فارغة، يرجى الذهاب إلى [تسجيل الدخول](/#/auth) لإدخال كلمة مرور صحيحة، أو أدخل مفتاح OpenAI API الخاص بك في [الإعدادات](/#/settings).",
|
||||
? `😆 واجهت المحادثة بعض المشكلات، لا داعي للقلق:
|
||||
\\ 1️⃣ إذا كنت ترغب في تجربة دون إعداد، [انقر هنا لبدء المحادثة فورًا 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ إذا كنت تريد استخدام موارد OpenAI الخاصة بك، انقر [هنا](/#/settings) لتعديل الإعدادات ⚙️`
|
||||
: `😆 واجهت المحادثة بعض المشكلات، لا داعي للقلق:
|
||||
\ 1️⃣ إذا كنت ترغب في تجربة دون إعداد، [انقر هنا لبدء المحادثة فورًا 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ إذا كنت تستخدم إصدار النشر الخاص، انقر [هنا](/#/auth) لإدخال مفتاح الوصول 🔑
|
||||
\ 3️⃣ إذا كنت تريد استخدام موارد OpenAI الخاصة بك، انقر [هنا](/#/settings) لتعديل الإعدادات ⚙️
|
||||
`,
|
||||
},
|
||||
Auth: {
|
||||
Title: "تحتاج إلى كلمة مرور",
|
||||
@@ -18,6 +24,10 @@ const ar: PartialLocaleType = {
|
||||
Input: "أدخل رمز الوصول هنا",
|
||||
Confirm: "تأكيد",
|
||||
Later: "في وقت لاحق",
|
||||
Return: "عودة",
|
||||
SaasTips: "الإعدادات معقدة، أريد استخدامه على الفور",
|
||||
TopTips:
|
||||
"🥳 عرض NextChat AI الأول، افتح الآن OpenAI o1, GPT-4o, Claude-3.5 وأحدث النماذج الكبيرة",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} محادثة`,
|
||||
@@ -281,6 +291,13 @@ const ar: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Access: {
|
||||
SaasStart: {
|
||||
Title: "استخدام NextChat AI",
|
||||
Label: "(أفضل حل من حيث التكلفة)",
|
||||
SubTitle:
|
||||
"مدعوم رسميًا من NextChat، جاهز للاستخدام بدون إعداد، يدعم أحدث النماذج الكبيرة مثل OpenAI o1 و GPT-4o و Claude-3.5",
|
||||
ChatNow: "الدردشة الآن",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "كلمة المرور للوصول",
|
||||
SubTitle: "قام المشرف بتمكين الوصول المشفر",
|
||||
|
@@ -1,15 +1,21 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const bn: PartialLocaleType = {
|
||||
WIP: "শীঘ্রই আসছে...",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "অবৈধ API কী সনাক্ত করা হয়েছে, অনুগ্রহ করে [সেটিংস](/#/settings) পৃষ্ঠায় যান এবং নিশ্চিত করুন যে API কী সঠিকভাবে কনফিগার করা হয়েছে।"
|
||||
: "অ্যাক্সেস পাসওয়ার্ড সঠিক নয় বা খালি, অনুগ্রহ করে [লগইন](/#/auth) পৃষ্ঠায় যান এবং সঠিক অ্যাক্সেস পাসওয়ার্ড প্রবেশ করান, অথবা [সেটিংস](/#/settings) পৃষ্ঠায় আপনার OpenAI API কী প্রবেশ করান।",
|
||||
? `😆 কথোপকথনে কিছু সমস্যা হয়েছে, চিন্তার কিছু নেই:
|
||||
\\ 1️⃣ যদি আপনি শূন্য কনফিগারেশনে শুরু করতে চান, তাহলে [এখানে ক্লিক করে অবিলম্বে কথোপকথন শুরু করুন 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ যদি আপনি আপনার নিজস্ব OpenAI সম্পদ ব্যবহার করতে চান, তাহলে [এখানে ক্লিক করুন](/#/settings) সেটিংস পরিবর্তন করতে ⚙️`
|
||||
: `😆 কথোপকথনে কিছু সমস্যা হয়েছে, চিন্তার কিছু নেই:
|
||||
\ 1️⃣ যদি আপনি শূন্য কনফিগারেশনে শুরু করতে চান, তাহলে [এখানে ক্লিক করে অবিলম্বে কথোপকথন শুরু করুন 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ যদি আপনি একটি প্রাইভেট ডেপ্লয়মেন্ট সংস্করণ ব্যবহার করেন, তাহলে [এখানে ক্লিক করুন](/#/auth) প্রবেশাধিকার কীগুলি প্রবেশ করতে 🔑
|
||||
\ 3️⃣ যদি আপনি আপনার নিজস্ব OpenAI সম্পদ ব্যবহার করতে চান, তাহলে [এখানে ক্লিক করুন](/#/settings) সেটিংস পরিবর্তন করতে ⚙️
|
||||
`,
|
||||
},
|
||||
Auth: {
|
||||
Title: "পাসওয়ার্ড প্রয়োজন",
|
||||
@@ -18,6 +24,10 @@ const bn: PartialLocaleType = {
|
||||
Input: "এখানে অ্যাক্সেস কোড লিখুন",
|
||||
Confirm: "নিশ্চিত করুন",
|
||||
Later: "পরে বলুন",
|
||||
Return: "ফিরে আসা",
|
||||
SaasTips: "কনফিগারেশন খুব কঠিন, আমি অবিলম্বে ব্যবহার করতে চাই",
|
||||
TopTips:
|
||||
"🥳 NextChat AI প্রথম প্রকাশের অফার, এখনই OpenAI o1, GPT-4o, Claude-3.5 এবং সর্বশেষ বড় মডেলগুলি আনলক করুন",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} টি চ্যাট`,
|
||||
@@ -284,6 +294,14 @@ const bn: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Access: {
|
||||
SaasStart: {
|
||||
Title: "NextChat AI ব্যবহার করুন",
|
||||
Label: "(সেরা মূল্যসাশ্রয়ী সমাধান)",
|
||||
SubTitle:
|
||||
"NextChat কর্তৃক অফিসিয়াল রক্ষণাবেক্ষণ, শূন্য কনফিগারেশন ব্যবহার শুরু করুন, OpenAI o1, GPT-4o, Claude-3.5 সহ সর্বশেষ বড় মডেলগুলি সমর্থন করে",
|
||||
ChatNow: "এখনই চ্যাট করুন",
|
||||
},
|
||||
|
||||
AccessCode: {
|
||||
Title: "অ্যাক্সেস পাসওয়ার্ড",
|
||||
SubTitle: "অ্যাডমিন এনক্রিপ্টেড অ্যাক্সেস সক্রিয় করেছেন",
|
||||
|
@@ -1,5 +1,6 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SubmitKey } from "../store/config";
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
@@ -7,16 +8,26 @@ const cn = {
|
||||
WIP: "该功能仍在开发中……",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "检测到无效 API Key,请前往[设置](/#/settings)页检查 API Key 是否配置正确。"
|
||||
: "访问密码不正确或为空,请前往[登录](/#/auth)页输入正确的访问密码,或者在[设置](/#/settings)页填入你自己的 OpenAI API Key。",
|
||||
? `😆 对话遇到了一些问题,不用慌:
|
||||
\\ 1️⃣ 想要零配置开箱即用,[点击这里立刻开启对话 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ 如果你想消耗自己的 OpenAI 资源,点击[这里](/#/settings)修改设置 ⚙️`
|
||||
: `😆 对话遇到了一些问题,不用慌:
|
||||
\ 1️⃣ 想要零配置开箱即用,[点击这里立刻开启对话 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ 如果你正在使用私有部署版本,点击[这里](/#/auth)输入访问秘钥 🔑
|
||||
\ 3️⃣ 如果你想消耗自己的 OpenAI 资源,点击[这里](/#/settings)修改设置 ⚙️
|
||||
`,
|
||||
},
|
||||
Auth: {
|
||||
Return: "返回",
|
||||
Title: "需要密码",
|
||||
Tips: "管理员开启了密码验证,请在下方填入访问码",
|
||||
SubTips: "或者输入你的 OpenAI 或 Google API 密钥",
|
||||
SubTips: "或者输入你的 OpenAI 或 Google AI 密钥",
|
||||
Input: "在此处填写访问码",
|
||||
Confirm: "确认",
|
||||
Later: "稍后再说",
|
||||
SaasTips: "配置太麻烦,想要立即使用",
|
||||
TopTips:
|
||||
"🥳 NextChat AI 首发优惠,立刻解锁 OpenAI o1, GPT-4o, Claude-3.5 等最新大模型",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} 条对话`,
|
||||
@@ -166,7 +177,7 @@ const cn = {
|
||||
},
|
||||
},
|
||||
Lang: {
|
||||
Name: "Language", // ATTENTION: if you wanna add a new translation, please do not translate this value, leave it as `Language`
|
||||
Name: "Language", // 注意:如果要添加新的翻译,请不要翻译此值,将它保留为 `Language`
|
||||
All: "所有语言",
|
||||
},
|
||||
Avatar: "头像",
|
||||
@@ -195,6 +206,8 @@ const cn = {
|
||||
IsChecking: "正在检查更新...",
|
||||
FoundUpdate: (x: string) => `发现新版本:${x}`,
|
||||
GoToUpdate: "前往更新",
|
||||
Success: "更新成功!",
|
||||
Failed: "更新失败",
|
||||
},
|
||||
SendKey: "发送键",
|
||||
Theme: "主题",
|
||||
@@ -298,6 +311,13 @@ const cn = {
|
||||
},
|
||||
|
||||
Access: {
|
||||
SaasStart: {
|
||||
Title: "使用 NextChat AI",
|
||||
Label: "(性价比最高的方案)",
|
||||
SubTitle:
|
||||
"由 NextChat 官方维护, 零配置开箱即用,支持 OpenAI o1, GPT-4o, Claude-3.5 等最新大模型",
|
||||
ChatNow: "立刻对话",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "访问密码",
|
||||
SubTitle: "管理员已开启加密访问",
|
||||
@@ -361,7 +381,7 @@ const cn = {
|
||||
ApiKey: {
|
||||
Title: "API 密钥",
|
||||
SubTitle: "从 Google AI 获取您的 API 密钥",
|
||||
Placeholder: "输入您的 Google AI Studio API 密钥",
|
||||
Placeholder: "Google AI API KEY",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
@@ -443,6 +463,39 @@ const cn = {
|
||||
SubTitle: "样例:",
|
||||
},
|
||||
},
|
||||
DeepSeek: {
|
||||
ApiKey: {
|
||||
Title: "接口密钥",
|
||||
SubTitle: "使用自定义DeepSeek API Key",
|
||||
Placeholder: "DeepSeek API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "样例:",
|
||||
},
|
||||
},
|
||||
XAI: {
|
||||
ApiKey: {
|
||||
Title: "接口密钥",
|
||||
SubTitle: "使用自定义XAI API Key",
|
||||
Placeholder: "XAI API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "样例:",
|
||||
},
|
||||
},
|
||||
ChatGLM: {
|
||||
ApiKey: {
|
||||
Title: "接口密钥",
|
||||
SubTitle: "使用自定义 ChatGLM API Key",
|
||||
Placeholder: "ChatGLM API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "样例:",
|
||||
},
|
||||
},
|
||||
Stability: {
|
||||
ApiKey: {
|
||||
Title: "接口密钥",
|
||||
@@ -478,8 +531,8 @@ const cn = {
|
||||
|
||||
Model: "模型 (model)",
|
||||
CompressModel: {
|
||||
Title: "压缩模型",
|
||||
SubTitle: "用于压缩历史记录的模型",
|
||||
Title: "对话摘要模型",
|
||||
SubTitle: "用于压缩历史记录、生成对话标题的模型",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "随机性 (temperature)",
|
||||
@@ -521,6 +574,39 @@ const cn = {
|
||||
SubTitle: "生成语音的速度",
|
||||
},
|
||||
},
|
||||
Realtime: {
|
||||
Enable: {
|
||||
Title: "实时聊天",
|
||||
SubTitle: "开启实时聊天功能",
|
||||
},
|
||||
Provider: {
|
||||
Title: "模型服务商",
|
||||
SubTitle: "切换不同的服务商",
|
||||
},
|
||||
Model: {
|
||||
Title: "模型",
|
||||
SubTitle: "选择一个模型",
|
||||
},
|
||||
ApiKey: {
|
||||
Title: "API Key",
|
||||
SubTitle: "API Key",
|
||||
Placeholder: "API Key",
|
||||
},
|
||||
Azure: {
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "接口地址",
|
||||
},
|
||||
Deployment: {
|
||||
Title: "部署名称",
|
||||
SubTitle: "部署名称",
|
||||
},
|
||||
},
|
||||
Temperature: {
|
||||
Title: "随机性 (temperature)",
|
||||
SubTitle: "值越大,回复越随机",
|
||||
},
|
||||
},
|
||||
},
|
||||
Store: {
|
||||
DefaultTopic: "新的聊天",
|
||||
@@ -556,7 +642,7 @@ const cn = {
|
||||
Sysmessage: "你是一个助手",
|
||||
},
|
||||
SearchChat: {
|
||||
Name: "搜索",
|
||||
Name: "搜索聊天记录",
|
||||
Page: {
|
||||
Title: "搜索聊天记录",
|
||||
Search: "输入搜索关键词",
|
||||
@@ -648,6 +734,10 @@ const cn = {
|
||||
Title: "启用Artifacts",
|
||||
SubTitle: "启用之后可以直接渲染HTML页面",
|
||||
},
|
||||
CodeFold: {
|
||||
Title: "启用代码折叠",
|
||||
SubTitle: "启用之后可以自动折叠/展开过长的代码块",
|
||||
},
|
||||
Share: {
|
||||
Title: "分享此面具",
|
||||
SubTitle: "生成此面具的直达链接",
|
||||
|
@@ -1,15 +1,21 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const cs: PartialLocaleType = {
|
||||
WIP: "V přípravě...",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "Byl zjištěn neplatný API Key, prosím přejděte na stránku [Nastavení](/#/settings) a zkontrolujte, zda je API Key správně nakonfigurován."
|
||||
: "Heslo je nesprávné nebo prázdné, prosím přejděte na stránku [Přihlášení](/#/auth) a zadejte správné heslo, nebo na stránku [Nastavení](/#/settings) a zadejte svůj vlastní OpenAI API Key.",
|
||||
? `😆 Rozhovor narazil na nějaké problémy, nebojte se:
|
||||
\\ 1️⃣ Pokud chcete začít bez konfigurace, [klikněte sem pro okamžitý začátek chatu 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Pokud chcete využít své vlastní zdroje OpenAI, klikněte [sem](/#/settings) a upravte nastavení ⚙️`
|
||||
: `😆 Rozhovor narazil na nějaké problémy, nebojte se:
|
||||
\ 1️⃣ Pokud chcete začít bez konfigurace, [klikněte sem pro okamžitý začátek chatu 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Pokud používáte verzi soukromého nasazení, klikněte [sem](/#/auth) a zadejte přístupový klíč 🔑
|
||||
\ 3️⃣ Pokud chcete využít své vlastní zdroje OpenAI, klikněte [sem](/#/settings) a upravte nastavení ⚙️
|
||||
`,
|
||||
},
|
||||
Auth: {
|
||||
Title: "Potřebné heslo",
|
||||
@@ -18,6 +24,10 @@ const cs: PartialLocaleType = {
|
||||
Input: "Zadejte přístupový kód zde",
|
||||
Confirm: "Potvrdit",
|
||||
Later: "Později",
|
||||
Return: "Návrat",
|
||||
SaasTips: "Konfigurace je příliš složitá, chci okamžitě začít používat",
|
||||
TopTips:
|
||||
"🥳 Uvítací nabídka NextChat AI, okamžitě odemkněte OpenAI o1, GPT-4o, Claude-3.5 a nejnovější velké modely",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} konverzací`,
|
||||
@@ -284,6 +294,14 @@ const cs: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Access: {
|
||||
SaasStart: {
|
||||
Title: "Použití NextChat AI",
|
||||
Label: "(Nejlepší nákladově efektivní řešení)",
|
||||
SubTitle:
|
||||
"Oficiálně udržováno NextChat, připraveno k použití bez konfigurace, podporuje nejnovější velké modely jako OpenAI o1, GPT-4o, Claude-3.5",
|
||||
ChatNow: "Začněte chatovat nyní",
|
||||
},
|
||||
|
||||
AccessCode: {
|
||||
Title: "Přístupový kód",
|
||||
SubTitle: "Administrátor aktivoval šifrovaný přístup",
|
||||
|
@@ -1,15 +1,21 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const de: PartialLocaleType = {
|
||||
WIP: "In Bearbeitung...",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "Ungültiger API-Schlüssel erkannt. Bitte gehen Sie zur [Einstellungen](/#/settings) Seite, um zu überprüfen, ob der API-Schlüssel korrekt konfiguriert ist."
|
||||
: "Das Passwort ist falsch oder leer. Bitte gehen Sie zur [Login](/#/auth) Seite, um das richtige Passwort einzugeben, oder fügen Sie Ihren OpenAI API-Schlüssel auf der [Einstellungen](/#/settings) Seite hinzu.",
|
||||
? `😆 Das Gespräch hatte einige Probleme, keine Sorge:
|
||||
\\ 1️⃣ Wenn du ohne Konfiguration sofort starten möchtest, [klicke hier, um sofort zu chatten 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Wenn du deine eigenen OpenAI-Ressourcen verwenden möchtest, klicke [hier](/#/settings), um die Einstellungen zu ändern ⚙️`
|
||||
: `😆 Das Gespräch hatte einige Probleme, keine Sorge:
|
||||
\ 1️⃣ Wenn du ohne Konfiguration sofort starten möchtest, [klicke hier, um sofort zu chatten 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Wenn du eine private Bereitstellung verwendest, klicke [hier](/#/auth), um den Zugriffsschlüssel einzugeben 🔑
|
||||
\ 3️⃣ Wenn du deine eigenen OpenAI-Ressourcen verwenden möchtest, klicke [hier](/#/settings), um die Einstellungen zu ändern ⚙️
|
||||
`,
|
||||
},
|
||||
Auth: {
|
||||
Title: "Passwort erforderlich",
|
||||
@@ -18,6 +24,11 @@ const de: PartialLocaleType = {
|
||||
Input: "Geben Sie hier den Zugangscode ein",
|
||||
Confirm: "Bestätigen",
|
||||
Later: "Später",
|
||||
Return: "Zurück",
|
||||
SaasTips:
|
||||
"Die Konfiguration ist zu kompliziert, ich möchte es sofort nutzen",
|
||||
TopTips:
|
||||
"🥳 NextChat AI Einführungsangebot, schalte jetzt OpenAI o1, GPT-4o, Claude-3.5 und die neuesten großen Modelle frei",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} Gespräche`,
|
||||
@@ -291,6 +302,14 @@ const de: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Access: {
|
||||
SaasStart: {
|
||||
Title: "NextChat AI verwenden",
|
||||
Label: "(Die kosteneffektivste Lösung)",
|
||||
SubTitle:
|
||||
"Offiziell von NextChat verwaltet, sofort einsatzbereit ohne Konfiguration, unterstützt die neuesten großen Modelle wie OpenAI o1, GPT-4o und Claude-3.5",
|
||||
ChatNow: "Jetzt chatten",
|
||||
},
|
||||
|
||||
AccessCode: {
|
||||
Title: "Zugangscode",
|
||||
SubTitle:
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SubmitKey } from "../store/config";
|
||||
import { LocaleType } from "./index";
|
||||
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
// if you are adding a new translation, please use PartialLocaleType instead of LocaleType
|
||||
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
@@ -9,16 +9,26 @@ const en: LocaleType = {
|
||||
WIP: "Coming Soon...",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "Invalid API Key, please check it in [Settings](/#/settings) page."
|
||||
: "Unauthorized access, please enter access code in [auth](/#/auth) page, or enter your OpenAI API Key.",
|
||||
? `😆 Oops, there's an issue. No worries:
|
||||
\\ 1️⃣ New here? [Click to start chatting now 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Want to use your own OpenAI resources? [Click here](/#/settings) to change settings ⚙️`
|
||||
: `😆 Oops, there's an issue. Let's fix it:
|
||||
\ 1️⃣ New here? [Click to start chatting now 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Using a private setup? [Click here](/#/auth) to enter your key 🔑
|
||||
\ 3️⃣ Want to use your own OpenAI resources? [Click here](/#/settings) to change settings ⚙️
|
||||
`,
|
||||
},
|
||||
Auth: {
|
||||
Return: "Return",
|
||||
Title: "Need Access Code",
|
||||
Tips: "Please enter access code below",
|
||||
SubTips: "Or enter your OpenAI or Google API Key",
|
||||
Input: "access code",
|
||||
Confirm: "Confirm",
|
||||
Later: "Later",
|
||||
SaasTips: "Too Complex, Use Immediately Now",
|
||||
TopTips:
|
||||
"🥳 NextChat AI launch promotion: Instantly unlock the latest models like OpenAI o1, GPT-4o, Claude-3.5!",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} messages`,
|
||||
@@ -198,6 +208,8 @@ const en: LocaleType = {
|
||||
IsChecking: "Checking update...",
|
||||
FoundUpdate: (x: string) => `Found new version: ${x}`,
|
||||
GoToUpdate: "Update",
|
||||
Success: "Update Successful.",
|
||||
Failed: "Update Failed.",
|
||||
},
|
||||
SendKey: "Send Key",
|
||||
Theme: "Theme",
|
||||
@@ -302,6 +314,14 @@ const en: LocaleType = {
|
||||
NoAccess: "Enter API Key to check balance",
|
||||
},
|
||||
Access: {
|
||||
SaasStart: {
|
||||
Title: "Use NextChat AI",
|
||||
Label: " (Most Cost-Effective Option)",
|
||||
SubTitle:
|
||||
"Maintained by NextChat, zero setup needed, unlock OpenAI o1, GPT-4o," +
|
||||
" Claude-3.5 and more",
|
||||
ChatNow: "Start Now",
|
||||
},
|
||||
AccessCode: {
|
||||
Title: "Access Code",
|
||||
SubTitle: "Access control Enabled",
|
||||
@@ -427,6 +447,39 @@ const en: LocaleType = {
|
||||
SubTitle: "Example: ",
|
||||
},
|
||||
},
|
||||
DeepSeek: {
|
||||
ApiKey: {
|
||||
Title: "DeepSeek API Key",
|
||||
SubTitle: "Use a custom DeepSeek API Key",
|
||||
Placeholder: "DeepSeek API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Endpoint Address",
|
||||
SubTitle: "Example: ",
|
||||
},
|
||||
},
|
||||
XAI: {
|
||||
ApiKey: {
|
||||
Title: "XAI API Key",
|
||||
SubTitle: "Use a custom XAI API Key",
|
||||
Placeholder: "XAI API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Endpoint Address",
|
||||
SubTitle: "Example: ",
|
||||
},
|
||||
},
|
||||
ChatGLM: {
|
||||
ApiKey: {
|
||||
Title: "ChatGLM API Key",
|
||||
SubTitle: "Use a custom ChatGLM API Key",
|
||||
Placeholder: "ChatGLM API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Endpoint Address",
|
||||
SubTitle: "Example: ",
|
||||
},
|
||||
},
|
||||
Stability: {
|
||||
ApiKey: {
|
||||
Title: "Stability API Key",
|
||||
@@ -462,7 +515,7 @@ const en: LocaleType = {
|
||||
ApiKey: {
|
||||
Title: "API Key",
|
||||
SubTitle: "Obtain your API Key from Google AI",
|
||||
Placeholder: "Enter your Google AI Studio API Key",
|
||||
Placeholder: "Google AI API Key",
|
||||
},
|
||||
|
||||
Endpoint: {
|
||||
@@ -483,8 +536,8 @@ const en: LocaleType = {
|
||||
|
||||
Model: "Model",
|
||||
CompressModel: {
|
||||
Title: "Compression Model",
|
||||
SubTitle: "Model used to compress history",
|
||||
Title: "Summary Model",
|
||||
SubTitle: "Model used to compress history and generate title",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "Temperature",
|
||||
@@ -529,6 +582,39 @@ const en: LocaleType = {
|
||||
},
|
||||
Engine: "TTS Engine",
|
||||
},
|
||||
Realtime: {
|
||||
Enable: {
|
||||
Title: "Realtime Chat",
|
||||
SubTitle: "Enable realtime chat feature",
|
||||
},
|
||||
Provider: {
|
||||
Title: "Model Provider",
|
||||
SubTitle: "Switch between different providers",
|
||||
},
|
||||
Model: {
|
||||
Title: "Model",
|
||||
SubTitle: "Select a model",
|
||||
},
|
||||
ApiKey: {
|
||||
Title: "API Key",
|
||||
SubTitle: "API Key",
|
||||
Placeholder: "API Key",
|
||||
},
|
||||
Azure: {
|
||||
Endpoint: {
|
||||
Title: "Endpoint",
|
||||
SubTitle: "Endpoint",
|
||||
},
|
||||
Deployment: {
|
||||
Title: "Deployment Name",
|
||||
SubTitle: "Deployment Name",
|
||||
},
|
||||
},
|
||||
Temperature: {
|
||||
Title: "Randomness (temperature)",
|
||||
SubTitle: "Higher values result in more random responses",
|
||||
},
|
||||
},
|
||||
},
|
||||
Store: {
|
||||
DefaultTopic: "New Conversation",
|
||||
@@ -658,6 +744,11 @@ const en: LocaleType = {
|
||||
Title: "Enable Artifacts",
|
||||
SubTitle: "Can render HTML page when enable artifacts.",
|
||||
},
|
||||
CodeFold: {
|
||||
Title: "Enable CodeFold",
|
||||
SubTitle:
|
||||
"Automatically collapse/expand overly long code blocks when CodeFold is enabled",
|
||||
},
|
||||
Share: {
|
||||
Title: "Share This Mask",
|
||||
SubTitle: "Generate a link to this mask",
|
||||
|
@@ -1,15 +1,21 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const es: PartialLocaleType = {
|
||||
WIP: "En construcción...",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "Se detectó una clave API inválida. Por favor, ve a la página de [Configuración](/#/settings) para verificar si la clave API está configurada correctamente."
|
||||
: "La contraseña de acceso es incorrecta o está vacía. Por favor, ve a la página de [Iniciar sesión](/#/auth) para ingresar la contraseña correcta, o en la página de [Configuración](/#/settings) para introducir tu propia clave API de OpenAI.",
|
||||
? `😆 La conversación encontró algunos problemas, no te preocupes:
|
||||
\\ 1️⃣ Si deseas comenzar sin configuración, [haz clic aquí para empezar a chatear inmediatamente 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Si deseas usar tus propios recursos de OpenAI, haz clic [aquí](/#/settings) para modificar la configuración ⚙️`
|
||||
: `😆 La conversación encontró algunos problemas, no te preocupes:
|
||||
\ 1️⃣ Si deseas comenzar sin configuración, [haz clic aquí para empezar a chatear inmediatamente 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Si estás utilizando una versión de implementación privada, haz clic [aquí](/#/auth) para ingresar la clave de acceso 🔑
|
||||
\ 3️⃣ Si deseas usar tus propios recursos de OpenAI, haz clic [aquí](/#/settings) para modificar la configuración ⚙️
|
||||
`,
|
||||
},
|
||||
Auth: {
|
||||
Title: "Se requiere contraseña",
|
||||
@@ -18,6 +24,11 @@ const es: PartialLocaleType = {
|
||||
Input: "Introduce el código de acceso aquí",
|
||||
Confirm: "Confirmar",
|
||||
Later: "Más tarde",
|
||||
Return: "Regresar",
|
||||
SaasTips:
|
||||
"La configuración es demasiado complicada, quiero usarlo de inmediato",
|
||||
TopTips:
|
||||
"🥳 Oferta de lanzamiento de NextChat AI, desbloquea OpenAI o1, GPT-4o, Claude-3.5 y los últimos grandes modelos",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} conversaciones`,
|
||||
@@ -294,6 +305,14 @@ const es: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Access: {
|
||||
SaasStart: {
|
||||
Title: "Use NextChat AI",
|
||||
Label: "(The most cost-effective solution)",
|
||||
SubTitle:
|
||||
"Officially maintained by NextChat, zero configuration ready to use, supports the latest large models like OpenAI o1, GPT-4o, and Claude-3.5",
|
||||
ChatNow: "Chat Now",
|
||||
},
|
||||
|
||||
AccessCode: {
|
||||
Title: "Contraseña de acceso",
|
||||
SubTitle: "El administrador ha habilitado el acceso encriptado",
|
||||
|
@@ -1,15 +1,21 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const fr: PartialLocaleType = {
|
||||
WIP: "Prochainement...",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "Clé API invalide détectée. Veuillez vérifier si la clé API est correctement configurée dans la page [Paramètres](/#/settings)."
|
||||
: "Le mot de passe d'accès est incorrect ou manquant. Veuillez entrer le mot de passe d'accès correct sur la page [Connexion](/#/auth) ou entrer votre propre clé API OpenAI sur la page [Paramètres](/#/settings).",
|
||||
? `😆 La conversation a rencontré quelques problèmes, pas de panique :
|
||||
\\ 1️⃣ Si vous souhaitez commencer sans configuration, [cliquez ici pour démarrer la conversation immédiatement 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Si vous souhaitez utiliser vos propres ressources OpenAI, cliquez [ici](/#/settings) pour modifier les paramètres ⚙️`
|
||||
: `😆 La conversation a rencontré quelques problèmes, pas de panique :
|
||||
\ 1️⃣ Si vous souhaitez commencer sans configuration, [cliquez ici pour démarrer la conversation immédiatement 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Si vous utilisez une version déployée privée, cliquez [ici](/#/auth) pour entrer la clé d'accès 🔑
|
||||
\ 3️⃣ Si vous souhaitez utiliser vos propres ressources OpenAI, cliquez [ici](/#/settings) pour modifier les paramètres ⚙️
|
||||
`,
|
||||
},
|
||||
Auth: {
|
||||
Title: "Mot de passe requis",
|
||||
@@ -18,6 +24,11 @@ const fr: PartialLocaleType = {
|
||||
Input: "Entrez le code d'accès ici",
|
||||
Confirm: "Confirmer",
|
||||
Later: "Plus tard",
|
||||
Return: "Retour",
|
||||
SaasTips:
|
||||
"La configuration est trop compliquée, je veux l'utiliser immédiatement",
|
||||
TopTips:
|
||||
"🥳 Offre de lancement NextChat AI, débloquez OpenAI o1, GPT-4o, Claude-3.5 et les derniers grands modèles",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} conversations`,
|
||||
@@ -294,6 +305,14 @@ const fr: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Access: {
|
||||
SaasStart: {
|
||||
Title: "Utiliser NextChat AI",
|
||||
Label: "(La solution la plus rentable)",
|
||||
SubTitle:
|
||||
"Officiellement maintenu par NextChat, prêt à l'emploi sans configuration, prend en charge les derniers grands modèles comme OpenAI o1, GPT-4o et Claude-3.5",
|
||||
ChatNow: "Discuter maintenant",
|
||||
},
|
||||
|
||||
AccessCode: {
|
||||
Title: "Mot de passe d'accès",
|
||||
SubTitle: "L'administrateur a activé l'accès sécurisé",
|
||||
|
@@ -1,15 +1,21 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const id: PartialLocaleType = {
|
||||
WIP: "Coming Soon...",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "API Key tidak valid terdeteksi, silakan periksa apakah API Key telah dikonfigurasi dengan benar di halaman [Pengaturan](/#/settings)."
|
||||
: "Kata sandi akses tidak benar atau kosong, silakan masukkan kata sandi akses yang benar di halaman [Masuk](/#/auth), atau masukkan OpenAI API Key Anda di halaman [Pengaturan](/#/settings).",
|
||||
? `😆 Percakapan mengalami beberapa masalah, tidak perlu khawatir:
|
||||
\\ 1️⃣ Jika Anda ingin memulai tanpa konfigurasi, [klik di sini untuk mulai mengobrol segera 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Jika Anda ingin menggunakan sumber daya OpenAI Anda sendiri, klik [di sini](/#/settings) untuk mengubah pengaturan ⚙️`
|
||||
: `😆 Percakapan mengalami beberapa masalah, tidak perlu khawatir:
|
||||
\ 1️⃣ Jika Anda ingin memulai tanpa konfigurasi, [klik di sini untuk mulai mengobrol segera 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Jika Anda menggunakan versi penyebaran pribadi, klik [di sini](/#/auth) untuk memasukkan kunci akses 🔑
|
||||
\ 3️⃣ Jika Anda ingin menggunakan sumber daya OpenAI Anda sendiri, klik [di sini](/#/settings) untuk mengubah pengaturan ⚙️
|
||||
`,
|
||||
},
|
||||
Auth: {
|
||||
Title: "Kebutuhan Kata Sandi",
|
||||
@@ -18,6 +24,10 @@ const id: PartialLocaleType = {
|
||||
Input: "Masukkan kode akses di sini",
|
||||
Confirm: "Konfirmasi",
|
||||
Later: "Nanti",
|
||||
Return: "Kembali",
|
||||
SaasTips: "Konfigurasi terlalu rumit, saya ingin menggunakannya segera",
|
||||
TopTips:
|
||||
"🥳 Penawaran Peluncuran NextChat AI, buka OpenAI o1, GPT-4o, Claude-3.5 dan model besar terbaru sekarang",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} percakapan`,
|
||||
@@ -285,6 +295,14 @@ const id: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Access: {
|
||||
SaasStart: {
|
||||
Title: "Gunakan NextChat AI",
|
||||
Label: "(Solusi paling hemat biaya)",
|
||||
SubTitle:
|
||||
"Dikelola secara resmi oleh NextChat, siap digunakan tanpa konfigurasi, mendukung model besar terbaru seperti OpenAI o1, GPT-4o, dan Claude-3.5",
|
||||
ChatNow: "Chat Sekarang",
|
||||
},
|
||||
|
||||
AccessCode: {
|
||||
Title: "Kata Sandi Akses",
|
||||
SubTitle: "Administrator telah mengaktifkan akses terenkripsi",
|
||||
|
@@ -1,15 +1,21 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const it: PartialLocaleType = {
|
||||
WIP: "Work in progress...",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "API Key non valido rilevato. Vai alla pagina [Impostazioni](/#/settings) per controllare se l'API Key è configurata correttamente."
|
||||
: "La password di accesso è errata o mancante. Vai alla pagina [Login](/#/auth) per inserire la password corretta o inserisci la tua API Key OpenAI nella pagina [Impostazioni](/#/settings).",
|
||||
? `😆 La conversazione ha incontrato alcuni problemi, non preoccuparti:
|
||||
\\ 1️⃣ Se vuoi iniziare senza configurazione, [clicca qui per iniziare a chattare immediatamente 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Se vuoi utilizzare le tue risorse OpenAI, clicca [qui](/#/settings) per modificare le impostazioni ⚙️`
|
||||
: `😆 La conversazione ha incontrato alcuni problemi, non preoccuparti:
|
||||
\ 1️⃣ Se vuoi iniziare senza configurazione, [clicca qui per iniziare a chattare immediatamente 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Se stai utilizzando una versione di distribuzione privata, clicca [qui](/#/auth) per inserire la chiave di accesso 🔑
|
||||
\ 3️⃣ Se vuoi utilizzare le tue risorse OpenAI, clicca [qui](/#/settings) per modificare le impostazioni ⚙️
|
||||
`,
|
||||
},
|
||||
Auth: {
|
||||
Title: "Password richiesta",
|
||||
@@ -18,6 +24,11 @@ const it: PartialLocaleType = {
|
||||
Input: "Inserisci il codice di accesso qui",
|
||||
Confirm: "Conferma",
|
||||
Later: "Più tardi",
|
||||
Return: "Ritorna",
|
||||
SaasTips:
|
||||
"La configurazione è troppo complicata, voglio usarlo immediatamente",
|
||||
TopTips:
|
||||
"🥳 Offerta di lancio NextChat AI, sblocca OpenAI o1, GPT-4o, Claude-3.5 e i più recenti modelli di grandi dimensioni",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} conversazioni`,
|
||||
@@ -295,6 +306,14 @@ const it: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Access: {
|
||||
SaasStart: {
|
||||
Title: "Usa NextChat AI",
|
||||
Label: "(La soluzione più conveniente)",
|
||||
SubTitle:
|
||||
"Mantenuto ufficialmente da NextChat, pronto all'uso senza configurazione, supporta i modelli più recenti come OpenAI o1, GPT-4o e Claude-3.5",
|
||||
ChatNow: "Chatta ora",
|
||||
},
|
||||
|
||||
AccessCode: {
|
||||
Title: "Password di accesso",
|
||||
SubTitle: "L'amministratore ha abilitato l'accesso criptato",
|
||||
|
@@ -1,15 +1,21 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const jp: PartialLocaleType = {
|
||||
WIP: "この機能は開発中です",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "無効なAPIキーが検出されました。[設定](/#/settings)ページでAPIキーが正しく設定されているか確認してください。"
|
||||
: "アクセスパスワードが正しくないか空です。[ログイン](/#/auth)ページで正しいアクセスパスワードを入力するか、[設定](/#/settings)ページで自分のOpenAI APIキーを入力してください。",
|
||||
? `😆 会話中に問題が発生しましたが、心配しないでください:
|
||||
\\ 1️⃣ 設定なしで始めたい場合は、[ここをクリックしてすぐにチャットを開始 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ 自分のOpenAIリソースを使用したい場合は、[ここをクリックして](/#/settings)設定を変更してください ⚙️`
|
||||
: `😆 会話中に問題が発生しましたが、心配しないでください:
|
||||
\ 1️⃣ 設定なしで始めたい場合は、[ここをクリックしてすぐにチャットを開始 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ プライベートデプロイ版を使用している場合は、[ここをクリックして](/#/auth)アクセストークンを入力してください 🔑
|
||||
\ 3️⃣ 自分のOpenAIリソースを使用したい場合は、[ここをクリックして](/#/settings)設定を変更してください ⚙️
|
||||
`,
|
||||
},
|
||||
Auth: {
|
||||
Title: "パスワードが必要です",
|
||||
@@ -18,6 +24,10 @@ const jp: PartialLocaleType = {
|
||||
Input: "ここにアクセスコードを入力",
|
||||
Confirm: "確認",
|
||||
Later: "後で",
|
||||
Return: "戻る",
|
||||
SaasTips: "設定が面倒すぎる、すぐに使いたい",
|
||||
TopTips:
|
||||
"🥳 NextChat AIの発売特典で、OpenAI o1、GPT-4o、Claude-3.5などの最新の大規模モデルを今すぐアンロック",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count}件の会話`,
|
||||
@@ -282,6 +292,14 @@ const jp: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Access: {
|
||||
SaasStart: {
|
||||
Title: "NextChat AIを使用する",
|
||||
Label: "(コストパフォーマンスの最も高いソリューション)",
|
||||
SubTitle:
|
||||
"NextChatによって公式に管理されており、設定なしですぐに使用でき、OpenAI o1、GPT-4o、Claude-3.5などの最新の大規模モデルをサポートしています",
|
||||
ChatNow: "今すぐチャット",
|
||||
},
|
||||
|
||||
AccessCode: {
|
||||
Title: "アクセスパスワード",
|
||||
SubTitle: "管理者が暗号化アクセスを有効にしました",
|
||||
|
@@ -1,15 +1,21 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const ko: PartialLocaleType = {
|
||||
WIP: "곧 출시 예정...",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "유효하지 않은 API 키가 감지되었습니다. [설정](/#/settings) 페이지에서 API 키가 올바르게 구성되었는지 확인하십시오."
|
||||
: "잘못된 접근 비밀번호이거나 비밀번호가 비어 있습니다. [로그인](/#/auth) 페이지에서 올바른 접근 비밀번호를 입력하거나 [설정](/#/settings) 페이지에서 OpenAI API 키를 입력하십시오.",
|
||||
? `😆 대화 중 문제가 발생했습니다, 걱정하지 마세요:
|
||||
\\ 1️⃣ 제로 구성으로 시작하고 싶다면, [여기를 클릭하여 즉시 대화를 시작하세요 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ 자신의 OpenAI 리소스를 사용하고 싶다면, [여기를 클릭하여](/#/settings) 설정을 수정하세요 ⚙️`
|
||||
: `😆 대화 중 문제가 발생했습니다, 걱정하지 마세요:
|
||||
\ 1️⃣ 제로 구성으로 시작하고 싶다면, [여기를 클릭하여 즉시 대화를 시작하세요 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ 개인 배포 버전을 사용하고 있다면, [여기를 클릭하여](/#/auth) 접근 키를 입력하세요 🔑
|
||||
\ 3️⃣ 자신의 OpenAI 리소스를 사용하고 싶다면, [여기를 클릭하여](/#/settings) 설정을 수정하세요 ⚙️
|
||||
`,
|
||||
},
|
||||
Auth: {
|
||||
Title: "비밀번호 필요",
|
||||
@@ -18,6 +24,10 @@ const ko: PartialLocaleType = {
|
||||
Input: "여기에 접근 코드를 입력하십시오.",
|
||||
Confirm: "확인",
|
||||
Later: "나중에 하기",
|
||||
Return: "돌아가기",
|
||||
SaasTips: "설정이 너무 복잡합니다. 즉시 사용하고 싶습니다.",
|
||||
TopTips:
|
||||
"🥳 NextChat AI 출시 기념 할인, 지금 OpenAI o1, GPT-4o, Claude-3.5 및 최신 대형 모델을 해제하세요",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} 개의 대화`,
|
||||
@@ -281,6 +291,14 @@ const ko: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Access: {
|
||||
SaasStart: {
|
||||
Title: "NextChat AI 사용하기",
|
||||
Label: "(가장 비용 효율적인 솔루션)",
|
||||
SubTitle:
|
||||
"NextChat에 의해 공식적으로 유지 관리되며, 제로 구성으로 즉시 사용할 수 있으며, OpenAI o1, GPT-4o, Claude-3.5와 같은 최신 대형 모델을 지원합니다",
|
||||
ChatNow: "지금 채팅하기",
|
||||
},
|
||||
|
||||
AccessCode: {
|
||||
Title: "접근 비밀번호",
|
||||
SubTitle: "관리자가 암호화된 접근을 활성화했습니다.",
|
||||
|
@@ -1,15 +1,21 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const no: PartialLocaleType = {
|
||||
WIP: "Arbeid pågår ...",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "Ugyldig API-nøkkel oppdaget. Gå til [innstillinger](/#/settings) for å sjekke om API-nøkkelen er riktig konfigurert."
|
||||
: "Adgangskoden er feil eller tom. Gå til [innlogging](/#/auth) for å oppgi riktig adgangskode, eller fyll inn din egen OpenAI API-nøkkel på [innstillinger](/#/settings) siden.",
|
||||
? `😆 Samtalen har støtt på noen problemer, ikke bekymre deg:
|
||||
\\ 1️⃣ Hvis du vil starte uten konfigurasjon, [klikk her for å begynne å chatte umiddelbart 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Hvis du vil bruke dine egne OpenAI-ressurser, klikk [her](/#/settings) for å endre innstillingene ⚙️`
|
||||
: `😆 Samtalen har støtt på noen problemer, ikke bekymre deg:
|
||||
\ 1️⃣ Hvis du vil starte uten konfigurasjon, [klikk her for å begynne å chatte umiddelbart 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Hvis du bruker en privat distribusjonsversjon, klikk [her](/#/auth) for å skrive inn tilgangsnøkkelen 🔑
|
||||
\ 3️⃣ Hvis du vil bruke dine egne OpenAI-ressurser, klikk [her](/#/settings) for å endre innstillingene ⚙️
|
||||
`,
|
||||
},
|
||||
Auth: {
|
||||
Title: "Passord påkrevd",
|
||||
@@ -18,6 +24,11 @@ const no: PartialLocaleType = {
|
||||
Input: "Skriv tilgangskoden her",
|
||||
Confirm: "Bekreft",
|
||||
Later: "Kom tilbake senere",
|
||||
Return: "Tilbake",
|
||||
SaasTips:
|
||||
"Konfigurasjonen er for komplisert, jeg vil bruke det med en gang",
|
||||
TopTips:
|
||||
"🥳 NextChat AI lanseringstilbud, lås opp OpenAI o1, GPT-4o, Claude-3.5 og de nyeste store modellene nå",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} samtaler`,
|
||||
@@ -288,6 +299,14 @@ const no: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Access: {
|
||||
SaasStart: {
|
||||
Title: "Bruk NextChat AI",
|
||||
Label: "(Den mest kostnadseffektive løsningen)",
|
||||
SubTitle:
|
||||
"Offisielt vedlikeholdt av NextChat, klar til bruk uten konfigurasjon, støtter de nyeste store modellene som OpenAI o1, GPT-4o og Claude-3.5",
|
||||
ChatNow: "Chat nå",
|
||||
},
|
||||
|
||||
AccessCode: {
|
||||
Title: "Adgangskode",
|
||||
SubTitle: "Administrator har aktivert kryptert tilgang",
|
||||
|
@@ -1,15 +1,21 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import { PartialLocaleType } from "../locales/index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const pt: PartialLocaleType = {
|
||||
WIP: "Em breve...",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "Chave API inválida, por favor verifique em [Configurações](/#/settings)."
|
||||
: "Acesso não autorizado, por favor insira o código de acesso em [auth](/#/auth) ou insira sua Chave API OpenAI.",
|
||||
? `😆 A conversa encontrou alguns problemas, não se preocupe:
|
||||
\\ 1️⃣ Se você quiser começar sem configuração, [clique aqui para começar a conversar imediatamente 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Se você deseja usar seus próprios recursos OpenAI, clique [aqui](/#/settings) para modificar as configurações ⚙️`
|
||||
: `😆 A conversa encontrou alguns problemas, não se preocupe:
|
||||
\ 1️⃣ Se você quiser começar sem configuração, [clique aqui para começar a conversar imediatamente 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Se você estiver usando uma versão de implantação privada, clique [aqui](/#/auth) para inserir a chave de acesso 🔑
|
||||
\ 3️⃣ Se você deseja usar seus próprios recursos OpenAI, clique [aqui](/#/settings) para modificar as configurações ⚙️
|
||||
`,
|
||||
},
|
||||
Auth: {
|
||||
Title: "Necessário Código de Acesso",
|
||||
@@ -18,6 +24,10 @@ const pt: PartialLocaleType = {
|
||||
Input: "código de acesso",
|
||||
Confirm: "Confirmar",
|
||||
Later: "Depois",
|
||||
Return: "Voltar",
|
||||
SaasTips: "A configuração é muito complicada, quero usá-la imediatamente",
|
||||
TopTips:
|
||||
"🥳 Oferta de Lançamento do NextChat AI, desbloqueie o OpenAI o1, GPT-4o, Claude-3.5 e os mais recentes grandes modelos agora",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} mensagens`,
|
||||
@@ -281,6 +291,14 @@ const pt: PartialLocaleType = {
|
||||
NoAccess: "Insira a Chave API para verificar o saldo",
|
||||
},
|
||||
Access: {
|
||||
SaasStart: {
|
||||
Title: "Usar NextChat AI",
|
||||
Label: "(A solução mais econômica)",
|
||||
SubTitle:
|
||||
"Mantido oficialmente pelo NextChat, pronto para uso sem configuração, suporta os mais recentes grandes modelos como OpenAI o1, GPT-4o e Claude-3.5",
|
||||
ChatNow: "Conversar agora",
|
||||
},
|
||||
|
||||
AccessCode: {
|
||||
Title: "Código de Acesso",
|
||||
SubTitle: "Controle de Acesso Habilitado",
|
||||
|
@@ -1,15 +1,21 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import { PartialLocaleType } from "../locales/index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const ru: PartialLocaleType = {
|
||||
WIP: "Скоро...",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "Обнаружен недействительный API-ключ. Пожалуйста, перейдите на страницу [Настройки](/#/settings), чтобы проверить правильность конфигурации API-ключа."
|
||||
: "Неверный или пустой пароль доступа. Пожалуйста, перейдите на страницу [Вход](/#/auth), чтобы ввести правильный пароль доступа, или на страницу [Настройки](/#/settings), чтобы ввести ваш собственный API-ключ OpenAI.",
|
||||
? `😆 В разговоре возникли некоторые проблемы, не переживайте:
|
||||
\\ 1️⃣ Если вы хотите начать без настройки, [нажмите здесь, чтобы немедленно начать разговор 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Если вы хотите использовать свои ресурсы OpenAI, нажмите [здесь](/#/settings), чтобы изменить настройки ⚙️`
|
||||
: `😆 В разговоре возникли некоторые проблемы, не переживайте:
|
||||
\ 1️⃣ Если вы хотите начать без настройки, [нажмите здесь, чтобы немедленно начать разговор 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Если вы используете частную версию развертывания, нажмите [здесь](/#/auth), чтобы ввести ключ доступа 🔑
|
||||
\ 3️⃣ Если вы хотите использовать свои ресурсы OpenAI, нажмите [здесь](/#/settings), чтобы изменить настройки ⚙️
|
||||
`,
|
||||
},
|
||||
Auth: {
|
||||
Title: "Требуется пароль",
|
||||
@@ -18,6 +24,10 @@ const ru: PartialLocaleType = {
|
||||
Input: "Введите код доступа здесь",
|
||||
Confirm: "Подтвердить",
|
||||
Later: "Позже",
|
||||
Return: "Назад",
|
||||
SaasTips: "Настройка слишком сложна, я хочу использовать это немедленно",
|
||||
TopTips:
|
||||
"🥳 Предложение по запуску NextChat AI: разблокируйте OpenAI o1, GPT-4o, Claude-3.5 и новейшие большие модели прямо сейчас",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} бесед`,
|
||||
@@ -286,6 +296,14 @@ const ru: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Access: {
|
||||
SaasStart: {
|
||||
Title: "Используйте NextChat AI",
|
||||
Label: "(Самое экономичное решение)",
|
||||
SubTitle:
|
||||
"Официально поддерживается NextChat, готов к использованию без настройки, поддерживает последние крупные модели, такие как OpenAI o1, GPT-4o и Claude-3.5",
|
||||
ChatNow: "Начать чат",
|
||||
},
|
||||
|
||||
AccessCode: {
|
||||
Title: "Пароль доступа",
|
||||
SubTitle: "Администратор включил защиту паролем",
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
// if you are adding a new translation, please use PartialLocaleType instead of LocaleType
|
||||
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
@@ -9,8 +9,14 @@ const sk: PartialLocaleType = {
|
||||
WIP: "Už čoskoro...",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "Neplatný API kľúč, prosím skontrolujte ho na stránke [Nastavenia](/#/settings)."
|
||||
: "Neoprávnený prístup, prosím zadajte prístupový kód na stránke [auth](/#/auth), alebo zadajte váš OpenAI API kľúč.",
|
||||
? `😆 Rozhovor narazil na nejaké problémy, nebojte sa:
|
||||
\\ 1️⃣ Ak chcete začať bez konfigurácie, [kliknite sem, aby ste okamžite začali chatovať 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Ak chcete používať svoje vlastné zdroje OpenAI, kliknite [sem](/#/settings), aby ste upravili nastavenia ⚙️`
|
||||
: `😆 Rozhovor narazil na nejaké problémy, nebojte sa:
|
||||
\ 1️⃣ Ak chcete začať bez konfigurácie, [kliknite sem, aby ste okamžite začali chatovať 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Ak používate verziu súkromného nasadenia, kliknite [sem](/#/auth), aby ste zadali prístupový kľúč 🔑
|
||||
\ 3️⃣ Ak chcete používať svoje vlastné zdroje OpenAI, kliknite [sem](/#/settings), aby ste upravili nastavenia ⚙️
|
||||
`,
|
||||
},
|
||||
Auth: {
|
||||
Title: "Potrebný prístupový kód",
|
||||
@@ -19,6 +25,10 @@ const sk: PartialLocaleType = {
|
||||
Input: "prístupový kód",
|
||||
Confirm: "Potvrdiť",
|
||||
Later: "Neskôr",
|
||||
Return: "Návrat",
|
||||
SaasTips: "Nastavenie je príliš zložité, chcem to okamžite použiť",
|
||||
TopTips:
|
||||
"🥳 Uvítacia ponuka NextChat AI, okamžite odomknite OpenAI o1, GPT-4o, Claude-3.5 a najnovšie veľké modely",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} správ`,
|
||||
@@ -281,6 +291,14 @@ const sk: PartialLocaleType = {
|
||||
NoAccess: "Zadajte API kľúč na skontrolovanie zostatku",
|
||||
},
|
||||
Access: {
|
||||
SaasStart: {
|
||||
Title: "Použite NextChat AI",
|
||||
Label: "(Najvýhodnejšie riešenie)",
|
||||
SubTitle:
|
||||
"Oficiálne udržiavané NextChat, pripravené na použitie bez konfigurácie, podporuje najnovšie veľké modely ako OpenAI o1, GPT-4o a Claude-3.5",
|
||||
ChatNow: "Chatovať teraz",
|
||||
},
|
||||
|
||||
AccessCode: {
|
||||
Title: "Prístupový kód",
|
||||
SubTitle: "Povolený prístupový kód",
|
||||
|
@@ -1,15 +1,21 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const tr: PartialLocaleType = {
|
||||
WIP: "Çalışma devam ediyor...",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "Geçersiz API Anahtarı tespit edildi, lütfen API Anahtarını doğru şekilde yapılandırmak için [Ayarlar](/#/settings) sayfasına gidin."
|
||||
: "Erişim şifresi yanlış veya boş, lütfen doğru erişim şifresini girmek için [Giriş](/#/auth) sayfasına gidin veya kendi OpenAI API Anahtarınızı [Ayarlar](/#/settings) sayfasına girin.",
|
||||
? `😆 Sohbet bazı sorunlarla karşılaştı, endişelenmeyin:
|
||||
\\ 1️⃣ Eğer sıfır yapılandırma ile başlamak istiyorsanız, [buraya tıklayarak hemen sohbete başlayın 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Kendi OpenAI kaynaklarınızı kullanmak istiyorsanız, [buraya tıklayarak](/#/settings) ayarları değiştirin ⚙️`
|
||||
: `😆 Sohbet bazı sorunlarla karşılaştı, endişelenmeyin:
|
||||
\ 1️⃣ Eğer sıfır yapılandırma ile başlamak istiyorsanız, [buraya tıklayarak hemen sohbete başlayın 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Eğer özel dağıtım sürümü kullanıyorsanız, [buraya tıklayarak](/#/auth) erişim anahtarını girin 🔑
|
||||
\ 3️⃣ Kendi OpenAI kaynaklarınızı kullanmak istiyorsanız, [buraya tıklayarak](/#/settings) ayarları değiştirin ⚙️
|
||||
`,
|
||||
},
|
||||
Auth: {
|
||||
Title: "Şifre Gerekli",
|
||||
@@ -18,6 +24,10 @@ const tr: PartialLocaleType = {
|
||||
Input: "Erişim kodunu buraya girin",
|
||||
Confirm: "Onayla",
|
||||
Later: "Sonra",
|
||||
Return: "Geri",
|
||||
SaasTips: "Ayarlar çok karmaşık, hemen kullanmak istiyorum",
|
||||
TopTips:
|
||||
"🥳 NextChat AI lansman teklifi, OpenAI o1, GPT-4o, Claude-3.5 ve en son büyük modelleri şimdi açın",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} konuşma`,
|
||||
@@ -286,6 +296,14 @@ const tr: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Access: {
|
||||
SaasStart: {
|
||||
Title: "NextChat AI kullanın",
|
||||
Label: "(En maliyet etkin çözüm)",
|
||||
SubTitle:
|
||||
"NextChat tarafından resmi olarak yönetilmektedir, yapılandırma olmadan hemen kullanıma hazırdır, OpenAI o1, GPT-4o, Claude-3.5 gibi en son büyük modelleri destekler",
|
||||
ChatNow: "Şimdi sohbet et",
|
||||
},
|
||||
|
||||
AccessCode: {
|
||||
Title: "Erişim Şifresi",
|
||||
SubTitle: "Yönetici şifreli erişimi etkinleştirdi",
|
||||
|
@@ -1,14 +1,20 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { SubmitKey } from "../store/config";
|
||||
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const tw = {
|
||||
WIP: "此功能仍在開發中……",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "偵測到無效的 API Key,請前往[設定](/#/settings)頁面檢查 API Key 是否設定正確。"
|
||||
: "存取密碼不正確或尚未填寫,請前往[登入](/#/auth)頁面輸入正確的存取密碼,或者在[設定](/#/settings)頁面填入你自己的 OpenAI API Key。",
|
||||
? `😆 對話遇到了一些問題,不用慌:
|
||||
\\ 1️⃣ 想要無須設定開箱即用,[點選這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ 如果你想消耗自己的 OpenAI 資源,點選[這裡](/#/settings)修改設定 ⚙️`
|
||||
: `😆 對話遇到了一些問題,不用慌:
|
||||
\ 1️⃣ 想要無須設定開箱即用,[點選這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ 如果你正在使用私有部署版本,點選[這裡](/#/auth)輸入存取金鑰 🔑
|
||||
\ 3️⃣ 如果你想消耗自己的 OpenAI 資源,點選[這裡](/#/settings)修改設定 ⚙️
|
||||
`,
|
||||
},
|
||||
|
||||
Auth: {
|
||||
@@ -18,6 +24,10 @@ const tw = {
|
||||
Input: "在此處填寫存取密碼",
|
||||
Confirm: "確認",
|
||||
Later: "稍候再說",
|
||||
Return: "返回",
|
||||
SaasTips: "設定太麻煩,想要立即使用",
|
||||
TopTips:
|
||||
"🥳 NextChat AI 首發優惠,立刻解鎖 OpenAI o1, GPT-4o, Claude-3.5 等最新的大型語言模型",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} 則對話`,
|
||||
@@ -43,8 +53,8 @@ const tw = {
|
||||
PinToastAction: "檢視",
|
||||
Delete: "刪除",
|
||||
Edit: "編輯",
|
||||
RefreshTitle: "刷新標題",
|
||||
RefreshToast: "已發送刷新標題請求",
|
||||
RefreshTitle: "重新整理標題",
|
||||
RefreshToast: "已傳送重新整理標題請求",
|
||||
},
|
||||
Commands: {
|
||||
new: "新建聊天",
|
||||
@@ -85,10 +95,10 @@ const tw = {
|
||||
IsContext: "預設提示詞",
|
||||
ShortcutKey: {
|
||||
Title: "鍵盤快捷方式",
|
||||
newChat: "打開新聊天",
|
||||
newChat: "開啟新聊天",
|
||||
focusInput: "聚焦輸入框",
|
||||
copyLastMessage: "複製最後一個回覆",
|
||||
copyLastCode: "複製最後一個代碼塊",
|
||||
copyLastCode: "複製最後一個程式碼區塊",
|
||||
showShortcutKey: "顯示快捷方式",
|
||||
clearContext: "清除上下文",
|
||||
},
|
||||
@@ -165,9 +175,9 @@ const tw = {
|
||||
SubTitle: "聊天內容的字型大小",
|
||||
},
|
||||
FontFamily: {
|
||||
Title: "聊天字體",
|
||||
SubTitle: "聊天內容的字體,若置空則應用全局默認字體",
|
||||
Placeholder: "字體名稱",
|
||||
Title: "聊天字型",
|
||||
SubTitle: "聊天內容的字型,若留空則套用全域預設字型",
|
||||
Placeholder: "字型名稱",
|
||||
},
|
||||
InjectSystemPrompts: {
|
||||
Title: "匯入系統提示",
|
||||
@@ -288,6 +298,14 @@ const tw = {
|
||||
},
|
||||
|
||||
Access: {
|
||||
SaasStart: {
|
||||
Title: "使用 NextChat AI",
|
||||
Label: "(性價比最高的方案)",
|
||||
SubTitle:
|
||||
"由 NextChat 官方維護,無須設定開箱即用,支援 OpenAI o1、GPT-4o、Claude-3.5 等最新的大型語言模型",
|
||||
ChatNow: "立刻開始對話",
|
||||
},
|
||||
|
||||
AccessCode: {
|
||||
Title: "存取密碼",
|
||||
SubTitle: "管理員已開啟加密存取",
|
||||
@@ -468,18 +486,18 @@ const tw = {
|
||||
},
|
||||
},
|
||||
SearchChat: {
|
||||
Name: "搜索",
|
||||
Name: "搜尋聊天記錄",
|
||||
Page: {
|
||||
Title: "搜索聊天記錄",
|
||||
Search: "輸入搜索關鍵詞",
|
||||
Title: "搜尋聊天記錄",
|
||||
Search: "輸入搜尋關鍵詞",
|
||||
NoResult: "沒有找到結果",
|
||||
NoData: "沒有數據",
|
||||
Loading: "加載中",
|
||||
NoData: "沒有資料",
|
||||
Loading: "載入中",
|
||||
|
||||
SubTitle: (count: number) => `找到 ${count} 條結果`,
|
||||
},
|
||||
Item: {
|
||||
View: "查看",
|
||||
View: "檢視",
|
||||
},
|
||||
},
|
||||
NewChat: {
|
||||
|
@@ -1,15 +1,21 @@
|
||||
import { SubmitKey } from "../store/config";
|
||||
import type { PartialLocaleType } from "./index";
|
||||
import { getClientConfig } from "../config/client";
|
||||
|
||||
import { SAAS_CHAT_UTM_URL } from "@/app/constant";
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
|
||||
const vi: PartialLocaleType = {
|
||||
WIP: "Sắp ra mắt...",
|
||||
Error: {
|
||||
Unauthorized: isApp
|
||||
? "Phát hiện khóa API không hợp lệ, vui lòng truy cập trang [Cài đặt](/#/settings) để kiểm tra xem khóa API có được cấu hình chính xác không."
|
||||
: "Mật khẩu truy cập không đúng hoặc để trống, vui lòng truy cập trang [Đăng nhập](/#/auth) để nhập mật khẩu truy cập chính xác, hoặc điền khóa API OpenAI của bạn vào trang [Cài đặt](/#/settings).",
|
||||
? `😆 Cuộc trò chuyện gặp một số vấn đề, đừng lo lắng:
|
||||
\\ 1️⃣ Nếu bạn muốn bắt đầu mà không cần cấu hình, [nhấp vào đây để bắt đầu trò chuyện ngay lập tức 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\\ 2️⃣ Nếu bạn muốn sử dụng tài nguyên OpenAI của riêng mình, hãy nhấp [vào đây](/#/settings) để thay đổi cài đặt ⚙️`
|
||||
: `😆 Cuộc trò chuyện gặp một số vấn đề, đừng lo lắng:
|
||||
\ 1️⃣ Nếu bạn muốn bắt đầu mà không cần cấu hình, [nhấp vào đây để bắt đầu trò chuyện ngay lập tức 🚀](${SAAS_CHAT_UTM_URL})
|
||||
\ 2️⃣ Nếu bạn đang sử dụng phiên bản triển khai riêng, hãy nhấp [vào đây](/#/auth) để nhập khóa truy cập 🔑
|
||||
\ 3️⃣ Nếu bạn muốn sử dụng tài nguyên OpenAI của riêng mình, hãy nhấp [vào đây](/#/settings) để thay đổi cài đặt ⚙️
|
||||
`,
|
||||
},
|
||||
Auth: {
|
||||
Title: "Cần mật khẩu",
|
||||
@@ -18,6 +24,10 @@ const vi: PartialLocaleType = {
|
||||
Input: "Nhập mã truy cập tại đây",
|
||||
Confirm: "Xác nhận",
|
||||
Later: "Để sau",
|
||||
Return: "Trở lại",
|
||||
SaasTips: "Cấu hình quá phức tạp, tôi muốn sử dụng ngay lập tức",
|
||||
TopTips:
|
||||
"🥳 Ưu đãi ra mắt NextChat AI, mở khóa OpenAI o1, GPT-4o, Claude-3.5 và các mô hình lớn mới nhất ngay bây giờ",
|
||||
},
|
||||
ChatItem: {
|
||||
ChatItemCount: (count: number) => `${count} cuộc trò chuyện`,
|
||||
@@ -283,6 +293,14 @@ const vi: PartialLocaleType = {
|
||||
},
|
||||
|
||||
Access: {
|
||||
SaasStart: {
|
||||
Title: "Sử dụng NextChat AI",
|
||||
Label: "(Giải pháp tiết kiệm chi phí nhất)",
|
||||
SubTitle:
|
||||
"Được NextChat chính thức duy trì, sẵn sàng sử dụng mà không cần cấu hình, hỗ trợ các mô hình lớn mới nhất như OpenAI o1, GPT-4o và Claude-3.5",
|
||||
ChatNow: "Chat ngay",
|
||||
},
|
||||
|
||||
AccessCode: {
|
||||
Title: "Mật khẩu truy cập",
|
||||
SubTitle: "Quản trị viên đã bật truy cập mã hóa",
|
||||
|
@@ -3,7 +3,7 @@ import { BuiltinMask } from "./typing";
|
||||
export const CN_MASKS: BuiltinMask[] = [
|
||||
{
|
||||
avatar: "1f5bc-fe0f",
|
||||
name: "以文搜图",
|
||||
name: "AI文生图",
|
||||
context: [
|
||||
{
|
||||
id: "text-to-pic-0",
|
||||
@@ -28,7 +28,7 @@ export const CN_MASKS: BuiltinMask[] = [
|
||||
id: "text-to-pic-3",
|
||||
role: "system",
|
||||
content:
|
||||
"助手善于判断用户意图,当确定需要提供图片时,助手会变得沉默寡言,只使用以下格式输出markdown图片:,因为这个语法可以自动按照提示生成并渲染图片。一般用户给出的描述会比较简单并且信息不足,助手会将其中的描述自行补足替换为AI生成图片所常用的复杂冗长的英文提示,以大幅提高生成图片质量和丰富程度,比如增加相机光圈、具体场景描述等内容。助手会避免用代码块或原始块包围markdown标记,因为那样只会渲染出代码块或原始块而不是图片。",
|
||||
"助手善于判断用户意图,当确定需要提供图片时,助手会变得沉默寡言,只使用以下格式输出markdown图片:,因为这个语法可以自动按照提示生成并渲染图片。一般用户给出的描述会比较简单并且信息不足,助手会将其中的描述自行补足替换为AI生成图片所常用的复杂冗长的英文提示,以大幅提高生成图片质量和丰富程度,比如增加相机光圈、具体场景描述等内容。助手会避免用代码块或原始块包围markdown标记,因为那样只会渲染出代码块或原始块而不是图片。url中的空格等符号需要转义。",
|
||||
date: "",
|
||||
},
|
||||
],
|
||||
|
@@ -1,59 +1,58 @@
|
||||
import {
|
||||
ApiPath,
|
||||
DEFAULT_API_HOST,
|
||||
GoogleSafetySettingsThreshold,
|
||||
ServiceProvider,
|
||||
StoreKey,
|
||||
ApiPath,
|
||||
OPENAI_BASE_URL,
|
||||
ANTHROPIC_BASE_URL,
|
||||
GEMINI_BASE_URL,
|
||||
BAIDU_BASE_URL,
|
||||
BYTEDANCE_BASE_URL,
|
||||
ALIBABA_BASE_URL,
|
||||
TENCENT_BASE_URL,
|
||||
MOONSHOT_BASE_URL,
|
||||
STABILITY_BASE_URL,
|
||||
IFLYTEK_BASE_URL,
|
||||
DEEPSEEK_BASE_URL,
|
||||
XAI_BASE_URL,
|
||||
CHATGLM_BASE_URL,
|
||||
} from "../constant";
|
||||
import { getHeaders } from "../client/api";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import { ensure } from "../utils/clone";
|
||||
import { DEFAULT_CONFIG } from "./config";
|
||||
import { getModelProvider } from "../utils/model";
|
||||
|
||||
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
|
||||
|
||||
const isApp = getClientConfig()?.buildMode === "export";
|
||||
|
||||
const DEFAULT_OPENAI_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/openai"
|
||||
: ApiPath.OpenAI;
|
||||
const DEFAULT_OPENAI_URL = isApp ? OPENAI_BASE_URL : ApiPath.OpenAI;
|
||||
|
||||
const DEFAULT_GOOGLE_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/google"
|
||||
: ApiPath.Google;
|
||||
const DEFAULT_GOOGLE_URL = isApp ? GEMINI_BASE_URL : ApiPath.Google;
|
||||
|
||||
const DEFAULT_ANTHROPIC_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/anthropic"
|
||||
: ApiPath.Anthropic;
|
||||
const DEFAULT_ANTHROPIC_URL = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic;
|
||||
|
||||
const DEFAULT_BAIDU_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/baidu"
|
||||
: ApiPath.Baidu;
|
||||
const DEFAULT_BAIDU_URL = isApp ? BAIDU_BASE_URL : ApiPath.Baidu;
|
||||
|
||||
const DEFAULT_BYTEDANCE_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/bytedance"
|
||||
: ApiPath.ByteDance;
|
||||
const DEFAULT_BYTEDANCE_URL = isApp ? BYTEDANCE_BASE_URL : ApiPath.ByteDance;
|
||||
|
||||
const DEFAULT_ALIBABA_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/alibaba"
|
||||
: ApiPath.Alibaba;
|
||||
const DEFAULT_ALIBABA_URL = isApp ? ALIBABA_BASE_URL : ApiPath.Alibaba;
|
||||
|
||||
const DEFAULT_TENCENT_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/tencent"
|
||||
: ApiPath.Tencent;
|
||||
const DEFAULT_TENCENT_URL = isApp ? TENCENT_BASE_URL : ApiPath.Tencent;
|
||||
|
||||
const DEFAULT_MOONSHOT_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/moonshot"
|
||||
: ApiPath.Moonshot;
|
||||
const DEFAULT_MOONSHOT_URL = isApp ? MOONSHOT_BASE_URL : ApiPath.Moonshot;
|
||||
|
||||
const DEFAULT_STABILITY_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/stability"
|
||||
: ApiPath.Stability;
|
||||
const DEFAULT_STABILITY_URL = isApp ? STABILITY_BASE_URL : ApiPath.Stability;
|
||||
|
||||
const DEFAULT_IFLYTEK_URL = isApp
|
||||
? DEFAULT_API_HOST + "/api/proxy/iflytek"
|
||||
: ApiPath.Iflytek;
|
||||
const DEFAULT_IFLYTEK_URL = isApp ? IFLYTEK_BASE_URL : ApiPath.Iflytek;
|
||||
|
||||
const DEFAULT_DEEPSEEK_URL = isApp ? DEEPSEEK_BASE_URL : ApiPath.DeepSeek;
|
||||
|
||||
const DEFAULT_XAI_URL = isApp ? XAI_BASE_URL : ApiPath.XAI;
|
||||
|
||||
const DEFAULT_CHATGLM_URL = isApp ? CHATGLM_BASE_URL : ApiPath.ChatGLM;
|
||||
|
||||
const DEFAULT_ACCESS_STATE = {
|
||||
accessCode: "",
|
||||
@@ -112,6 +111,18 @@ const DEFAULT_ACCESS_STATE = {
|
||||
iflytekApiKey: "",
|
||||
iflytekApiSecret: "",
|
||||
|
||||
// deepseek
|
||||
deepseekUrl: DEFAULT_DEEPSEEK_URL,
|
||||
deepseekApiKey: "",
|
||||
|
||||
// xai
|
||||
xaiUrl: DEFAULT_XAI_URL,
|
||||
xaiApiKey: "",
|
||||
|
||||
// chatglm
|
||||
chatglmUrl: DEFAULT_CHATGLM_URL,
|
||||
chatglmApiKey: "",
|
||||
|
||||
// server config
|
||||
needCode: true,
|
||||
hideUserApiKey: false,
|
||||
@@ -120,6 +131,7 @@ const DEFAULT_ACCESS_STATE = {
|
||||
disableFastLink: false,
|
||||
customModels: "",
|
||||
defaultModel: "",
|
||||
visionModels: "",
|
||||
|
||||
// tts config
|
||||
edgeTTSVoiceName: "zh-CN-YunxiNeural",
|
||||
@@ -134,7 +146,10 @@ export const useAccessStore = createPersistStore(
|
||||
|
||||
return get().needCode;
|
||||
},
|
||||
|
||||
getVisionModels() {
|
||||
this.fetch();
|
||||
return get().visionModels;
|
||||
},
|
||||
edgeVoiceName() {
|
||||
this.fetch();
|
||||
|
||||
@@ -179,6 +194,17 @@ export const useAccessStore = createPersistStore(
|
||||
isValidIflytek() {
|
||||
return ensure(get(), ["iflytekApiKey"]);
|
||||
},
|
||||
isValidDeepSeek() {
|
||||
return ensure(get(), ["deepseekApiKey"]);
|
||||
},
|
||||
|
||||
isValidXAI() {
|
||||
return ensure(get(), ["xaiApiKey"]);
|
||||
},
|
||||
|
||||
isValidChatGLM() {
|
||||
return ensure(get(), ["chatglmApiKey"]);
|
||||
},
|
||||
|
||||
isAuthorized() {
|
||||
this.fetch();
|
||||
@@ -195,6 +221,9 @@ export const useAccessStore = createPersistStore(
|
||||
this.isValidTencent() ||
|
||||
this.isValidMoonshot() ||
|
||||
this.isValidIflytek() ||
|
||||
this.isValidDeepSeek() ||
|
||||
this.isValidXAI() ||
|
||||
this.isValidChatGLM() ||
|
||||
!this.enabledAccessControl() ||
|
||||
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
|
||||
);
|
||||
@@ -211,10 +240,13 @@ export const useAccessStore = createPersistStore(
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
// Set default model from env request
|
||||
let defaultModel = res.defaultModel ?? "";
|
||||
if (defaultModel !== "")
|
||||
DEFAULT_CONFIG.modelConfig.model = defaultModel;
|
||||
const defaultModel = res.defaultModel ?? "";
|
||||
if (defaultModel !== "") {
|
||||
const [model, providerName] = getModelProvider(defaultModel);
|
||||
DEFAULT_CONFIG.modelConfig.model = model;
|
||||
DEFAULT_CONFIG.modelConfig.providerName = providerName as any;
|
||||
}
|
||||
|
||||
return res;
|
||||
})
|
||||
.then((res: DangerConfig) => {
|
||||
|
@@ -16,6 +16,9 @@ import {
|
||||
DEFAULT_SYSTEM_TEMPLATE,
|
||||
KnowledgeCutOffDate,
|
||||
StoreKey,
|
||||
SUMMARIZE_MODEL,
|
||||
GEMINI_SUMMARIZE_MODEL,
|
||||
ServiceProvider,
|
||||
} from "../constant";
|
||||
import Locale, { getLang } from "../locales";
|
||||
import { isDalle3, safeLocalStorage } from "../utils";
|
||||
@@ -23,6 +26,8 @@ import { prettyObject } from "../utils/format";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import { estimateTokenLength } from "../utils/token";
|
||||
import { ModelConfig, ModelType, useAppConfig } from "./config";
|
||||
import { useAccessStore } from "./access";
|
||||
import { collectModelsWithDefaultModel } from "../utils/model";
|
||||
import { createEmptyMask, Mask } from "./mask";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
@@ -37,6 +42,7 @@ export type ChatMessageTool = {
|
||||
};
|
||||
content?: string;
|
||||
isError?: boolean;
|
||||
errorMsg?: string;
|
||||
};
|
||||
|
||||
export type ChatMessage = RequestMessage & {
|
||||
@@ -46,6 +52,7 @@ export type ChatMessage = RequestMessage & {
|
||||
id: string;
|
||||
model?: ModelType;
|
||||
tools?: ChatMessageTool[];
|
||||
audio_url?: string;
|
||||
};
|
||||
|
||||
export function createMessage(override: Partial<ChatMessage>): ChatMessage {
|
||||
@@ -102,6 +109,35 @@ function createEmptySession(): ChatSession {
|
||||
};
|
||||
}
|
||||
|
||||
function getSummarizeModel(
|
||||
currentModel: string,
|
||||
providerName: string,
|
||||
): string[] {
|
||||
// if it is using gpt-* models, force to use 4o-mini to summarize
|
||||
if (currentModel.startsWith("gpt") || currentModel.startsWith("chatgpt")) {
|
||||
const configStore = useAppConfig.getState();
|
||||
const accessStore = useAccessStore.getState();
|
||||
const allModel = collectModelsWithDefaultModel(
|
||||
configStore.models,
|
||||
[configStore.customModels, accessStore.customModels].join(","),
|
||||
accessStore.defaultModel,
|
||||
);
|
||||
const summarizeModel = allModel.find(
|
||||
(m) => m.name === SUMMARIZE_MODEL && m.available,
|
||||
);
|
||||
if (summarizeModel) {
|
||||
return [
|
||||
summarizeModel.name,
|
||||
summarizeModel.provider?.providerName as string,
|
||||
];
|
||||
}
|
||||
}
|
||||
if (currentModel.startsWith("gemini")) {
|
||||
return [GEMINI_SUMMARIZE_MODEL, ServiceProvider.Google];
|
||||
}
|
||||
return [currentModel, providerName];
|
||||
}
|
||||
|
||||
function countMessages(msgs: ChatMessage[]) {
|
||||
return msgs.reduce(
|
||||
(pre, cur) => pre + estimateTokenLength(getMessageTextContent(cur)),
|
||||
@@ -317,13 +353,13 @@ export const useChatStore = createPersistStore(
|
||||
return session;
|
||||
},
|
||||
|
||||
onNewMessage(message: ChatMessage) {
|
||||
get().updateCurrentSession((session) => {
|
||||
onNewMessage(message: ChatMessage, targetSession: ChatSession) {
|
||||
get().updateTargetSession(targetSession, (session) => {
|
||||
session.messages = session.messages.concat();
|
||||
session.lastUpdate = Date.now();
|
||||
});
|
||||
get().updateStat(message);
|
||||
get().summarizeSession();
|
||||
get().updateStat(message, targetSession);
|
||||
get().summarizeSession(false, targetSession);
|
||||
},
|
||||
|
||||
async onUserInput(content: string, attachImages?: string[]) {
|
||||
@@ -337,22 +373,16 @@ export const useChatStore = createPersistStore(
|
||||
|
||||
if (attachImages && attachImages.length > 0) {
|
||||
mContent = [
|
||||
{
|
||||
type: "text",
|
||||
text: userContent,
|
||||
},
|
||||
...(userContent
|
||||
? [{ type: "text" as const, text: userContent }]
|
||||
: []),
|
||||
...attachImages.map((url) => ({
|
||||
type: "image_url" as const,
|
||||
image_url: { url },
|
||||
})),
|
||||
];
|
||||
mContent = mContent.concat(
|
||||
attachImages.map((url) => {
|
||||
return {
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: url,
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let userMessage: ChatMessage = createMessage({
|
||||
role: "user",
|
||||
content: mContent,
|
||||
@@ -367,10 +397,10 @@ export const useChatStore = createPersistStore(
|
||||
// get recent messages
|
||||
const recentMessages = get().getMessagesWithMemory();
|
||||
const sendMessages = recentMessages.concat(userMessage);
|
||||
const messageIndex = get().currentSession().messages.length + 1;
|
||||
const messageIndex = session.messages.length + 1;
|
||||
|
||||
// save user's and bot's message
|
||||
get().updateCurrentSession((session) => {
|
||||
get().updateTargetSession(session, (session) => {
|
||||
const savedUserMessage = {
|
||||
...userMessage,
|
||||
content: mContent,
|
||||
@@ -391,7 +421,7 @@ export const useChatStore = createPersistStore(
|
||||
if (message) {
|
||||
botMessage.content = message;
|
||||
}
|
||||
get().updateCurrentSession((session) => {
|
||||
get().updateTargetSession(session, (session) => {
|
||||
session.messages = session.messages.concat();
|
||||
});
|
||||
},
|
||||
@@ -399,13 +429,14 @@ export const useChatStore = createPersistStore(
|
||||
botMessage.streaming = false;
|
||||
if (message) {
|
||||
botMessage.content = message;
|
||||
get().onNewMessage(botMessage);
|
||||
botMessage.date = new Date().toLocaleString();
|
||||
get().onNewMessage(botMessage, session);
|
||||
}
|
||||
ChatControllerPool.remove(session.id, botMessage.id);
|
||||
},
|
||||
onBeforeTool(tool: ChatMessageTool) {
|
||||
(botMessage.tools = botMessage?.tools || []).push(tool);
|
||||
get().updateCurrentSession((session) => {
|
||||
get().updateTargetSession(session, (session) => {
|
||||
session.messages = session.messages.concat();
|
||||
});
|
||||
},
|
||||
@@ -415,7 +446,7 @@ export const useChatStore = createPersistStore(
|
||||
tools[i] = { ...tool };
|
||||
}
|
||||
});
|
||||
get().updateCurrentSession((session) => {
|
||||
get().updateTargetSession(session, (session) => {
|
||||
session.messages = session.messages.concat();
|
||||
});
|
||||
},
|
||||
@@ -430,7 +461,7 @@ export const useChatStore = createPersistStore(
|
||||
botMessage.streaming = false;
|
||||
userMessage.isError = !isAborted;
|
||||
botMessage.isError = !isAborted;
|
||||
get().updateCurrentSession((session) => {
|
||||
get().updateTargetSession(session, (session) => {
|
||||
session.messages = session.messages.concat();
|
||||
});
|
||||
ChatControllerPool.remove(
|
||||
@@ -562,24 +593,33 @@ export const useChatStore = createPersistStore(
|
||||
set(() => ({ sessions }));
|
||||
},
|
||||
|
||||
resetSession() {
|
||||
get().updateCurrentSession((session) => {
|
||||
resetSession(session: ChatSession) {
|
||||
get().updateTargetSession(session, (session) => {
|
||||
session.messages = [];
|
||||
session.memoryPrompt = "";
|
||||
});
|
||||
},
|
||||
|
||||
summarizeSession(refreshTitle: boolean = false) {
|
||||
summarizeSession(
|
||||
refreshTitle: boolean = false,
|
||||
targetSession: ChatSession,
|
||||
) {
|
||||
const config = useAppConfig.getState();
|
||||
const session = get().currentSession();
|
||||
const session = targetSession;
|
||||
const modelConfig = session.mask.modelConfig;
|
||||
// skip summarize when using dalle3?
|
||||
if (isDalle3(modelConfig.model)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const providerName = modelConfig.compressProviderName;
|
||||
const api: ClientApi = getClientApi(providerName);
|
||||
// if not config compressModel, then using getSummarizeModel
|
||||
const [model, providerName] = modelConfig.compressModel
|
||||
? [modelConfig.compressModel, modelConfig.compressProviderName]
|
||||
: getSummarizeModel(
|
||||
session.mask.modelConfig.model,
|
||||
session.mask.modelConfig.providerName,
|
||||
);
|
||||
const api: ClientApi = getClientApi(providerName as ServiceProvider);
|
||||
|
||||
// remove error messages if any
|
||||
const messages = session.messages;
|
||||
@@ -610,16 +650,19 @@ export const useChatStore = createPersistStore(
|
||||
api.llm.chat({
|
||||
messages: topicMessages,
|
||||
config: {
|
||||
model: modelConfig.compressModel,
|
||||
model,
|
||||
stream: false,
|
||||
providerName,
|
||||
},
|
||||
onFinish(message) {
|
||||
get().updateCurrentSession(
|
||||
(session) =>
|
||||
(session.topic =
|
||||
message.length > 0 ? trimTopic(message) : DEFAULT_TOPIC),
|
||||
);
|
||||
onFinish(message, responseRes) {
|
||||
if (responseRes?.status === 200) {
|
||||
get().updateTargetSession(
|
||||
session,
|
||||
(session) =>
|
||||
(session.topic =
|
||||
message.length > 0 ? trimTopic(message) : DEFAULT_TOPIC),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -633,7 +676,7 @@ export const useChatStore = createPersistStore(
|
||||
|
||||
const historyMsgLength = countMessages(toBeSummarizedMsgs);
|
||||
|
||||
if (historyMsgLength > modelConfig?.max_tokens ?? 4000) {
|
||||
if (historyMsgLength > (modelConfig?.max_tokens || 4000)) {
|
||||
const n = toBeSummarizedMsgs.length;
|
||||
toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
|
||||
Math.max(0, n - modelConfig.historyMessageCount),
|
||||
@@ -673,17 +716,20 @@ export const useChatStore = createPersistStore(
|
||||
config: {
|
||||
...modelcfg,
|
||||
stream: true,
|
||||
model: modelConfig.compressModel,
|
||||
model,
|
||||
providerName,
|
||||
},
|
||||
onUpdate(message) {
|
||||
session.memoryPrompt = message;
|
||||
},
|
||||
onFinish(message) {
|
||||
console.log("[Memory] ", message);
|
||||
get().updateCurrentSession((session) => {
|
||||
session.lastSummarizeIndex = lastSummarizeIndex;
|
||||
session.memoryPrompt = message; // Update the memory prompt for stored it in local storage
|
||||
});
|
||||
onFinish(message, responseRes) {
|
||||
if (responseRes?.status === 200) {
|
||||
console.log("[Memory] ", message);
|
||||
get().updateTargetSession(session, (session) => {
|
||||
session.lastSummarizeIndex = lastSummarizeIndex;
|
||||
session.memoryPrompt = message; // Update the memory prompt for stored it in local storage
|
||||
});
|
||||
}
|
||||
},
|
||||
onError(err) {
|
||||
console.error("[Summarize] ", err);
|
||||
@@ -692,20 +738,22 @@ export const useChatStore = createPersistStore(
|
||||
}
|
||||
},
|
||||
|
||||
updateStat(message: ChatMessage) {
|
||||
get().updateCurrentSession((session) => {
|
||||
updateStat(message: ChatMessage, session: ChatSession) {
|
||||
get().updateTargetSession(session, (session) => {
|
||||
session.stat.charCount += message.content.length;
|
||||
// TODO: should update chat count and word count
|
||||
});
|
||||
},
|
||||
|
||||
updateCurrentSession(updater: (session: ChatSession) => void) {
|
||||
updateTargetSession(
|
||||
targetSession: ChatSession,
|
||||
updater: (session: ChatSession) => void,
|
||||
) {
|
||||
const sessions = get().sessions;
|
||||
const index = get().currentSessionIndex;
|
||||
const index = sessions.findIndex((s) => s.id === targetSession.id);
|
||||
if (index < 0) return;
|
||||
updater(sessions[index]);
|
||||
set(() => ({ sessions }));
|
||||
},
|
||||
|
||||
async clearAllData() {
|
||||
await indexedDBStorage.clear();
|
||||
localStorage.clear();
|
||||
@@ -722,7 +770,7 @@ export const useChatStore = createPersistStore(
|
||||
},
|
||||
{
|
||||
name: StoreKey.Chat,
|
||||
version: 3.2,
|
||||
version: 3.3,
|
||||
migrate(persistedState, version) {
|
||||
const state = persistedState as any;
|
||||
const newState = JSON.parse(
|
||||
@@ -778,6 +826,14 @@ export const useChatStore = createPersistStore(
|
||||
config.modelConfig.compressProviderName;
|
||||
});
|
||||
}
|
||||
// revert default summarize model for every session
|
||||
if (version < 3.3) {
|
||||
newState.sessions.forEach((s) => {
|
||||
const config = useAppConfig.getState();
|
||||
s.mask.modelConfig.compressModel = "";
|
||||
s.mask.modelConfig.compressProviderName = "";
|
||||
});
|
||||
}
|
||||
|
||||
return newState as any;
|
||||
},
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { LLMModel } from "../client/api";
|
||||
import { DalleSize, DalleQuality, DalleStyle } from "../typing";
|
||||
import { DalleQuality, DalleStyle, ModelSize } from "../typing";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import {
|
||||
DEFAULT_INPUT_TEMPLATE,
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
ServiceProvider,
|
||||
} from "../constant";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import type { Voice } from "rt-client";
|
||||
|
||||
export type ModelType = (typeof DEFAULT_MODELS)[number]["name"];
|
||||
export type TTSModelType = (typeof DEFAULT_TTS_MODELS)[number];
|
||||
@@ -50,6 +51,10 @@ export const DEFAULT_CONFIG = {
|
||||
enableAutoGenerateTitle: true,
|
||||
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
|
||||
|
||||
enableArtifacts: true, // show artifacts config
|
||||
|
||||
enableCodeFold: true, // code fold config
|
||||
|
||||
disablePromptHint: false,
|
||||
|
||||
dontShowMaskSplashScreen: false, // dont show splash screen when create chat
|
||||
@@ -69,11 +74,11 @@ export const DEFAULT_CONFIG = {
|
||||
sendMemory: true,
|
||||
historyMessageCount: 4,
|
||||
compressMessageLengthThreshold: 1000,
|
||||
compressModel: "gpt-4o-mini" as ModelType,
|
||||
compressProviderName: "OpenAI" as ServiceProvider,
|
||||
compressModel: "",
|
||||
compressProviderName: "",
|
||||
enableInjectSystemPrompts: true,
|
||||
template: config?.template ?? DEFAULT_INPUT_TEMPLATE,
|
||||
size: "1024x1024" as DalleSize,
|
||||
size: "1024x1024" as ModelSize,
|
||||
quality: "standard" as DalleQuality,
|
||||
style: "vivid" as DalleStyle,
|
||||
},
|
||||
@@ -86,12 +91,26 @@ export const DEFAULT_CONFIG = {
|
||||
voice: DEFAULT_TTS_VOICE,
|
||||
speed: 1.0,
|
||||
},
|
||||
|
||||
realtimeConfig: {
|
||||
enable: false,
|
||||
provider: "OpenAI" as ServiceProvider,
|
||||
model: "gpt-4o-realtime-preview-2024-10-01",
|
||||
apiKey: "",
|
||||
azure: {
|
||||
endpoint: "",
|
||||
deployment: "",
|
||||
},
|
||||
temperature: 0.9,
|
||||
voice: "alloy" as Voice,
|
||||
},
|
||||
};
|
||||
|
||||
export type ChatConfig = typeof DEFAULT_CONFIG;
|
||||
|
||||
export type ModelConfig = ChatConfig["modelConfig"];
|
||||
export type TTSConfig = ChatConfig["ttsConfig"];
|
||||
export type RealtimeConfig = ChatConfig["realtimeConfig"];
|
||||
|
||||
export function limitNumber(
|
||||
x: number,
|
||||
@@ -176,7 +195,7 @@ export const useAppConfig = createPersistStore(
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Config,
|
||||
version: 4,
|
||||
version: 4.1,
|
||||
|
||||
merge(persistedState, currentState) {
|
||||
const state = persistedState as ChatConfig | undefined;
|
||||
@@ -229,7 +248,7 @@ export const useAppConfig = createPersistStore(
|
||||
: config?.template ?? DEFAULT_INPUT_TEMPLATE;
|
||||
}
|
||||
|
||||
if (version < 4) {
|
||||
if (version < 4.1) {
|
||||
state.modelConfig.compressModel =
|
||||
DEFAULT_CONFIG.modelConfig.compressModel;
|
||||
state.modelConfig.compressProviderName =
|
||||
|
@@ -19,6 +19,7 @@ export type Mask = {
|
||||
builtin: boolean;
|
||||
plugin?: string[];
|
||||
enableArtifacts?: boolean;
|
||||
enableCodeFold?: boolean;
|
||||
};
|
||||
|
||||
export const DEFAULT_MASK_STATE = {
|
||||
|
@@ -2,8 +2,12 @@ import OpenAPIClientAxios from "openapi-client-axios";
|
||||
import { StoreKey } from "../constant";
|
||||
import { nanoid } from "nanoid";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import yaml from "js-yaml";
|
||||
import { adapter } from "../utils";
|
||||
import { adapter, getOperationId } from "../utils";
|
||||
import { useAccessStore } from "./access";
|
||||
|
||||
const isApp = getClientConfig()?.isApp !== false;
|
||||
|
||||
export type Plugin = {
|
||||
id: string;
|
||||
@@ -16,7 +20,6 @@ export type Plugin = {
|
||||
authLocation?: string;
|
||||
authHeader?: string;
|
||||
authToken?: string;
|
||||
usingProxy?: boolean;
|
||||
};
|
||||
|
||||
export type FunctionToolItem = {
|
||||
@@ -46,18 +49,25 @@ export const FunctionToolService = {
|
||||
plugin?.authType == "basic"
|
||||
? `Basic ${plugin?.authToken}`
|
||||
: plugin?.authType == "bearer"
|
||||
? ` Bearer ${plugin?.authToken}`
|
||||
? `Bearer ${plugin?.authToken}`
|
||||
: plugin?.authToken;
|
||||
const authLocation = plugin?.authLocation || "header";
|
||||
const definition = yaml.load(plugin.content) as any;
|
||||
const serverURL = definition?.servers?.[0]?.url;
|
||||
const baseURL = !!plugin?.usingProxy ? "/api/proxy" : serverURL;
|
||||
const baseURL = !isApp ? "/api/proxy" : serverURL;
|
||||
const headers: Record<string, string | undefined> = {
|
||||
"X-Base-URL": !!plugin?.usingProxy ? serverURL : undefined,
|
||||
"X-Base-URL": !isApp ? serverURL : undefined,
|
||||
};
|
||||
if (authLocation == "header") {
|
||||
headers[headerName] = tokenValue;
|
||||
}
|
||||
// try using openaiApiKey for Dalle3 Plugin.
|
||||
if (!tokenValue && plugin.id === "dalle3") {
|
||||
const openaiApiKey = useAccessStore.getState().openaiApiKey;
|
||||
if (openaiApiKey) {
|
||||
headers[headerName] = `Bearer ${openaiApiKey}`;
|
||||
}
|
||||
}
|
||||
const api = new OpenAPIClientAxios({
|
||||
definition: yaml.load(plugin.content) as any,
|
||||
axiosConfigDefaults: {
|
||||
@@ -106,7 +116,7 @@ export const FunctionToolService = {
|
||||
return {
|
||||
type: "function",
|
||||
function: {
|
||||
name: o.operationId,
|
||||
name: getOperationId(o),
|
||||
description: o.description || o.summary,
|
||||
parameters: parameters,
|
||||
},
|
||||
@@ -114,7 +124,7 @@ export const FunctionToolService = {
|
||||
}),
|
||||
funcs: operations.reduce((s, o) => {
|
||||
// @ts-ignore
|
||||
s[o.operationId] = function (args) {
|
||||
s[getOperationId(o)] = function (args) {
|
||||
const parameters: Record<string, any> = {};
|
||||
if (o.parameters instanceof Array) {
|
||||
o.parameters.forEach((p) => {
|
||||
@@ -129,8 +139,8 @@ export const FunctionToolService = {
|
||||
} else if (authLocation == "body") {
|
||||
args[headerName] = tokenValue;
|
||||
}
|
||||
// @ts-ignore
|
||||
return api.client[o.operationId](
|
||||
// @ts-ignore if o.operationId is null, then using o.path and o.method
|
||||
return api.client.paths[o.path][o.method](
|
||||
parameters,
|
||||
args,
|
||||
api.axiosConfigDefaults,
|
||||
@@ -165,7 +175,7 @@ export const usePluginStore = createPersistStore(
|
||||
(set, get) => ({
|
||||
create(plugin?: Partial<Plugin>) {
|
||||
const plugins = get().plugins;
|
||||
const id = nanoid();
|
||||
const id = plugin?.id || nanoid();
|
||||
plugins[id] = {
|
||||
...createEmptyPlugin(),
|
||||
...plugin,
|
||||
@@ -220,5 +230,42 @@ export const usePluginStore = createPersistStore(
|
||||
{
|
||||
name: StoreKey.Plugin,
|
||||
version: 1,
|
||||
onRehydrateStorage(state) {
|
||||
// Skip store rehydration on server side
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("./plugins.json")
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
Promise.all(
|
||||
res.map((item: any) =>
|
||||
// skip get schema
|
||||
state.get(item.id)
|
||||
? item
|
||||
: fetch(item.schema)
|
||||
.then((res) => res.text())
|
||||
.then((content) => ({
|
||||
...item,
|
||||
content,
|
||||
}))
|
||||
.catch((e) => item),
|
||||
),
|
||||
).then((builtinPlugins: any) => {
|
||||
builtinPlugins
|
||||
.filter((item: any) => item?.content)
|
||||
.forEach((item: any) => {
|
||||
const plugin = state.create(item);
|
||||
state.updatePlugin(plugin.id, (plugin) => {
|
||||
const tool = FunctionToolService.add(plugin, true);
|
||||
plugin.title = tool.api.definition.info.title;
|
||||
plugin.version = tool.api.definition.info.version;
|
||||
plugin.builtin = true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
|