Merge branch 'main' into tts-stt

This commit is contained in:
DDMeaqua 2024-09-18 10:39:56 +08:00
commit 212605a7e3
62 changed files with 2254 additions and 524 deletions

View File

@ -91,13 +91,13 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
- [x] Desktop App with tauri - [x] Desktop App with tauri
- [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc. - [x] Self-host Model: Fully compatible with [RWKV-Runner](https://github.com/josStorer/RWKV-Runner), as well as server deployment of [LocalAI](https://github.com/go-skynet/LocalAI): llama/gpt4all/rwkv/vicuna/koala/gpt4all-j/cerebras/falcon/dolly etc.
- [x] Artifacts: Easily preview, copy and share generated content/webpages through a separate window [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092) - [x] Artifacts: Easily preview, copy and share generated content/webpages through a separate window [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
- [x] Plugins: support artifacts, network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) - [x] Plugins: support network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
- [x] artifacts - [x] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
- [ ] network search, calculator, any other apis etc. [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
- [ ] local knowledge base - [ ] local knowledge base
## What's New ## What's New
- 🚀 v2.15.0 Now supports Plugins! Read this: [NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
- 🚀 v2.14.0 Now supports Artifacts & SD - 🚀 v2.14.0 Now supports Artifacts & SD
- 🚀 v2.10.1 support Google Gemini Pro model. - 🚀 v2.10.1 support Google Gemini Pro model.
- 🚀 v2.9.11 you can use azure endpoint now. - 🚀 v2.9.11 you can use azure endpoint now.
@ -128,13 +128,13 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
- [x] 使用 tauri 打包桌面应用 - [x] 使用 tauri 打包桌面应用
- [x] 支持自部署的大语言模型:开箱即用 [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) ,服务端部署 [LocalAI 项目](https://github.com/go-skynet/LocalAI) llama / gpt4all / rwkv / vicuna / koala / gpt4all-j / cerebras / falcon / dolly 等等,或者使用 [api-for-open-llm](https://github.com/xusenlinzy/api-for-open-llm) - [x] 支持自部署的大语言模型:开箱即用 [RWKV-Runner](https://github.com/josStorer/RWKV-Runner) ,服务端部署 [LocalAI 项目](https://github.com/go-skynet/LocalAI) llama / gpt4all / rwkv / vicuna / koala / gpt4all-j / cerebras / falcon / dolly 等等,或者使用 [api-for-open-llm](https://github.com/xusenlinzy/api-for-open-llm)
- [x] Artifacts: 通过独立窗口,轻松预览、复制和分享生成的内容/可交互网页 [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092) - [x] Artifacts: 通过独立窗口,轻松预览、复制和分享生成的内容/可交互网页 [#5092](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/pull/5092)
- [x] 插件机制,支持 artifacts联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) - [x] 插件机制,支持`联网搜索`、`计算器`、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
- [x] artifacts - [x] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165) [#5353](https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/issues/5353)
- [ ] 支持联网搜索、计算器、调用其他平台 api [#165](https://github.com/Yidadaa/ChatGPT-Next-Web/issues/165)
- [ ] 本地知识库 - [ ] 本地知识库
## 最新动态 ## 最新动态
- 🚀 v2.15.0 现在支持插件功能了!了解更多:[NextChat-Awesome-Plugins](https://github.com/ChatGPTNextWeb/NextChat-Awesome-Plugins)
- 🚀 v2.14.0 现在支持 Artifacts & SD 了。 - 🚀 v2.14.0 现在支持 Artifacts & SD 了。
- 🚀 v2.10.1 现在支持 Gemini Pro 模型。 - 🚀 v2.10.1 现在支持 Gemini Pro 模型。
- 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。 - 🚀 v2.9.11 现在可以使用自定义 Azure 服务了。

View File

@ -10,6 +10,8 @@ import { handle as alibabaHandler } from "../../alibaba";
import { handle as moonshotHandler } from "../../moonshot"; import { handle as moonshotHandler } from "../../moonshot";
import { handle as stabilityHandler } from "../../stability"; import { handle as stabilityHandler } from "../../stability";
import { handle as iflytekHandler } from "../../iflytek"; import { handle as iflytekHandler } from "../../iflytek";
import { handle as proxyHandler } from "../../proxy";
async function handle( async function handle(
req: NextRequest, req: NextRequest,
{ params }: { params: { provider: string; path: string[] } }, { params }: { params: { provider: string; path: string[] } },
@ -36,8 +38,10 @@ async function handle(
return stabilityHandler(req, { params }); return stabilityHandler(req, { params });
case ApiPath.Iflytek: case ApiPath.Iflytek:
return iflytekHandler(req, { params }); return iflytekHandler(req, { params });
default: case ApiPath.OpenAI:
return openaiHandler(req, { params }); return openaiHandler(req, { params });
default:
return proxyHandler(req, { params });
} }
} }

View File

@ -98,6 +98,7 @@ async function request(req: NextRequest) {
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Cache-Control": "no-store", "Cache-Control": "no-store",
"anthropic-dangerous-direct-browser-access": "true",
[authHeaderName]: authValue, [authHeaderName]: authValue,
"anthropic-version": "anthropic-version":
req.headers.get("anthropic-version") || req.headers.get("anthropic-version") ||

View File

@ -32,10 +32,7 @@ export async function requestOpenai(req: NextRequest) {
authHeaderName = "Authorization"; authHeaderName = "Authorization";
} }
let path = `${req.nextUrl.pathname}${req.nextUrl.search}`.replaceAll( let path = `${req.nextUrl.pathname}`.replaceAll("/api/openai/", "");
"/api/openai/",
"",
);
let baseUrl = let baseUrl =
(isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL; (isAzure ? serverConfig.azureUrl : serverConfig.baseUrl) || OPENAI_BASE_URL;

View File

@ -13,7 +13,9 @@ function getModels(remoteModelRes: OpenAIListModelResponse) {
if (config.disableGPT4) { if (config.disableGPT4) {
remoteModelRes.data = remoteModelRes.data.filter( remoteModelRes.data = remoteModelRes.data.filter(
(m) => !m.id.startsWith("gpt-4") || m.id.startsWith("gpt-4o-mini"), (m) =>
!(m.id.startsWith("gpt-4") || m.id.startsWith("chatgpt-4o")) ||
m.id.startsWith("gpt-4o-mini"),
); );
} }

75
app/api/proxy.ts Normal file
View File

@ -0,0 +1,75 @@
import { NextRequest, NextResponse } from "next/server";
export async function handle(
req: NextRequest,
{ params }: { params: { path: string[] } },
) {
console.log("[Proxy Route] params ", params);
if (req.method === "OPTIONS") {
return NextResponse.json({ body: "OK" }, { status: 200 });
}
// remove path params from searchParams
req.nextUrl.searchParams.delete("path");
req.nextUrl.searchParams.delete("provider");
const subpath = params.path.join("/");
const fetchUrl = `${req.headers.get(
"x-base-url",
)}/${subpath}?${req.nextUrl.searchParams.toString()}`;
const skipHeaders = ["connection", "host", "origin", "referer", "cookie"];
const headers = new Headers(
Array.from(req.headers.entries()).filter((item) => {
if (
item[0].indexOf("x-") > -1 ||
item[0].indexOf("sec-") > -1 ||
skipHeaders.includes(item[0])
) {
return false;
}
return true;
}),
);
const controller = new AbortController();
const fetchOptions: RequestInit = {
headers,
method: req.method,
body: req.body,
// to fix #2485: https://stackoverflow.com/questions/55920957/cloudflare-worker-typeerror-one-time-use-body
redirect: "manual",
// @ts-ignore
duplex: "half",
signal: controller.signal,
};
const timeoutId = setTimeout(
() => {
controller.abort();
},
10 * 60 * 1000,
);
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");
// The latest version of the OpenAI API forced the content-encoding to be "br" in json response
// So if the streaming is disabled, we need to remove the content-encoding header
// Because Vercel uses gzip to compress the response, if we don't remove the content-encoding header
// The browser will try to decode the response with brotli and fail
newHeaders.delete("content-encoding");
return new Response(res.body, {
status: res.status,
statusText: res.statusText,
headers: newHeaders,
});
} finally {
clearTimeout(timeoutId);
}
}

View File

@ -5,7 +5,13 @@ import {
ModelProvider, ModelProvider,
ServiceProvider, ServiceProvider,
} from "../constant"; } from "../constant";
import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store"; import {
ChatMessageTool,
ChatMessage,
ModelType,
useAccessStore,
useChatStore,
} from "../store";
import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai"; import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
import { GeminiProApi } from "./platforms/google"; import { GeminiProApi } from "./platforms/google";
import { ClaudeApi } from "./platforms/anthropic"; import { ClaudeApi } from "./platforms/anthropic";
@ -76,6 +82,8 @@ export interface ChatOptions {
onFinish: (message: string) => void; onFinish: (message: string) => void;
onError?: (err: Error) => void; onError?: (err: Error) => void;
onController?: (controller: AbortController) => void; onController?: (controller: AbortController) => void;
onBeforeTool?: (tool: ChatMessageTool) => void;
onAfterTool?: (tool: ChatMessageTool) => void;
} }
export interface LLMUsage { export interface LLMUsage {

View File

@ -7,7 +7,13 @@ import {
SpeechOptions, SpeechOptions,
TranscriptionOptions, TranscriptionOptions,
} from "../api"; } from "../api";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import {
useAccessStore,
useAppConfig,
useChatStore,
usePluginStore,
ChatMessageTool,
} from "@/app/store";
import { getClientConfig } from "@/app/config/client"; import { getClientConfig } from "@/app/config/client";
import { DEFAULT_API_HOST } from "@/app/constant"; import { DEFAULT_API_HOST } from "@/app/constant";
import { import {
@ -18,8 +24,9 @@ import {
import Locale from "../../locales"; import Locale from "../../locales";
import { prettyObject } from "@/app/utils/format"; import { prettyObject } from "@/app/utils/format";
import { getMessageTextContent, isVisionModel } from "@/app/utils"; import { getMessageTextContent, isVisionModel } from "@/app/utils";
import { preProcessImageContent } from "@/app/utils/chat"; import { preProcessImageContent, stream } from "@/app/utils/chat";
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
import { RequestPayload } from "./openai";
export type MultiBlockContent = { export type MultiBlockContent = {
type: "image" | "text"; type: "image" | "text";
@ -205,112 +212,126 @@ export class ClaudeApi implements LLMApi {
const controller = new AbortController(); const controller = new AbortController();
options.onController?.(controller); options.onController?.(controller);
const payload = {
method: "POST",
body: JSON.stringify(requestBody),
signal: controller.signal,
headers: {
...getHeaders(), // get common headers
"anthropic-version": accessStore.anthropicApiVersion,
// do not send `anthropicApiKey` in browser!!!
// Authorization: getAuthKey(accessStore.anthropicApiKey),
},
};
if (shouldStream) { if (shouldStream) {
try { let index = -1;
const context = { const [tools, funcs] = usePluginStore
text: "", .getState()
finished: false, .getAsTools(
}; useChatStore.getState().currentSession().mask?.plugin || [],
);
const finish = () => { return stream(
if (!context.finished) { path,
options.onFinish(context.text); requestBody,
context.finished = true; {
} ...getHeaders(),
}; "anthropic-version": accessStore.anthropicApiVersion,
},
controller.signal.onabort = finish; // @ts-ignore
fetchEventSource(path, { tools.map((tool) => ({
...payload, name: tool?.function?.name,
async onopen(res) { description: tool?.function?.description,
const contentType = res.headers.get("content-type"); input_schema: tool?.function?.parameters,
console.log("response content type: ", contentType); })),
funcs,
if (contentType?.startsWith("text/plain")) { controller,
context.text = await res.clone().text(); // parseSSE
return finish(); (text: string, runTools: ChatMessageTool[]) => {
} // console.log("parseSSE", text, runTools);
let chunkJson:
if ( | undefined
!res.ok || | {
!res.headers type: "content_block_delta" | "content_block_stop";
.get("content-type") content_block?: {
?.startsWith(EventStreamContentType) || type: "tool_use";
res.status !== 200 id: string;
) { name: string;
const responseTexts = [context.text];
let extraInfo = await res.clone().text();
try {
const resJson = await res.clone().json();
extraInfo = prettyObject(resJson);
} catch {}
if (res.status === 401) {
responseTexts.push(Locale.Error.Unauthorized);
}
if (extraInfo) {
responseTexts.push(extraInfo);
}
context.text = responseTexts.join("\n\n");
return finish();
}
},
onmessage(msg) {
let chunkJson:
| undefined
| {
type: "content_block_delta" | "content_block_stop";
delta?: {
type: "text_delta";
text: string;
};
index: number;
}; };
try { delta?: {
chunkJson = JSON.parse(msg.data); type: "text_delta" | "input_json_delta";
} catch (e) { text?: string;
console.error("[Response] parse error", msg.data); partial_json?: string;
} };
index: number;
};
chunkJson = JSON.parse(text);
if (!chunkJson || chunkJson.type === "content_block_stop") { if (chunkJson?.content_block?.type == "tool_use") {
return finish(); index += 1;
} const id = chunkJson?.content_block.id;
const name = chunkJson?.content_block.name;
const { delta } = chunkJson; runTools.push({
if (delta?.text) { id,
context.text += delta.text; type: "function",
options.onUpdate?.(context.text, delta.text); function: {
} name,
}, arguments: "",
onclose() { },
finish(); });
}, }
onerror(e) { if (
options.onError?.(e); chunkJson?.delta?.type == "input_json_delta" &&
throw e; chunkJson?.delta?.partial_json
}, ) {
openWhenHidden: true, // @ts-ignore
}); runTools[index]["function"]["arguments"] +=
} catch (e) { chunkJson?.delta?.partial_json;
console.error("failed to chat", e); }
options.onError?.(e as Error); return chunkJson?.delta?.text;
} },
// processToolMessage, include tool_calls message and tool call results
(
requestPayload: RequestPayload,
toolCallMessage: any,
toolCallResult: any[],
) => {
// reset index value
index = -1;
// @ts-ignore
requestPayload?.messages?.splice(
// @ts-ignore
requestPayload?.messages?.length,
0,
{
role: "assistant",
content: toolCallMessage.tool_calls.map(
(tool: ChatMessageTool) => ({
type: "tool_use",
id: tool.id,
name: tool?.function?.name,
input: tool?.function?.arguments
? JSON.parse(tool?.function?.arguments)
: {},
}),
),
},
// @ts-ignore
...toolCallResult.map((result) => ({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: result.tool_call_id,
content: result.content,
},
],
})),
);
},
options,
);
} else { } else {
const payload = {
method: "POST",
body: JSON.stringify(requestBody),
signal: controller.signal,
headers: {
...getHeaders(), // get common headers
"anthropic-version": accessStore.anthropicApiVersion,
// do not send `anthropicApiKey` in browser!!!
// Authorization: getAuthKey(accessStore.anthropicApiKey),
},
};
try { try {
controller.signal.onabort = () => options.onFinish(""); controller.signal.onabort = () => options.onFinish("");

View File

@ -8,9 +8,15 @@ import {
REQUEST_TIMEOUT_MS, REQUEST_TIMEOUT_MS,
ServiceProvider, ServiceProvider,
} from "@/app/constant"; } from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import {
useAccessStore,
useAppConfig,
useChatStore,
ChatMessageTool,
usePluginStore,
} from "@/app/store";
import { collectModelsWithDefaultModel } from "@/app/utils/model"; import { collectModelsWithDefaultModel } from "@/app/utils/model";
import { preProcessImageContent } from "@/app/utils/chat"; import { preProcessImageContent, stream } from "@/app/utils/chat";
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
import { import {
@ -125,115 +131,66 @@ export class MoonshotApi implements LLMApi {
); );
if (shouldStream) { if (shouldStream) {
let responseText = ""; const [tools, funcs] = usePluginStore
let remainText = ""; .getState()
let finished = false; .getAsTools(
useChatStore.getState().currentSession().mask?.plugin || [],
// animate response to make it looks smooth );
function animateResponseText() { return stream(
if (finished || controller.signal.aborted) { chatPath,
responseText += remainText; requestPayload,
console.log("[Response Animation] finished"); getHeaders(),
if (responseText?.length === 0) { tools as any,
options.onError?.(new Error("empty response from server")); 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; return choices[0]?.delta?.content;
} },
// processToolMessage, include tool_calls message and tool call results
if (remainText.length > 0) { (
const fetchCount = Math.max(1, Math.round(remainText.length / 60)); requestPayload: RequestPayload,
const fetchText = remainText.slice(0, fetchCount); toolCallMessage: any,
responseText += fetchText; toolCallResult: any[],
remainText = remainText.slice(fetchCount); ) => {
options.onUpdate?.(responseText, fetchText); // @ts-ignore
} requestPayload?.messages?.splice(
// @ts-ignore
requestAnimationFrame(animateResponseText); requestPayload?.messages?.length,
} 0,
toolCallMessage,
// start animaion ...toolCallResult,
animateResponseText();
const finish = () => {
if (!finished) {
finished = true;
options.onFinish(responseText + remainText);
}
};
controller.signal.onabort = finish;
fetchEventSource(chatPath, {
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
console.log(
"[OpenAI] request response content type: ",
contentType,
); );
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) { options,
if (msg.data === "[DONE]" || finished) { );
return finish();
}
const text = msg.data;
try {
const json = JSON.parse(text);
const choices = json.choices as Array<{
delta: { content: string };
}>;
const delta = choices[0]?.delta?.content;
const textmoderation = json?.prompt_filter_results;
if (delta) {
remainText += delta;
}
} catch (e) {
console.error("[Request] parse error", text, msg);
}
},
onclose() {
finish();
},
onerror(e) {
options.onError?.(e);
throw e;
},
openWhenHidden: true,
});
} else { } else {
const res = await fetch(chatPath, chatPayload); const res = await fetch(chatPath, chatPayload);
clearTimeout(requestTimeoutId); clearTimeout(requestTimeoutId);

View File

@ -9,12 +9,19 @@ import {
REQUEST_TIMEOUT_MS, REQUEST_TIMEOUT_MS,
ServiceProvider, ServiceProvider,
} from "@/app/constant"; } from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import {
ChatMessageTool,
useAccessStore,
useAppConfig,
useChatStore,
usePluginStore,
} from "@/app/store";
import { collectModelsWithDefaultModel } from "@/app/utils/model"; import { collectModelsWithDefaultModel } from "@/app/utils/model";
import { import {
preProcessImageContent, preProcessImageContent,
uploadImage, uploadImage,
base64Image2Blob, base64Image2Blob,
stream,
} from "@/app/utils/chat"; } from "@/app/utils/chat";
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
import { DalleSize, DalleQuality, DalleStyle } from "@/app/typing"; import { DalleSize, DalleQuality, DalleStyle } from "@/app/typing";
@ -234,6 +241,7 @@ export class ChatGPTApi implements LLMApi {
let requestPayload: RequestPayload | DalleRequestPayload; let requestPayload: RequestPayload | DalleRequestPayload;
const isDalle3 = _isDalle3(options.config.model); const isDalle3 = _isDalle3(options.config.model);
const isO1 = options.config.model.startsWith("o1");
if (isDalle3) { if (isDalle3) {
const prompt = getMessageTextContent( const prompt = getMessageTextContent(
options.messages.slice(-1)?.pop() as any, options.messages.slice(-1)?.pop() as any,
@ -255,30 +263,32 @@ export class ChatGPTApi implements LLMApi {
const content = visionModel const content = visionModel
? await preProcessImageContent(v.content) ? await preProcessImageContent(v.content)
: getMessageTextContent(v); : getMessageTextContent(v);
messages.push({ role: v.role, content }); if (!(isO1 && v.role === "system"))
messages.push({ role: v.role, content });
} }
// O1 not support image, tools (plugin in ChatGPTNextWeb) and system, stream, logprobs, temperature, top_p, n, presence_penalty, frequency_penalty yet.
requestPayload = { requestPayload = {
messages, messages,
stream: options.config.stream, stream: !isO1 ? options.config.stream : false,
model: modelConfig.model, model: modelConfig.model,
temperature: modelConfig.temperature, temperature: !isO1 ? modelConfig.temperature : 1,
presence_penalty: modelConfig.presence_penalty, presence_penalty: !isO1 ? modelConfig.presence_penalty : 0,
frequency_penalty: modelConfig.frequency_penalty, frequency_penalty: !isO1 ? modelConfig.frequency_penalty : 0,
top_p: modelConfig.top_p, top_p: !isO1 ? modelConfig.top_p : 1,
// max_tokens: Math.max(modelConfig.max_tokens, 1024), // 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. // Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore.
}; };
// add max_tokens to vision model // add max_tokens to vision model
if (visionModel && modelConfig.model.includes("preview")) { if (visionModel) {
requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000); requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000);
} }
} }
console.log("[Request] openai payload: ", requestPayload); console.log("[Request] openai payload: ", requestPayload);
const shouldStream = !isDalle3 && !!options.config.stream; const shouldStream = !isDalle3 && !!options.config.stream && !isO1;
const controller = new AbortController(); const controller = new AbortController();
options.onController?.(controller); options.onController?.(controller);
@ -314,143 +324,82 @@ export class ChatGPTApi implements LLMApi {
isDalle3 ? OpenaiPath.ImagePath : OpenaiPath.ChatPath, isDalle3 ? OpenaiPath.ImagePath : OpenaiPath.ChatPath,
); );
} }
const chatPayload = {
method: "POST",
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
};
// make a fetch request
const requestTimeoutId = setTimeout(
() => controller.abort(),
isDalle3 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
);
if (shouldStream) { if (shouldStream) {
let responseText = ""; const [tools, funcs] = usePluginStore
let remainText = ""; .getState()
let finished = false; .getAsTools(
useChatStore.getState().currentSession().mask?.plugin || [],
// animate response to make it looks smooth );
function animateResponseText() { // console.log("getAsTools", tools, funcs);
if (finished || controller.signal.aborted) { stream(
responseText += remainText; chatPath,
console.log("[Response Animation] finished"); requestPayload,
if (responseText?.length === 0) { getHeaders(),
options.onError?.(new Error("empty response from server")); 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; return choices[0]?.delta?.content;
} },
// processToolMessage, include tool_calls message and tool call results
if (remainText.length > 0) { (
const fetchCount = Math.max(1, Math.round(remainText.length / 60)); requestPayload: RequestPayload,
const fetchText = remainText.slice(0, fetchCount); toolCallMessage: any,
responseText += fetchText; toolCallResult: any[],
remainText = remainText.slice(fetchCount); ) => {
options.onUpdate?.(responseText, fetchText); // @ts-ignore
} requestPayload?.messages?.splice(
// @ts-ignore
requestAnimationFrame(animateResponseText); requestPayload?.messages?.length,
} 0,
toolCallMessage,
// start animaion ...toolCallResult,
animateResponseText(); );
},
const finish = () => { options,
if (!finished) { );
finished = true; } else {
options.onFinish(responseText + remainText); const chatPayload = {
} method: "POST",
body: JSON.stringify(requestPayload),
signal: controller.signal,
headers: getHeaders(),
}; };
controller.signal.onabort = finish; // make a fetch request
const requestTimeoutId = setTimeout(
() => controller.abort(),
isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
);
fetchEventSource(chatPath, {
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
console.log(
"[OpenAI] request response content type: ",
contentType,
);
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 choices = json.choices as Array<{
delta: { content: string };
}>;
const delta = choices[0]?.delta?.content;
const textmoderation = json?.prompt_filter_results;
if (delta) {
remainText += delta;
}
if (
textmoderation &&
textmoderation.length > 0 &&
ServiceProvider.Azure
) {
const contentFilterResults =
textmoderation[0]?.content_filter_results;
console.log(
`[${ServiceProvider.Azure}] [Text Moderation] flagged categories result:`,
contentFilterResults,
);
}
} catch (e) {
console.error("[Request] parse error", text, msg);
}
},
onclose() {
finish();
},
onerror(e) {
options.onError?.(e);
throw e;
},
openWhenHidden: true,
});
} else {
const res = await fetch(chatPath, chatPayload); const res = await fetch(chatPath, chatPayload);
clearTimeout(requestTimeoutId); clearTimeout(requestTimeoutId);
@ -542,7 +491,9 @@ export class ChatGPTApi implements LLMApi {
}); });
const resJson = (await res.json()) as OpenAIListModelResponse; const resJson = (await res.json()) as OpenAIListModelResponse;
const chatModels = resJson.data?.filter((m) => m.id.startsWith("gpt-")); const chatModels = resJson.data?.filter(
(m) => m.id.startsWith("gpt-") || m.id.startsWith("chatgpt-"),
);
console.log("[Models]", chatModels); console.log("[Models]", chatModels);
if (!chatModels) { if (!chatModels) {

View File

@ -80,7 +80,7 @@ export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
}, [props.autoHeight, props.height, iframeHeight]); }, [props.autoHeight, props.height, iframeHeight]);
const srcDoc = useMemo(() => { const srcDoc = useMemo(() => {
const script = `<script>new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', height: entries[0].target.clientHeight}, '*')).observe(document.body)</script>`; const script = `<script>window.addEventListener("DOMContentLoaded", () => new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', height: entries[0].target.clientHeight}, '*')).observe(document.body))</script>`;
if (props.code.includes("<!DOCTYPE html>")) { if (props.code.includes("<!DOCTYPE html>")) {
props.code.replace("<!DOCTYPE html>", "<!DOCTYPE html>" + script); props.code.replace("<!DOCTYPE html>", "<!DOCTYPE html>" + script);
} }

View File

@ -413,6 +413,21 @@
margin-top: 5px; margin-top: 5px;
} }
.chat-message-tools {
font-size: 12px;
color: #aaa;
line-height: 1.5;
margin-top: 5px;
.chat-message-tool {
display: flex;
align-items: end;
svg {
margin-left: 5px;
margin-right: 5px;
}
}
}
.chat-message-item { .chat-message-item {
box-sizing: border-box; box-sizing: border-box;
max-width: 100%; max-width: 100%;
@ -631,3 +646,51 @@
bottom: 30px; bottom: 30px;
} }
} }
.shortcut-key-container {
padding: 10px;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.shortcut-key-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
gap: 16px;
}
.shortcut-key-item {
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden;
padding: 10px;
background-color: var(--white);
}
.shortcut-key-title {
font-size: 14px;
color: var(--black);
}
.shortcut-key-keys {
display: flex;
gap: 8px;
}
.shortcut-key {
display: flex;
align-items: center;
justify-content: center;
border: var(--border-in-light);
border-radius: 8px;
padding: 4px;
background-color: var(--gray);
min-width: 32px;
}
.shortcut-key span {
font-size: 12px;
color: var(--black);
}

View File

@ -31,6 +31,7 @@ import DeleteIcon from "../icons/clear.svg";
import PinIcon from "../icons/pin.svg"; import PinIcon from "../icons/pin.svg";
import EditIcon from "../icons/rename.svg"; import EditIcon from "../icons/rename.svg";
import ConfirmIcon from "../icons/confirm.svg"; import ConfirmIcon from "../icons/confirm.svg";
import CloseIcon from "../icons/close.svg";
import CancelIcon from "../icons/cancel.svg"; import CancelIcon from "../icons/cancel.svg";
import ImageIcon from "../icons/image.svg"; import ImageIcon from "../icons/image.svg";
@ -44,6 +45,8 @@ import SizeIcon from "../icons/size.svg";
import QualityIcon from "../icons/hd.svg"; import QualityIcon from "../icons/hd.svg";
import StyleIcon from "../icons/palette.svg"; import StyleIcon from "../icons/palette.svg";
import PluginIcon from "../icons/plugin.svg"; import PluginIcon from "../icons/plugin.svg";
import ShortcutkeyIcon from "../icons/shortcutkey.svg";
import ReloadIcon from "../icons/reload.svg";
import { import {
ChatMessage, ChatMessage,
@ -56,6 +59,7 @@ import {
useAppConfig, useAppConfig,
DEFAULT_TOPIC, DEFAULT_TOPIC,
ModelType, ModelType,
usePluginStore,
} from "../store"; } from "../store";
import { import {
@ -67,6 +71,8 @@ import {
getMessageImages, getMessageImages,
isVisionModel, isVisionModel,
isDalle3, isDalle3,
showPlugins,
safeLocalStorage,
isFirefox, isFirefox,
} from "../utils"; } from "../utils";
@ -103,7 +109,6 @@ import {
REQUEST_TIMEOUT_MS, REQUEST_TIMEOUT_MS,
UNFINISHED_INPUT, UNFINISHED_INPUT,
ServiceProvider, ServiceProvider,
Plugin,
} from "../constant"; } from "../constant";
import { Avatar } from "./emoji"; import { Avatar } from "./emoji";
import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask"; import { ContextPrompts, MaskAvatar, MaskConfig } from "./mask";
@ -114,6 +119,8 @@ import { ExportMessageModal } from "./exporter";
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
import { useAllModels } from "../utils/hooks"; import { useAllModels } from "../utils/hooks";
import { MultimodalContent } from "../client/api"; import { MultimodalContent } from "../client/api";
const localStorage = safeLocalStorage();
import { ClientApi } from "../client/api"; import { ClientApi } from "../client/api";
import { createTTSPlayer } from "../utils/audio"; import { createTTSPlayer } from "../utils/audio";
import { import {
@ -204,7 +211,7 @@ function PromptToast(props: {
return ( return (
<div className={styles["prompt-toast"]} key="prompt-toast"> <div className={styles["prompt-toast"]} key="prompt-toast">
{props.showToast && ( {props.showToast && context.length > 0 && (
<div <div
className={styles["prompt-toast-inner"] + " clickable"} className={styles["prompt-toast-inner"] + " clickable"}
role="button" role="button"
@ -453,11 +460,13 @@ export function ChatActions(props: {
showPromptHints: () => void; showPromptHints: () => void;
hitBottom: boolean; hitBottom: boolean;
uploading: boolean; uploading: boolean;
setShowShortcutKeyModal: React.Dispatch<React.SetStateAction<boolean>>;
setUserInput: (input: string) => void; setUserInput: (input: string) => void;
}) { }) {
const config = useAppConfig(); const config = useAppConfig();
const navigate = useNavigate(); const navigate = useNavigate();
const chatStore = useChatStore(); const chatStore = useChatStore();
const pluginStore = usePluginStore();
// switch themes // switch themes
const theme = config.theme; const theme = config.theme;
@ -518,6 +527,8 @@ export function ChatActions(props: {
const currentStyle = const currentStyle =
chatStore.currentSession().mask.modelConfig?.style ?? "vivid"; chatStore.currentSession().mask.modelConfig?.style ?? "vivid";
const isMobileScreen = useMobileScreen();
useEffect(() => { useEffect(() => {
const show = isVisionModel(currentModel); const show = isVisionModel(currentModel);
setShowUploadImage(show); setShowUploadImage(show);
@ -528,8 +539,8 @@ export function ChatActions(props: {
// if current model is not available // if current model is not available
// switch to first available model // switch to first available model
const isUnavaliableModel = !models.some((m) => m.name === currentModel); const isUnavailableModel = !models.some((m) => m.name === currentModel);
if (isUnavaliableModel && models.length > 0) { if (isUnavailableModel && models.length > 0) {
// show next model to default model if exist // show next model to default model if exist
let nextModel = models.find((model) => model.isDefault) || models[0]; let nextModel = models.find((model) => model.isDefault) || models[0];
chatStore.updateCurrentSession((session) => { chatStore.updateCurrentSession((session) => {
@ -671,7 +682,7 @@ export function ChatActions(props: {
items={models.map((m) => ({ items={models.map((m) => ({
title: `${m.displayName}${ title: `${m.displayName}${
m?.provider?.providerName m?.provider?.providerName
? "(" + m?.provider?.providerName + ")" ? " (" + m?.provider?.providerName + ")"
: "" : ""
}`, }`,
value: `${m.name}@${m?.provider?.providerName}`, value: `${m.name}@${m?.provider?.providerName}`,
@ -780,34 +791,44 @@ export function ChatActions(props: {
/> />
)} )}
<ChatAction {showPlugins(currentProviderName, currentModel) && (
onClick={() => setShowPluginSelector(true)} <ChatAction
text={Locale.Plugin.Name} onClick={() => {
icon={<PluginIcon />} if (pluginStore.getAll().length == 0) {
/> navigate(Path.Plugins);
} else {
setShowPluginSelector(true);
}
}}
text={Locale.Plugin.Name}
icon={<PluginIcon />}
/>
)}
{showPluginSelector && ( {showPluginSelector && (
<Selector <Selector
multiple multiple
defaultSelectedValue={chatStore.currentSession().mask?.plugin} defaultSelectedValue={chatStore.currentSession().mask?.plugin}
items={[ items={pluginStore.getAll().map((item) => ({
{ title: `${item?.title}@${item?.version}`,
title: Locale.Plugin.Artifacts, value: item?.id,
value: Plugin.Artifacts, }))}
},
]}
onClose={() => setShowPluginSelector(false)} onClose={() => setShowPluginSelector(false)}
onSelection={(s) => { onSelection={(s) => {
const plugin = s[0];
chatStore.updateCurrentSession((session) => { chatStore.updateCurrentSession((session) => {
session.mask.plugin = s; session.mask.plugin = s as string[];
}); });
if (plugin) {
showToast(plugin);
}
}} }}
/> />
)} )}
{!isMobileScreen && (
<ChatAction
onClick={() => props.setShowShortcutKeyModal(true)}
text={Locale.Chat.ShortcutKey.Title}
icon={<ShortcutkeyIcon />}
/>
)}
{config.sttConfig.enable && ( {config.sttConfig.enable && (
<ChatAction <ChatAction
onClick={async () => onClick={async () =>
@ -891,6 +912,67 @@ export function DeleteImageButton(props: { deleteImage: () => void }) {
); );
} }
export function ShortcutKeyModal(props: { onClose: () => void }) {
const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0;
const shortcuts = [
{
title: Locale.Chat.ShortcutKey.newChat,
keys: isMac ? ["⌘", "Shift", "O"] : ["Ctrl", "Shift", "O"],
},
{ title: Locale.Chat.ShortcutKey.focusInput, keys: ["Shift", "Esc"] },
{
title: Locale.Chat.ShortcutKey.copyLastCode,
keys: isMac ? ["⌘", "Shift", ";"] : ["Ctrl", "Shift", ";"],
},
{
title: Locale.Chat.ShortcutKey.copyLastMessage,
keys: isMac ? ["⌘", "Shift", "C"] : ["Ctrl", "Shift", "C"],
},
{
title: Locale.Chat.ShortcutKey.showShortcutKey,
keys: isMac ? ["⌘", "/"] : ["Ctrl", "/"],
},
];
return (
<div className="modal-mask">
<Modal
title={Locale.Chat.ShortcutKey.Title}
onClose={props.onClose}
actions={[
<IconButton
type="primary"
text={Locale.UI.Confirm}
icon={<ConfirmIcon />}
key="ok"
onClick={() => {
props.onClose();
}}
/>,
]}
>
<div className={styles["shortcut-key-container"]}>
<div className={styles["shortcut-key-grid"]}>
{shortcuts.map((shortcut, index) => (
<div key={index} className={styles["shortcut-key-item"]}>
<div className={styles["shortcut-key-title"]}>
{shortcut.title}
</div>
<div className={styles["shortcut-key-keys"]}>
{shortcut.keys.map((key, i) => (
<div key={i} className={styles["shortcut-key"]}>
<span>{key}</span>
</div>
))}
</div>
</div>
))}
</div>
</div>
</Modal>
</div>
);
}
function _Chat() { function _Chat() {
type RenderMessage = ChatMessage & { preview?: boolean }; type RenderMessage = ChatMessage & { preview?: boolean };
@ -1003,7 +1085,7 @@ function _Chat() {
.onUserInput(userInput, attachImages) .onUserInput(userInput, attachImages)
.then(() => setIsLoading(false)); .then(() => setIsLoading(false));
setAttachImages([]); setAttachImages([]);
localStorage.setItem(LAST_INPUT_KEY, userInput); chatStore.setLastInput(userInput);
setUserInput(""); setUserInput("");
setPromptHints([]); setPromptHints([]);
if (!isMobileScreen) inputRef.current?.focus(); if (!isMobileScreen) inputRef.current?.focus();
@ -1069,7 +1151,7 @@ function _Chat() {
userInput.length <= 0 && userInput.length <= 0 &&
!(e.metaKey || e.altKey || e.ctrlKey) !(e.metaKey || e.altKey || e.ctrlKey)
) { ) {
setUserInput(localStorage.getItem(LAST_INPUT_KEY) ?? ""); setUserInput(chatStore.lastInput ?? "");
e.preventDefault(); e.preventDefault();
return; return;
} }
@ -1480,6 +1562,70 @@ function _Chat() {
setAttachImages(images); setAttachImages(images);
} }
// 快捷键 shortcut keys
const [showShortcutKeyModal, setShowShortcutKeyModal] = useState(false);
useEffect(() => {
const handleKeyDown = (event: any) => {
// 打开新聊天 command + shift + o
if (
(event.metaKey || event.ctrlKey) &&
event.shiftKey &&
event.key.toLowerCase() === "o"
) {
event.preventDefault();
setTimeout(() => {
chatStore.newSession();
navigate(Path.Chat);
}, 10);
}
// 聚焦聊天输入 shift + esc
else if (event.shiftKey && event.key.toLowerCase() === "escape") {
event.preventDefault();
inputRef.current?.focus();
}
// 复制最后一个代码块 command + shift + ;
else if (
(event.metaKey || event.ctrlKey) &&
event.shiftKey &&
event.code === "Semicolon"
) {
event.preventDefault();
const copyCodeButton =
document.querySelectorAll<HTMLElement>(".copy-code-button");
if (copyCodeButton.length > 0) {
copyCodeButton[copyCodeButton.length - 1].click();
}
}
// 复制最后一个回复 command + shift + c
else if (
(event.metaKey || event.ctrlKey) &&
event.shiftKey &&
event.key.toLowerCase() === "c"
) {
event.preventDefault();
const lastNonUserMessage = messages
.filter((message) => message.role !== "user")
.pop();
if (lastNonUserMessage) {
const lastMessageContent = getMessageTextContent(lastNonUserMessage);
copyToClipboard(lastMessageContent);
}
}
// 展示快捷键 command + /
else if ((event.metaKey || event.ctrlKey) && event.key === "/") {
event.preventDefault();
setShowShortcutKeyModal(true);
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [messages, chatStore, navigate]);
return ( return (
<div className={styles.chat} key={session.id}> <div className={styles.chat} key={session.id}>
<div className="window-header" data-tauri-drag-region> <div className="window-header" data-tauri-drag-region>
@ -1508,6 +1654,17 @@ function _Chat() {
</div> </div>
</div> </div>
<div className="window-actions"> <div className="window-actions">
<div className="window-action-button">
<IconButton
icon={<ReloadIcon />}
bordered
title={Locale.Chat.Actions.RefreshTitle}
onClick={() => {
showToast(Locale.Chat.Actions.RefreshToast);
chatStore.summarizeSession(true);
}}
/>
</div>
{!isMobileScreen && ( {!isMobileScreen && (
<div className="window-action-button"> <div className="window-action-button">
<IconButton <IconButton
@ -1704,11 +1861,31 @@ function _Chat() {
</div> </div>
)} )}
</div> </div>
{showTyping && ( {message?.tools?.length == 0 && showTyping && (
<div className={styles["chat-message-status"]}> <div className={styles["chat-message-status"]}>
{Locale.Chat.Typing} {Locale.Chat.Typing}
</div> </div>
)} )}
{/*@ts-ignore*/}
{message?.tools?.length > 0 && (
<div className={styles["chat-message-tools"]}>
{message?.tools?.map((tool) => (
<div
key={tool.id}
className={styles["chat-message-tool"]}
>
{tool.isError === false ? (
<ConfirmIcon />
) : tool.isError === true ? (
<CloseIcon />
) : (
<LoadingButtonIcon />
)}
<span>{tool?.function?.name}</span>
</div>
))}
</div>
)}
<div className={styles["chat-message-item"]}> <div className={styles["chat-message-item"]}>
<Markdown <Markdown
key={message.streaming ? "loading" : "done"} key={message.streaming ? "loading" : "done"}
@ -1718,7 +1895,7 @@ function _Chat() {
message.content.length === 0 && message.content.length === 0 &&
!isUser !isUser
} }
onContextMenu={(e) => onRightClick(e, message)} // onContextMenu={(e) => onRightClick(e, message)} // hard to use
onDoubleClickCapture={() => { onDoubleClickCapture={() => {
if (!isMobileScreen) return; if (!isMobileScreen) return;
setUserInput(getMessageTextContent(message)); setUserInput(getMessageTextContent(message));
@ -1795,6 +1972,7 @@ function _Chat() {
setUserInput("/"); setUserInput("/");
onSearch(""); onSearch("");
}} }}
setShowShortcutKeyModal={setShowShortcutKeyModal}
setUserInput={setUserInput} setUserInput={setUserInput}
/> />
<label <label
@ -1867,6 +2045,10 @@ function _Chat() {
}} }}
/> />
)} )}
{showShortcutKeyModal && (
<ShortcutKeyModal onClose={() => setShowShortcutKeyModal(false)} />
)}
</div> </div>
); );
} }

View File

@ -36,7 +36,8 @@ export function Avatar(props: { model?: ModelType; avatar?: string }) {
if (props.model) { if (props.model) {
return ( return (
<div className="no-dark"> <div className="no-dark">
{props.model?.startsWith("gpt-4") ? ( {props.model?.startsWith("gpt-4") ||
props.model?.startsWith("chatgpt-4o") ? (
<BlackBotIcon className="user-avatar" /> <BlackBotIcon className="user-avatar" />
) : ( ) : (
<BotIcon className="user-avatar" /> <BotIcon className="user-avatar" />

View File

@ -8,6 +8,7 @@ import { ISSUE_URL } from "../constant";
import Locale from "../locales"; import Locale from "../locales";
import { showConfirm } from "./ui-lib"; import { showConfirm } from "./ui-lib";
import { useSyncStore } from "../store/sync"; import { useSyncStore } from "../store/sync";
import { useChatStore } from "../store/chat";
interface IErrorBoundaryState { interface IErrorBoundaryState {
hasError: boolean; hasError: boolean;
@ -30,8 +31,7 @@ export class ErrorBoundary extends React.Component<any, IErrorBoundaryState> {
try { try {
useSyncStore.getState().export(); useSyncStore.getState().export();
} finally { } finally {
localStorage.clear(); useChatStore.getState().clearAllData();
location.reload();
} }
} }

View File

@ -59,6 +59,10 @@ const MaskPage = dynamic(async () => (await import("./mask")).MaskPage, {
loading: () => <Loading noLogo />, loading: () => <Loading noLogo />,
}); });
const PluginPage = dynamic(async () => (await import("./plugin")).PluginPage, {
loading: () => <Loading noLogo />,
});
const SearchChat = dynamic( const SearchChat = dynamic(
async () => (await import("./search-chat")).SearchChatPage, async () => (await import("./search-chat")).SearchChatPage,
{ {
@ -181,6 +185,7 @@ function Screen() {
<Route path={Path.Home} element={<Chat />} /> <Route path={Path.Home} element={<Chat />} />
<Route path={Path.NewChat} element={<NewChat />} /> <Route path={Path.NewChat} element={<NewChat />} />
<Route path={Path.Masks} element={<MaskPage />} /> <Route path={Path.Masks} element={<MaskPage />} />
<Route path={Path.Plugins} element={<PluginPage />} />
<Route path={Path.SearchChat} element={<SearchChat />} /> <Route path={Path.SearchChat} element={<SearchChat />} />
<Route path={Path.Chat} element={<Chat />} /> <Route path={Path.Chat} element={<Chat />} />
<Route path={Path.Settings} element={<Settings />} /> <Route path={Path.Settings} element={<Settings />} />

View File

@ -19,7 +19,6 @@ import {
HTMLPreview, HTMLPreview,
HTMLPreviewHander, HTMLPreviewHander,
} from "./artifacts"; } from "./artifacts";
import { Plugin } from "../constant";
import { useChatStore } from "../store"; import { useChatStore } from "../store";
import { IconButton } from "./button"; import { IconButton } from "./button";
@ -77,7 +76,6 @@ export function PreCode(props: { children: any }) {
const { height } = useWindowSize(); const { height } = useWindowSize();
const chatStore = useChatStore(); const chatStore = useChatStore();
const session = chatStore.currentSession(); const session = chatStore.currentSession();
const plugins = session.mask?.plugin;
const renderArtifacts = useDebouncedCallback(() => { const renderArtifacts = useDebouncedCallback(() => {
if (!ref.current) return; if (!ref.current) return;
@ -94,10 +92,7 @@ export function PreCode(props: { children: any }) {
} }
}, 600); }, 600);
const enableArtifacts = useMemo( const enableArtifacts = session.mask?.enableArtifacts !== false;
() => plugins?.includes(Plugin.Artifacts),
[plugins],
);
//Wrap the paragraph for plain-text //Wrap the paragraph for plain-text
useEffect(() => { useEffect(() => {
@ -168,7 +163,7 @@ export function PreCode(props: { children: any }) {
); );
} }
function CustomCode(props: { children: any }) { function CustomCode(props: { children: any; className?: string }) {
const ref = useRef<HTMLPreElement>(null); const ref = useRef<HTMLPreElement>(null);
const [collapsed, setCollapsed] = useState(true); const [collapsed, setCollapsed] = useState(true);
const [showToggle, setShowToggle] = useState(false); const [showToggle, setShowToggle] = useState(false);
@ -187,6 +182,7 @@ function CustomCode(props: { children: any }) {
return ( return (
<> <>
<code <code
className={props?.className}
ref={ref} ref={ref}
style={{ style={{
maxHeight: collapsed ? "400px" : "none", maxHeight: collapsed ? "400px" : "none",
@ -241,9 +237,26 @@ function escapeBrackets(text: string) {
); );
} }
function tryWrapHtmlCode(text: string) {
// try add wrap html code (fixed: html codeblock include 2 newline)
return text
.replace(
/([`]*?)(\w*?)([\n\r]*?)(<!DOCTYPE html>)/g,
(match, quoteStart, lang, newLine, doctype) => {
return !quoteStart ? "\n```html\n" + doctype : match;
},
)
.replace(
/(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*?)([`]*?)([\n\r]*?)/g,
(match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match;
},
);
}
function _MarkDownContent(props: { content: string }) { function _MarkDownContent(props: { content: string }) {
const escapedContent = useMemo(() => { const escapedContent = useMemo(() => {
return escapeBrackets(escapeDollarNumber(props.content)); return tryWrapHtmlCode(escapeBrackets(escapeDollarNumber(props.content)));
}, [props.content]); }, [props.content]);
return ( return (

View File

@ -167,6 +167,22 @@ export function MaskConfig(props: {
></input> ></input>
</ListItem> </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>
{!props.shouldSyncFromGlobal ? ( {!props.shouldSyncFromGlobal ? (
<ListItem <ListItem
title={Locale.Mask.Config.Share.Title} title={Locale.Mask.Config.Share.Title}
@ -410,16 +426,7 @@ export function MaskPage() {
const maskStore = useMaskStore(); const maskStore = useMaskStore();
const chatStore = useChatStore(); const chatStore = useChatStore();
const [filterLang, setFilterLang] = useState<Lang | undefined>( const filterLang = maskStore.language;
() => localStorage.getItem("Mask-language") as Lang | undefined,
);
useEffect(() => {
if (filterLang) {
localStorage.setItem("Mask-language", filterLang);
} else {
localStorage.removeItem("Mask-language");
}
}, [filterLang]);
const allMasks = maskStore const allMasks = maskStore
.getAll() .getAll()
@ -526,9 +533,9 @@ export function MaskPage() {
onChange={(e) => { onChange={(e) => {
const value = e.currentTarget.value; const value = e.currentTarget.value;
if (value === Locale.Settings.Lang.All) { if (value === Locale.Settings.Lang.All) {
setFilterLang(undefined); maskStore.setLanguage(undefined);
} else { } else {
setFilterLang(value as Lang); maskStore.setLanguage(value as Lang);
} }
}} }}
> >

View File

@ -5,13 +5,19 @@ import Locale from "../locales";
import { InputRange } from "./input-range"; import { InputRange } from "./input-range";
import { ListItem, Select } from "./ui-lib"; import { ListItem, Select } from "./ui-lib";
import { useAllModels } from "../utils/hooks"; import { useAllModels } from "../utils/hooks";
import { groupBy } from "lodash-es";
export function ModelConfigList(props: { export function ModelConfigList(props: {
modelConfig: ModelConfig; modelConfig: ModelConfig;
updateConfig: (updater: (config: ModelConfig) => void) => void; updateConfig: (updater: (config: ModelConfig) => void) => void;
}) { }) {
const allModels = useAllModels(); const allModels = useAllModels();
const groupModels = groupBy(
allModels.filter((v) => v.available),
"provider.providerName",
);
const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`; const value = `${props.modelConfig.model}@${props.modelConfig?.providerName}`;
const compressModelValue = `${props.modelConfig.compressModel}@${props.modelConfig?.compressProviderName}`;
return ( return (
<> <>
@ -19,6 +25,7 @@ export function ModelConfigList(props: {
<Select <Select
aria-label={Locale.Settings.Model} aria-label={Locale.Settings.Model}
value={value} value={value}
align="left"
onChange={(e) => { onChange={(e) => {
const [model, providerName] = e.currentTarget.value.split("@"); const [model, providerName] = e.currentTarget.value.split("@");
props.updateConfig((config) => { props.updateConfig((config) => {
@ -27,13 +34,15 @@ export function ModelConfigList(props: {
}); });
}} }}
> >
{allModels {Object.keys(groupModels).map((providerName, index) => (
.filter((v) => v.available) <optgroup label={providerName} key={index}>
.map((v, i) => ( {groupModels[providerName].map((v, i) => (
<option value={`${v.name}@${v.provider?.providerName}`} key={i}> <option value={`${v.name}@${v.provider?.providerName}`} key={i}>
{v.displayName}({v.provider?.providerName}) {v.displayName}
</option> </option>
))} ))}
</optgroup>
))}
</Select> </Select>
</ListItem> </ListItem>
<ListItem <ListItem
@ -228,6 +237,30 @@ export function ModelConfigList(props: {
} }
></input> ></input>
</ListItem> </ListItem>
<ListItem
title={Locale.Settings.CompressModel.Title}
subTitle={Locale.Settings.CompressModel.SubTitle}
>
<Select
aria-label={Locale.Settings.CompressModel.Title}
value={compressModelValue}
onChange={(e) => {
const [model, providerName] = e.currentTarget.value.split("@");
props.updateConfig((config) => {
config.compressModel = ModalConfigValidator.model(model);
config.compressProviderName = providerName as ServiceProvider;
});
}}
>
{allModels
.filter((v) => v.available)
.map((v, i) => (
<option value={`${v.name}@${v.provider?.providerName}`} key={i}>
{v.displayName}({v.provider?.providerName})
</option>
))}
</Select>
</ListItem>
</> </>
); );
} }

View File

@ -0,0 +1,16 @@
.plugin-title {
font-weight: bolder;
font-size: 16px;
margin: 10px 0;
}
.plugin-content {
font-size: 14px;
font-family: inherit;
pre code {
max-height: 240px;
overflow-y: auto;
white-space: pre-wrap;
min-width: 300px;
}
}

393
app/components/plugin.tsx Normal file
View File

@ -0,0 +1,393 @@
import { useDebouncedCallback } from "use-debounce";
import OpenAPIClientAxios from "openapi-client-axios";
import yaml from "js-yaml";
import { PLUGINS_REPO_URL } from "../constant";
import { IconButton } from "./button";
import { ErrorBoundary } from "./error";
import styles from "./mask.module.scss";
import pluginStyles from "./plugin.module.scss";
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";
import { Plugin, usePluginStore, FunctionToolService } from "../store/plugin";
import {
PasswordInput,
List,
ListItem,
Modal,
showConfirm,
showToast,
} from "./ui-lib";
import Locale from "../locales";
import { useNavigate } from "react-router-dom";
import { useEffect, useState } from "react";
import { getClientConfig } from "../config/client";
export function PluginPage() {
const navigate = useNavigate();
const pluginStore = usePluginStore();
const allPlugins = pluginStore.getAll();
const [searchPlugins, setSearchPlugins] = useState<Plugin[]>([]);
const [searchText, setSearchText] = useState("");
const plugins = searchText.length > 0 ? searchPlugins : allPlugins;
// refactored already, now it accurate
const onSearch = (text: string) => {
setSearchText(text);
if (text.length > 0) {
const result = allPlugins.filter(
(m) => m?.title.toLowerCase().includes(text.toLowerCase()),
);
setSearchPlugins(result);
} else {
setSearchPlugins(allPlugins);
}
};
const [editingPluginId, setEditingPluginId] = useState<string | undefined>();
const editingPlugin = pluginStore.get(editingPluginId);
const editingPluginTool = FunctionToolService.get(editingPlugin?.id);
const closePluginModal = () => setEditingPluginId(undefined);
const onChangePlugin = useDebouncedCallback((editingPlugin, e) => {
const content = e.target.innerText;
try {
const api = new OpenAPIClientAxios({
definition: yaml.load(content) as any,
});
api
.init()
.then(() => {
if (content != editingPlugin.content) {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.content = content;
const tool = FunctionToolService.add(plugin, true);
plugin.title = tool.api.definition.info.title;
plugin.version = tool.api.definition.info.version;
});
}
})
.catch((e) => {
console.error(e);
showToast(Locale.Plugin.EditModal.Error);
});
} catch (e) {
console.error(e);
showToast(Locale.Plugin.EditModal.Error);
}
}, 100).bind(null, editingPlugin);
const [loadUrl, setLoadUrl] = useState<string>("");
const loadFromUrl = (loadUrl: string) =>
fetch(loadUrl)
.catch((e) => {
const p = new URL(loadUrl);
return fetch(`/api/proxy/${p.pathname}?${p.search}`, {
headers: {
"X-Base-URL": p.origin,
},
});
})
.then((res) => res.text())
.then((content) => {
try {
return JSON.stringify(JSON.parse(content), null, " ");
} catch (e) {
return content;
}
})
.then((content) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.content = content;
const tool = FunctionToolService.add(plugin, true);
plugin.title = tool.api.definition.info.title;
plugin.version = tool.api.definition.info.version;
});
})
.catch((e) => {
showToast(Locale.Plugin.EditModal.Error);
});
return (
<ErrorBoundary>
<div className={styles["mask-page"]}>
<div className="window-header">
<div className="window-header-title">
<div className="window-header-main-title">
{Locale.Plugin.Page.Title}
</div>
<div className="window-header-submai-title">
{Locale.Plugin.Page.SubTitle(plugins.length)}
</div>
</div>
<div className="window-actions">
<div className="window-action-button">
<a
href={PLUGINS_REPO_URL}
target="_blank"
rel="noopener noreferrer"
>
<IconButton icon={<GithubIcon />} bordered />
</a>
</div>
<div className="window-action-button">
<IconButton
icon={<CloseIcon />}
bordered
onClick={() => navigate(-1)}
/>
</div>
</div>
</div>
<div className={styles["mask-page-body"]}>
<div className={styles["mask-filter"]}>
<input
type="text"
className={styles["search-bar"]}
placeholder={Locale.Plugin.Page.Search}
autoFocus
onInput={(e) => onSearch(e.currentTarget.value)}
/>
<IconButton
className={styles["mask-create"]}
icon={<AddIcon />}
text={Locale.Plugin.Page.Create}
bordered
onClick={() => {
const createdPlugin = pluginStore.create();
setEditingPluginId(createdPlugin.id);
}}
/>
</div>
<div>
{plugins.length == 0 && (
<div
style={{
display: "flex",
margin: "60px auto",
alignItems: "center",
justifyContent: "center",
}}
>
{Locale.Plugin.Page.Find}
<a
href={PLUGINS_REPO_URL}
target="_blank"
rel="noopener noreferrer"
style={{ marginLeft: 16 }}
>
<IconButton icon={<GithubIcon />} bordered />
</a>
</div>
)}
{plugins.map((m) => (
<div className={styles["mask-item"]} key={m.id}>
<div className={styles["mask-header"]}>
<div className={styles["mask-icon"]}></div>
<div className={styles["mask-title"]}>
<div className={styles["mask-name"]}>
{m.title}@<small>{m.version}</small>
</div>
<div className={styles["mask-info"] + " one-line"}>
{Locale.Plugin.Item.Info(
FunctionToolService.add(m).length,
)}
</div>
</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)}
/>
)}
{!m.builtin && (
<IconButton
icon={<DeleteIcon />}
text={Locale.Plugin.Item.Delete}
onClick={async () => {
if (
await showConfirm(Locale.Plugin.Item.DeleteConfirm)
) {
pluginStore.delete(m.id);
}
}}
/>
)}
</div>
</div>
))}
</div>
</div>
</div>
{editingPlugin && (
<div className="modal-mask">
<Modal
title={Locale.Plugin.EditModal.Title(editingPlugin?.builtin)}
onClose={closePluginModal}
actions={[
<IconButton
icon={<ConfirmIcon />}
text={Locale.UI.Confirm}
key="export"
bordered
onClick={() => setEditingPluginId("")}
/>,
]}
>
<List>
<ListItem title={Locale.Plugin.EditModal.Auth}>
<select
value={editingPlugin?.authType}
onChange={(e) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.authType = e.target.value;
});
}}
>
<option value="">{Locale.Plugin.Auth.None}</option>
<option value="bearer">{Locale.Plugin.Auth.Bearer}</option>
<option value="basic">{Locale.Plugin.Auth.Basic}</option>
<option value="custom">{Locale.Plugin.Auth.Custom}</option>
</select>
</ListItem>
{["bearer", "basic", "custom"].includes(
editingPlugin.authType as string,
) && (
<ListItem title={Locale.Plugin.Auth.Location}>
<select
value={editingPlugin?.authLocation}
onChange={(e) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.authLocation = e.target.value;
});
}}
>
<option value="header">
{Locale.Plugin.Auth.LocationHeader}
</option>
<option value="query">
{Locale.Plugin.Auth.LocationQuery}
</option>
<option value="body">
{Locale.Plugin.Auth.LocationBody}
</option>
</select>
</ListItem>
)}
{editingPlugin.authType == "custom" && (
<ListItem title={Locale.Plugin.Auth.CustomHeader}>
<input
type="text"
value={editingPlugin?.authHeader}
onChange={(e) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.authHeader = e.target.value;
});
}}
></input>
</ListItem>
)}
{["bearer", "basic", "custom"].includes(
editingPlugin.authType as string,
) && (
<ListItem title={Locale.Plugin.Auth.Token}>
<PasswordInput
type="text"
value={editingPlugin?.authToken}
onChange={(e) => {
pluginStore.updatePlugin(editingPlugin.id, (plugin) => {
plugin.authToken = e.currentTarget.value;
});
}}
></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" }}>
<input
type="text"
style={{ minWidth: 200, marginRight: 20 }}
onInput={(e) => setLoadUrl(e.currentTarget.value)}
></input>
<IconButton
icon={<ReloadIcon />}
text={Locale.Plugin.EditModal.Load}
bordered
onClick={() => loadFromUrl(loadUrl)}
/>
</div>
</ListItem>
<ListItem
subTitle={
<div
className={`markdown-body ${pluginStyles["plugin-content"]}`}
dir="auto"
>
<pre>
<code
contentEditable={true}
dangerouslySetInnerHTML={{
__html: editingPlugin.content,
}}
onBlur={onChangePlugin}
></code>
</pre>
</div>
}
></ListItem>
{editingPluginTool?.tools.map((tool, index) => (
<ListItem
key={index}
title={tool?.function?.name}
subTitle={tool?.function?.description}
/>
))}
</List>
</Modal>
</div>
)}
</ErrorBoundary>
);
}

View File

@ -252,6 +252,12 @@
position: relative; position: relative;
max-width: fit-content; max-width: fit-content;
&.left-align-option {
option {
text-align: left;
}
}
.select-with-icon-select { .select-with-icon-select {
height: 100%; height: 100%;
border: var(--border-in-light); border: var(--border-in-light);

View File

@ -50,8 +50,8 @@ export function Card(props: { children: JSX.Element[]; className?: string }) {
} }
export function ListItem(props: { export function ListItem(props: {
title: string; title?: string;
subTitle?: string; subTitle?: string | JSX.Element;
children?: JSX.Element | JSX.Element[]; children?: JSX.Element | JSX.Element[];
icon?: JSX.Element; icon?: JSX.Element;
className?: string; className?: string;
@ -292,13 +292,19 @@ export function PasswordInput(
export function Select( export function Select(
props: React.DetailedHTMLProps< props: React.DetailedHTMLProps<
React.SelectHTMLAttributes<HTMLSelectElement>, React.SelectHTMLAttributes<HTMLSelectElement> & {
align?: "left" | "center";
},
HTMLSelectElement HTMLSelectElement
>, >,
) { ) {
const { className, children, ...otherProps } = props; const { className, children, align, ...otherProps } = props;
return ( return (
<div className={`${styles["select-with-icon"]} ${className}`}> <div
className={`${styles["select-with-icon"]} ${
align === "left" ? styles["left-align-option"] : ""
} ${className}`}
>
<select className={styles["select-with-icon-select"]} {...otherProps}> <select className={styles["select-with-icon-select"]} {...otherProps}>
{children} {children}
</select> </select>

View File

@ -120,12 +120,15 @@ export const getServerSideConfig = () => {
if (disableGPT4) { if (disableGPT4) {
if (customModels) customModels += ","; if (customModels) customModels += ",";
customModels += DEFAULT_MODELS.filter( customModels += DEFAULT_MODELS.filter(
(m) => m.name.startsWith("gpt-4") && !m.name.startsWith("gpt-4o-mini"), (m) =>
(m.name.startsWith("gpt-4") || m.name.startsWith("chatgpt-4o")) &&
!m.name.startsWith("gpt-4o-mini"),
) )
.map((m) => "-" + m.name) .map((m) => "-" + m.name)
.join(","); .join(",");
if ( if (
defaultModel.startsWith("gpt-4") && (defaultModel.startsWith("gpt-4") ||
defaultModel.startsWith("chatgpt-4o")) &&
!defaultModel.startsWith("gpt-4o-mini") !defaultModel.startsWith("gpt-4o-mini")
) )
defaultModel = ""; defaultModel = "";

View File

@ -3,6 +3,7 @@ import path from "path";
export const OWNER = "ChatGPTNextWeb"; export const OWNER = "ChatGPTNextWeb";
export const REPO = "ChatGPT-Next-Web"; export const REPO = "ChatGPT-Next-Web";
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`; export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
export const PLUGINS_REPO_URL = `https://github.com/${OWNER}/NextChat-Awesome-Plugins`;
export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`; export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
export const UPDATE_URL = `${REPO_URL}#keep-updated`; export const UPDATE_URL = `${REPO_URL}#keep-updated`;
export const RELEASE_URL = `${REPO_URL}/releases`; export const RELEASE_URL = `${REPO_URL}/releases`;
@ -39,6 +40,7 @@ export enum Path {
Settings = "/settings", Settings = "/settings",
NewChat = "/new-chat", NewChat = "/new-chat",
Masks = "/masks", Masks = "/masks",
Plugins = "/plugins",
Auth = "/auth", Auth = "/auth",
Sd = "/sd", Sd = "/sd",
SdNew = "/sd-new", SdNew = "/sd-new",
@ -72,12 +74,9 @@ export enum FileName {
Prompts = "prompts.json", Prompts = "prompts.json",
} }
export enum Plugin {
Artifacts = "artifacts",
}
export enum StoreKey { export enum StoreKey {
Chat = "chat-next-web-store", Chat = "chat-next-web-store",
Plugin = "chat-next-web-plugin",
Access = "access-control", Access = "access-control",
Config = "app-config", Config = "app-config",
Mask = "mask-store", Mask = "mask-store",
@ -249,9 +248,12 @@ export const KnowledgeCutOffDate: Record<string, string> = {
"gpt-4o": "2023-10", "gpt-4o": "2023-10",
"gpt-4o-2024-05-13": "2023-10", "gpt-4o-2024-05-13": "2023-10",
"gpt-4o-2024-08-06": "2023-10", "gpt-4o-2024-08-06": "2023-10",
"chatgpt-4o-latest": "2023-10",
"gpt-4o-mini": "2023-10", "gpt-4o-mini": "2023-10",
"gpt-4o-mini-2024-07-18": "2023-10", "gpt-4o-mini-2024-07-18": "2023-10",
"gpt-4-vision-preview": "2023-04", "gpt-4-vision-preview": "2023-04",
"o1-mini": "2023-10",
"o1-preview": "2023-10",
// After improvements, // After improvements,
// it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously. // it's now easier to add "KnowledgeCutOffDate" instead of stupid hardcoding it, as was done previously.
"gemini-pro": "2023-12", "gemini-pro": "2023-12",
@ -289,12 +291,15 @@ const openaiModels = [
"gpt-4o", "gpt-4o",
"gpt-4o-2024-05-13", "gpt-4o-2024-05-13",
"gpt-4o-2024-08-06", "gpt-4o-2024-08-06",
"chatgpt-4o-latest",
"gpt-4o-mini", "gpt-4o-mini",
"gpt-4o-mini-2024-07-18", "gpt-4o-mini-2024-07-18",
"gpt-4-vision-preview", "gpt-4-vision-preview",
"gpt-4-turbo-2024-04-09", "gpt-4-turbo-2024-04-09",
"gpt-4-1106-preview", "gpt-4-1106-preview",
"dall-e-3", "dall-e-3",
"o1-mini",
"o1-preview",
]; ];
const googleModels = [ const googleModels = [
@ -499,6 +504,7 @@ export const internalAllowedWebDavEndpoints = [
export const DEFAULT_GA_ID = "G-89WN60ZK2E"; export const DEFAULT_GA_ID = "G-89WN60ZK2E";
export const PLUGINS = [ export const PLUGINS = [
{ name: "Plugins", path: Path.Plugins },
{ name: "Stable Diffusion", path: Path.Sd }, { name: "Stable Diffusion", path: Path.Sd },
{ name: "Search Chat", path: Path.SearchChat }, { name: "Search Chat", path: Path.SearchChat },
]; ];

8
app/global.d.ts vendored
View File

@ -21,10 +21,16 @@ declare interface Window {
writeBinaryFile(path: string, data: Uint8Array): Promise<void>; writeBinaryFile(path: string, data: Uint8Array): Promise<void>;
writeTextFile(path: string, data: string): Promise<void>; writeTextFile(path: string, data: string): Promise<void>;
}; };
notification:{ notification: {
requestPermission(): Promise<Permission>; requestPermission(): Promise<Permission>;
isPermissionGranted(): Promise<boolean>; isPermissionGranted(): Promise<boolean>;
sendNotification(options: string | Options): void; sendNotification(options: string | Options): void;
}; };
http: {
fetch<T>(
url: string,
options?: Record<string, unknown>,
): Promise<Response<T>>;
};
}; };
} }

View File

@ -0,0 +1 @@
<?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="M42 7H6C4.89543 7 4 7.89543 4 9V37C4 38.1046 4.89543 39 6 39H42C43.1046 39 44 38.1046 44 37V9C44 7.89543 43.1046 7 42 7Z" fill="none" stroke="#000" stroke-width="3" stroke-linejoin="round"/><path d="M12 19H14" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M21 19H23" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M29 19H36" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 28H36" stroke="#000" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>

After

Width:  |  Height:  |  Size: 734 B

View File

@ -41,7 +41,11 @@ export default function RootLayout({
name="viewport" name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"
/> />
<link rel="manifest" href="/site.webmanifest"></link> <link
rel="manifest"
href="/site.webmanifest"
crossOrigin="use-credentials"
></link>
<script src="/serviceWorkerRegister.js" defer></script> <script src="/serviceWorkerRegister.js" defer></script>
</head> </head>
<body> <body>

View File

@ -43,6 +43,8 @@ const ar: PartialLocaleType = {
PinToastAction: "عرض", PinToastAction: "عرض",
Delete: "حذف", Delete: "حذف",
Edit: "تحرير", Edit: "تحرير",
RefreshTitle: "تحديث العنوان",
RefreshToast: "تم إرسال طلب تحديث العنوان",
}, },
Commands: { Commands: {
new: "دردشة جديدة", new: "دردشة جديدة",
@ -404,6 +406,10 @@ const ar: PartialLocaleType = {
}, },
Model: "النموذج", Model: "النموذج",
CompressModel: {
Title: "نموذج الضغط",
SubTitle: "النموذج المستخدم لضغط السجل التاريخي",
},
Temperature: { Temperature: {
Title: "العشوائية (temperature)", Title: "العشوائية (temperature)",
SubTitle: "كلما زادت القيمة، زادت العشوائية في الردود", SubTitle: "كلما زادت القيمة، زادت العشوائية في الردود",

View File

@ -43,6 +43,8 @@ const bn: PartialLocaleType = {
PinToastAction: "দেখুন", PinToastAction: "দেখুন",
Delete: "মুছে ফেলুন", Delete: "মুছে ফেলুন",
Edit: "সম্পাদনা করুন", Edit: "সম্পাদনা করুন",
RefreshTitle: "শিরোনাম রিফ্রেশ করুন",
RefreshToast: "শিরোনাম রিফ্রেশ অনুরোধ পাঠানো হয়েছে",
}, },
Commands: { Commands: {
new: "নতুন চ্যাট", new: "নতুন চ্যাট",
@ -411,6 +413,10 @@ const bn: PartialLocaleType = {
}, },
Model: "মডেল (model)", Model: "মডেল (model)",
CompressModel: {
Title: "সংকোচন মডেল",
SubTitle: "ইতিহাস সংকুচিত করার জন্য ব্যবহৃত মডেল",
},
Temperature: { Temperature: {
Title: "যাদুকরিতা (temperature)", Title: "যাদুকরিতা (temperature)",
SubTitle: "মান বাড়ালে উত্তর বেশি এলোমেলো হবে", SubTitle: "মান বাড়ালে উত্তর বেশি এলোমেলো হবে",

View File

@ -1,3 +1,4 @@
import { ShortcutKeyModal } from "../components/chat";
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
import { SubmitKey } from "../store/config"; import { SubmitKey } from "../store/config";
@ -43,6 +44,8 @@ const cn = {
Delete: "删除", Delete: "删除",
Edit: "编辑", Edit: "编辑",
FullScreen: "全屏", FullScreen: "全屏",
RefreshTitle: "刷新标题",
RefreshToast: "已发送刷新标题请求",
Speech: "朗读", Speech: "朗读",
StopSpeech: "停止", StopSpeech: "停止",
}, },
@ -85,6 +88,14 @@ const cn = {
SaveAs: "存为面具", SaveAs: "存为面具",
}, },
IsContext: "预设提示词", IsContext: "预设提示词",
ShortcutKey: {
Title: "键盘快捷方式",
newChat: "打开新聊天",
focusInput: "聚焦输入框",
copyLastMessage: "复制最后一个回复",
copyLastCode: "复制最后一个代码块",
showShortcutKey: "显示快捷方式",
},
}, },
Export: { Export: {
Title: "分享聊天记录", Title: "分享聊天记录",
@ -465,6 +476,10 @@ const cn = {
}, },
Model: "模型 (model)", Model: "模型 (model)",
CompressModel: {
Title: "压缩模型",
SubTitle: "用于压缩历史记录的模型",
},
Temperature: { Temperature: {
Title: "随机性 (temperature)", Title: "随机性 (temperature)",
SubTitle: "值越大,回复越随机", SubTitle: "值越大,回复越随机",
@ -529,8 +544,8 @@ const cn = {
}, },
}, },
Copy: { Copy: {
Success: "已写入剪板", Success: "已写入剪板",
Failed: "复制失败,请赋予剪板权限", Failed: "复制失败,请赋予剪板权限",
}, },
Download: { Download: {
Success: "内容已下载到您的目录。", Success: "内容已下载到您的目录。",
@ -543,10 +558,6 @@ const cn = {
Clear: "上下文已清除", Clear: "上下文已清除",
Revert: "恢复上下文", Revert: "恢复上下文",
}, },
Plugin: {
Name: "插件",
Artifacts: "Artifacts",
},
Discovery: { Discovery: {
Name: "发现", Name: "发现",
}, },
@ -568,6 +579,46 @@ const cn = {
View: "查看", View: "查看",
}, },
}, },
Plugin: {
Name: "插件",
Page: {
Title: "插件",
SubTitle: (count: number) => `${count} 个插件`,
Search: "搜索插件",
Create: "新建",
Find: "您可以在Github上找到优秀的插件",
},
Item: {
Info: (count: number) => `${count} 方法`,
View: "查看",
Edit: "编辑",
Delete: "删除",
DeleteConfirm: "确认删除?",
},
Auth: {
None: "不需要授权",
Basic: "Basic",
Bearer: "Bearer",
Custom: "自定义",
CustomHeader: "自定义参数名称",
Token: "Token",
Proxy: "使用代理",
ProxyDescription: "使用代理解决 CORS 错误",
Location: "位置",
LocationHeader: "Header",
LocationQuery: "Query",
LocationBody: "Body",
},
EditModal: {
Title: (readonly: boolean) => `编辑插件 ${readonly ? "(只读)" : ""}`,
Download: "下载",
Auth: "授权方式",
Content: "OpenAPI Schema",
Load: "从网页加载",
Method: "方法",
Error: "格式错误",
},
},
Mask: { Mask: {
Name: "面具", Name: "面具",
Page: { Page: {
@ -602,6 +653,10 @@ const cn = {
Title: "隐藏预设对话", Title: "隐藏预设对话",
SubTitle: "隐藏后预设对话不会出现在聊天界面", SubTitle: "隐藏后预设对话不会出现在聊天界面",
}, },
Artifacts: {
Title: "启用Artifacts",
SubTitle: "启用之后可以直接渲染HTML页面",
},
Share: { Share: {
Title: "分享此面具", Title: "分享此面具",
SubTitle: "生成此面具的直达链接", SubTitle: "生成此面具的直达链接",

View File

@ -43,6 +43,8 @@ const cs: PartialLocaleType = {
PinToastAction: "Zobrazit", PinToastAction: "Zobrazit",
Delete: "Smazat", Delete: "Smazat",
Edit: "Upravit", Edit: "Upravit",
RefreshTitle: "Obnovit název",
RefreshToast: "Požadavek na obnovení názvu byl odeslán",
}, },
Commands: { Commands: {
new: "Nová konverzace", new: "Nová konverzace",
@ -410,6 +412,10 @@ const cs: PartialLocaleType = {
}, },
Model: "Model (model)", Model: "Model (model)",
CompressModel: {
Title: "Kompresní model",
SubTitle: "Model používaný pro kompresi historie",
},
Temperature: { Temperature: {
Title: "Náhodnost (temperature)", Title: "Náhodnost (temperature)",
SubTitle: "Čím vyšší hodnota, tím náhodnější odpovědi", SubTitle: "Čím vyšší hodnota, tím náhodnější odpovědi",

View File

@ -43,6 +43,8 @@ const de: PartialLocaleType = {
PinToastAction: "Ansehen", PinToastAction: "Ansehen",
Delete: "Löschen", Delete: "Löschen",
Edit: "Bearbeiten", Edit: "Bearbeiten",
RefreshTitle: "Titel aktualisieren",
RefreshToast: "Anfrage zur Titelaktualisierung gesendet",
}, },
Commands: { Commands: {
new: "Neues Gespräch", new: "Neues Gespräch",
@ -421,6 +423,10 @@ const de: PartialLocaleType = {
}, },
Model: "Modell", Model: "Modell",
CompressModel: {
Title: "Kompressionsmodell",
SubTitle: "Modell zur Komprimierung des Verlaufs",
},
Temperature: { Temperature: {
Title: "Zufälligkeit (temperature)", Title: "Zufälligkeit (temperature)",
SubTitle: "Je höher der Wert, desto zufälliger die Antwort", SubTitle: "Je höher der Wert, desto zufälliger die Antwort",

View File

@ -45,6 +45,8 @@ const en: LocaleType = {
Delete: "Delete", Delete: "Delete",
Edit: "Edit", Edit: "Edit",
FullScreen: "FullScreen", FullScreen: "FullScreen",
RefreshTitle: "Refresh Title",
RefreshToast: "Title refresh request sent",
Speech: "Play", Speech: "Play",
StopSpeech: "Stop", StopSpeech: "Stop",
}, },
@ -87,6 +89,14 @@ const en: LocaleType = {
SaveAs: "Save as Mask", SaveAs: "Save as Mask",
}, },
IsContext: "Contextual Prompt", IsContext: "Contextual Prompt",
ShortcutKey: {
Title: "Keyboard Shortcuts",
newChat: "Open New Chat",
focusInput: "Focus Input Field",
copyLastMessage: "Copy Last Reply",
copyLastCode: "Copy Last Code Block",
showShortcutKey: "Show Shortcuts",
},
}, },
Export: { Export: {
Title: "Export Messages", Title: "Export Messages",
@ -470,6 +480,10 @@ const en: LocaleType = {
}, },
Model: "Model", Model: "Model",
CompressModel: {
Title: "Compression Model",
SubTitle: "Model used to compress history",
},
Temperature: { Temperature: {
Title: "Temperature", Title: "Temperature",
SubTitle: "A larger value makes the more random output", SubTitle: "A larger value makes the more random output",
@ -552,10 +566,6 @@ const en: LocaleType = {
Clear: "Context Cleared", Clear: "Context Cleared",
Revert: "Revert", Revert: "Revert",
}, },
Plugin: {
Name: "Plugin",
Artifacts: "Artifacts",
},
Discovery: { Discovery: {
Name: "Discovery", Name: "Discovery",
}, },
@ -577,6 +587,47 @@ const en: LocaleType = {
View: "View", View: "View",
}, },
}, },
Plugin: {
Name: "Plugin",
Page: {
Title: "Plugins",
SubTitle: (count: number) => `${count} plugins`,
Search: "Search Plugin",
Create: "Create",
Find: "You can find awesome plugins on github: ",
},
Item: {
Info: (count: number) => `${count} method`,
View: "View",
Edit: "Edit",
Delete: "Delete",
DeleteConfirm: "Confirm to delete?",
},
Auth: {
None: "None",
Basic: "Basic",
Bearer: "Bearer",
Custom: "Custom",
CustomHeader: "Parameter Name",
Token: "Token",
Proxy: "Using Proxy",
ProxyDescription: "Using proxies to solve CORS error",
Location: "Location",
LocationHeader: "Header",
LocationQuery: "Query",
LocationBody: "Body",
},
EditModal: {
Title: (readonly: boolean) =>
`Edit Plugin ${readonly ? "(readonly)" : ""}`,
Download: "Download",
Auth: "Authentication Type",
Content: "OpenAPI Schema",
Load: "Load From URL",
Method: "Method",
Error: "OpenAPI Schema Error",
},
},
Mask: { Mask: {
Name: "Mask", Name: "Mask",
Page: { Page: {
@ -611,6 +662,10 @@ const en: LocaleType = {
Title: "Hide Context Prompts", Title: "Hide Context Prompts",
SubTitle: "Do not show in-context prompts in chat", SubTitle: "Do not show in-context prompts in chat",
}, },
Artifacts: {
Title: "Enable Artifacts",
SubTitle: "Can render HTML page when enable artifacts.",
},
Share: { Share: {
Title: "Share This Mask", Title: "Share This Mask",
SubTitle: "Generate a link to this mask", SubTitle: "Generate a link to this mask",

View File

@ -44,6 +44,8 @@ const es: PartialLocaleType = {
PinToastAction: "Ver", PinToastAction: "Ver",
Delete: "Eliminar", Delete: "Eliminar",
Edit: "Editar", Edit: "Editar",
RefreshTitle: "Actualizar título",
RefreshToast: "Se ha enviado la solicitud de actualización del título",
}, },
Commands: { Commands: {
new: "Nueva conversación", new: "Nueva conversación",
@ -423,6 +425,10 @@ const es: PartialLocaleType = {
}, },
Model: "Modelo (model)", Model: "Modelo (model)",
CompressModel: {
Title: "Modelo de compresión",
SubTitle: "Modelo utilizado para comprimir el historial",
},
Temperature: { Temperature: {
Title: "Aleatoriedad (temperature)", Title: "Aleatoriedad (temperature)",
SubTitle: "Cuanto mayor sea el valor, más aleatorio será el resultado", SubTitle: "Cuanto mayor sea el valor, más aleatorio será el resultado",

View File

@ -43,6 +43,8 @@ const fr: PartialLocaleType = {
PinToastAction: "Voir", PinToastAction: "Voir",
Delete: "Supprimer", Delete: "Supprimer",
Edit: "Modifier", Edit: "Modifier",
RefreshTitle: "Actualiser le titre",
RefreshToast: "Demande d'actualisation du titre envoyée",
}, },
Commands: { Commands: {
new: "Nouvelle discussion", new: "Nouvelle discussion",
@ -422,6 +424,10 @@ const fr: PartialLocaleType = {
}, },
Model: "Modèle", Model: "Modèle",
CompressModel: {
Title: "Modèle de compression",
SubTitle: "Modèle utilisé pour compresser l'historique",
},
Temperature: { Temperature: {
Title: "Aléatoire (temperature)", Title: "Aléatoire (temperature)",
SubTitle: "Plus la valeur est élevée, plus les réponses sont aléatoires", SubTitle: "Plus la valeur est élevée, plus les réponses sont aléatoires",

View File

@ -43,6 +43,8 @@ const id: PartialLocaleType = {
PinToastAction: "Lihat", PinToastAction: "Lihat",
Delete: "Hapus", Delete: "Hapus",
Edit: "Edit", Edit: "Edit",
RefreshTitle: "Segarkan Judul",
RefreshToast: "Permintaan penyegaran judul telah dikirim",
}, },
Commands: { Commands: {
new: "Obrolan Baru", new: "Obrolan Baru",
@ -411,6 +413,10 @@ const id: PartialLocaleType = {
}, },
Model: "Model", Model: "Model",
CompressModel: {
Title: "Model Kompresi",
SubTitle: "Model yang digunakan untuk mengompres riwayat",
},
Temperature: { Temperature: {
Title: "Randomness (temperature)", Title: "Randomness (temperature)",
SubTitle: "Semakin tinggi nilainya, semakin acak responsnya", SubTitle: "Semakin tinggi nilainya, semakin acak responsnya",

View File

@ -18,10 +18,13 @@ import ar from "./ar";
import bn from "./bn"; import bn from "./bn";
import sk from "./sk"; import sk from "./sk";
import { merge } from "../utils/merge"; import { merge } from "../utils/merge";
import { safeLocalStorage } from "@/app/utils";
import type { LocaleType } from "./cn"; import type { LocaleType } from "./cn";
export type { LocaleType, PartialLocaleType } from "./cn"; export type { LocaleType, PartialLocaleType } from "./cn";
const localStorage = safeLocalStorage();
const ALL_LANGS = { const ALL_LANGS = {
cn, cn,
en, en,
@ -82,17 +85,11 @@ merge(fallbackLang, targetLang);
export default fallbackLang as LocaleType; export default fallbackLang as LocaleType;
function getItem(key: string) { function getItem(key: string) {
try { return localStorage.getItem(key);
return localStorage.getItem(key);
} catch {
return null;
}
} }
function setItem(key: string, value: string) { function setItem(key: string, value: string) {
try { localStorage.setItem(key, value);
localStorage.setItem(key, value);
} catch {}
} }
function getLanguage() { function getLanguage() {

View File

@ -43,6 +43,8 @@ const it: PartialLocaleType = {
PinToastAction: "Visualizza", PinToastAction: "Visualizza",
Delete: "Elimina", Delete: "Elimina",
Edit: "Modifica", Edit: "Modifica",
RefreshTitle: "Aggiorna titolo",
RefreshToast: "Richiesta di aggiornamento del titolo inviata",
}, },
Commands: { Commands: {
new: "Nuova chat", new: "Nuova chat",
@ -423,6 +425,10 @@ const it: PartialLocaleType = {
}, },
Model: "Modello (model)", Model: "Modello (model)",
CompressModel: {
Title: "Modello di compressione",
SubTitle: "Modello utilizzato per comprimere la cronologia",
},
Temperature: { Temperature: {
Title: "Casualità (temperature)", Title: "Casualità (temperature)",
SubTitle: "Valore più alto, risposte più casuali", SubTitle: "Valore più alto, risposte più casuali",

View File

@ -43,6 +43,8 @@ const jp: PartialLocaleType = {
PinToastAction: "見る", PinToastAction: "見る",
Delete: "削除", Delete: "削除",
Edit: "編集", Edit: "編集",
RefreshTitle: "タイトルを更新",
RefreshToast: "タイトル更新リクエストが送信されました",
}, },
Commands: { Commands: {
new: "新しいチャット", new: "新しいチャット",
@ -407,6 +409,10 @@ const jp: PartialLocaleType = {
}, },
Model: "モデル (model)", Model: "モデル (model)",
CompressModel: {
Title: "圧縮モデル",
SubTitle: "履歴を圧縮するために使用されるモデル",
},
Temperature: { Temperature: {
Title: "ランダム性 (temperature)", Title: "ランダム性 (temperature)",
SubTitle: "値が大きいほど応答がランダムになります", SubTitle: "値が大きいほど応答がランダムになります",

View File

@ -43,6 +43,8 @@ const ko: PartialLocaleType = {
PinToastAction: "보기", PinToastAction: "보기",
Delete: "삭제", Delete: "삭제",
Edit: "편집", Edit: "편집",
RefreshTitle: "제목 새로고침",
RefreshToast: "제목 새로고침 요청이 전송되었습니다",
}, },
Commands: { Commands: {
new: "새 채팅", new: "새 채팅",
@ -404,6 +406,10 @@ const ko: PartialLocaleType = {
}, },
Model: "모델 (model)", Model: "모델 (model)",
CompressModel: {
Title: "압축 모델",
SubTitle: "기록을 압축하는 데 사용되는 모델",
},
Temperature: { Temperature: {
Title: "무작위성 (temperature)", Title: "무작위성 (temperature)",
SubTitle: "값이 클수록 응답이 더 무작위적", SubTitle: "값이 클수록 응답이 더 무작위적",

View File

@ -44,6 +44,8 @@ const no: PartialLocaleType = {
PinToastAction: "Se", PinToastAction: "Se",
Delete: "Slett", Delete: "Slett",
Edit: "Rediger", Edit: "Rediger",
RefreshTitle: "Oppdater tittel",
RefreshToast: "Forespørsel om titteloppdatering sendt",
}, },
Commands: { Commands: {
new: "Ny samtale", new: "Ny samtale",
@ -415,6 +417,10 @@ const no: PartialLocaleType = {
}, },
Model: "Modell", Model: "Modell",
CompressModel: {
Title: "Komprimeringsmodell",
SubTitle: "Modell brukt for å komprimere historikken",
},
Temperature: { Temperature: {
Title: "Tilfeldighet (temperature)", Title: "Tilfeldighet (temperature)",
SubTitle: "Høyere verdi gir mer tilfeldige svar", SubTitle: "Høyere verdi gir mer tilfeldige svar",

View File

@ -43,6 +43,8 @@ const pt: PartialLocaleType = {
PinToastAction: "Visualizar", PinToastAction: "Visualizar",
Delete: "Deletar", Delete: "Deletar",
Edit: "Editar", Edit: "Editar",
RefreshTitle: "Atualizar Título",
RefreshToast: "Solicitação de atualização de título enviada",
}, },
Commands: { Commands: {
new: "Iniciar um novo chat", new: "Iniciar um novo chat",
@ -346,6 +348,10 @@ const pt: PartialLocaleType = {
}, },
Model: "Modelo", Model: "Modelo",
CompressModel: {
Title: "Modelo de Compressão",
SubTitle: "Modelo usado para comprimir o histórico",
},
Temperature: { Temperature: {
Title: "Temperatura", Title: "Temperatura",
SubTitle: "Um valor maior torna a saída mais aleatória", SubTitle: "Um valor maior torna a saída mais aleatória",

View File

@ -43,6 +43,8 @@ const ru: PartialLocaleType = {
PinToastAction: "Просмотреть", PinToastAction: "Просмотреть",
Delete: "Удалить", Delete: "Удалить",
Edit: "Редактировать", Edit: "Редактировать",
RefreshTitle: "Обновить заголовок",
RefreshToast: "Запрос на обновление заголовка отправлен",
}, },
Commands: { Commands: {
new: "Новый чат", new: "Новый чат",
@ -414,6 +416,10 @@ const ru: PartialLocaleType = {
}, },
Model: "Модель", Model: "Модель",
CompressModel: {
Title: "Модель сжатия",
SubTitle: "Модель, используемая для сжатия истории",
},
Temperature: { Temperature: {
Title: "Случайность (temperature)", Title: "Случайность (temperature)",
SubTitle: "Чем больше значение, тем более случайные ответы", SubTitle: "Чем больше значение, тем более случайные ответы",

View File

@ -45,6 +45,8 @@ const sk: PartialLocaleType = {
PinToastAction: "Zobraziť", PinToastAction: "Zobraziť",
Delete: "Vymazať", Delete: "Vymazať",
Edit: "Upraviť", Edit: "Upraviť",
RefreshTitle: "Obnoviť názov",
RefreshToast: "Požiadavka na obnovenie názvu bola odoslaná",
}, },
Commands: { Commands: {
new: "Začať nový chat", new: "Začať nový chat",
@ -365,6 +367,10 @@ const sk: PartialLocaleType = {
}, },
Model: "Model", Model: "Model",
CompressModel: {
Title: "Kompresný model",
SubTitle: "Model používaný na kompresiu histórie",
},
Temperature: { Temperature: {
Title: "Teplota", Title: "Teplota",
SubTitle: "Vyššia hodnota robí výstup náhodnejším", SubTitle: "Vyššia hodnota robí výstup náhodnejším",

View File

@ -43,6 +43,8 @@ const tr: PartialLocaleType = {
PinToastAction: "Görünüm", PinToastAction: "Görünüm",
Delete: "Sil", Delete: "Sil",
Edit: "Düzenle", Edit: "Düzenle",
RefreshTitle: "Başlığı Yenile",
RefreshToast: "Başlık yenileme isteği gönderildi",
}, },
Commands: { Commands: {
new: "Yeni sohbet", new: "Yeni sohbet",
@ -414,6 +416,10 @@ const tr: PartialLocaleType = {
}, },
Model: "Model (model)", Model: "Model (model)",
CompressModel: {
Title: "Sıkıştırma Modeli",
SubTitle: "Geçmişi sıkıştırmak için kullanılan model",
},
Temperature: { Temperature: {
Title: "Rastgelelik (temperature)", Title: "Rastgelelik (temperature)",
SubTitle: "Değer arttıkça yanıt daha rastgele olur", SubTitle: "Değer arttıkça yanıt daha rastgele olur",

View File

@ -43,6 +43,8 @@ const tw = {
PinToastAction: "檢視", PinToastAction: "檢視",
Delete: "刪除", Delete: "刪除",
Edit: "編輯", Edit: "編輯",
RefreshTitle: "刷新標題",
RefreshToast: "已發送刷新標題請求",
}, },
Commands: { Commands: {
new: "新建聊天", new: "新建聊天",
@ -81,6 +83,14 @@ const tw = {
SaveAs: "另存新檔", SaveAs: "另存新檔",
}, },
IsContext: "預設提示詞", IsContext: "預設提示詞",
ShortcutKey: {
Title: "鍵盤快捷方式",
newChat: "打開新聊天",
focusInput: "聚焦輸入框",
copyLastMessage: "複製最後一個回覆",
copyLastCode: "複製最後一個代碼塊",
showShortcutKey: "顯示快捷方式",
},
}, },
Export: { Export: {
Title: "將聊天記錄匯出為 Markdown", Title: "將聊天記錄匯出為 Markdown",
@ -360,6 +370,10 @@ const tw = {
}, },
Model: "模型 (model)", Model: "模型 (model)",
CompressModel: {
Title: "壓縮模型",
SubTitle: "用於壓縮歷史記錄的模型",
},
Temperature: { Temperature: {
Title: "隨機性 (temperature)", Title: "隨機性 (temperature)",
SubTitle: "值越大,回應越隨機", SubTitle: "值越大,回應越隨機",

View File

@ -43,6 +43,8 @@ const vi: PartialLocaleType = {
PinToastAction: "Xem", PinToastAction: "Xem",
Delete: "Xóa", Delete: "Xóa",
Edit: "Chỉnh sửa", Edit: "Chỉnh sửa",
RefreshTitle: "Làm mới tiêu đề",
RefreshToast: "Đã gửi yêu cầu làm mới tiêu đề",
}, },
Commands: { Commands: {
new: "Tạo cuộc trò chuyện mới", new: "Tạo cuộc trò chuyện mới",
@ -410,6 +412,10 @@ const vi: PartialLocaleType = {
}, },
Model: "Mô hình (model)", Model: "Mô hình (model)",
CompressModel: {
Title: "Mô hình nén",
SubTitle: "Mô hình được sử dụng để nén lịch sử",
},
Temperature: { Temperature: {
Title: "Độ ngẫu nhiên (temperature)", Title: "Độ ngẫu nhiên (temperature)",
SubTitle: "Giá trị càng lớn, câu trả lời càng ngẫu nhiên", SubTitle: "Giá trị càng lớn, câu trả lời càng ngẫu nhiên",

View File

@ -1,32 +1,43 @@
import { trimTopic, getMessageTextContent } from "../utils"; import { getMessageTextContent, trimTopic } from "../utils";
import Locale, { getLang } from "../locales"; import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
import { nanoid } from "nanoid";
import type {
ClientApi,
MultimodalContent,
RequestMessage,
} from "../client/api";
import { getClientApi } from "../client/api";
import { ChatControllerPool } from "../client/controller";
import { showToast } from "../components/ui-lib"; import { showToast } from "../components/ui-lib";
import { ModelConfig, ModelType, useAppConfig } from "./config";
import { createEmptyMask, Mask } from "./mask";
import { import {
DEFAULT_INPUT_TEMPLATE, DEFAULT_INPUT_TEMPLATE,
DEFAULT_MODELS, DEFAULT_MODELS,
DEFAULT_SYSTEM_TEMPLATE, DEFAULT_SYSTEM_TEMPLATE,
KnowledgeCutOffDate, KnowledgeCutOffDate,
StoreKey, StoreKey,
SUMMARIZE_MODEL,
GEMINI_SUMMARIZE_MODEL,
} from "../constant"; } from "../constant";
import { getClientApi } from "../client/api"; import Locale, { getLang } from "../locales";
import type { import { isDalle3, safeLocalStorage } from "../utils";
ClientApi,
RequestMessage,
MultimodalContent,
} from "../client/api";
import { ChatControllerPool } from "../client/controller";
import { prettyObject } from "../utils/format"; import { prettyObject } from "../utils/format";
import { estimateTokenLength } from "../utils/token";
import { nanoid } from "nanoid";
import { createPersistStore } from "../utils/store"; import { createPersistStore } from "../utils/store";
import { collectModelsWithDefaultModel } from "../utils/model"; import { estimateTokenLength } from "../utils/token";
import { useAccessStore } from "./access"; import { ModelConfig, ModelType, useAppConfig } from "./config";
import { isDalle3 } from "../utils"; import { createEmptyMask, Mask } from "./mask";
const localStorage = safeLocalStorage();
export type ChatMessageTool = {
id: string;
index?: number;
type?: string;
function?: {
name: string;
arguments?: string;
};
content?: string;
isError?: boolean;
};
export type ChatMessage = RequestMessage & { export type ChatMessage = RequestMessage & {
date: string; date: string;
@ -34,6 +45,7 @@ export type ChatMessage = RequestMessage & {
isError?: boolean; isError?: boolean;
id: string; id: string;
model?: ModelType; model?: ModelType;
tools?: ChatMessageTool[];
}; };
export function createMessage(override: Partial<ChatMessage>): ChatMessage { export function createMessage(override: Partial<ChatMessage>): ChatMessage {
@ -90,27 +102,6 @@ function createEmptySession(): ChatSession {
}; };
} }
function getSummarizeModel(currentModel: string) {
// if it is using gpt-* models, force to use 4o-mini to summarize
if (currentModel.startsWith("gpt")) {
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,
);
return summarizeModel?.name ?? currentModel;
}
if (currentModel.startsWith("gemini")) {
return GEMINI_SUMMARIZE_MODEL;
}
return currentModel;
}
function countMessages(msgs: ChatMessage[]) { function countMessages(msgs: ChatMessage[]) {
return msgs.reduce( return msgs.reduce(
(pre, cur) => pre + estimateTokenLength(getMessageTextContent(cur)), (pre, cur) => pre + estimateTokenLength(getMessageTextContent(cur)),
@ -165,6 +156,7 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
const DEFAULT_CHAT_STATE = { const DEFAULT_CHAT_STATE = {
sessions: [createEmptySession()], sessions: [createEmptySession()],
currentSessionIndex: 0, currentSessionIndex: 0,
lastInput: "",
}; };
export const useChatStore = createPersistStore( export const useChatStore = createPersistStore(
@ -389,8 +381,24 @@ export const useChatStore = createPersistStore(
} }
ChatControllerPool.remove(session.id, botMessage.id); ChatControllerPool.remove(session.id, botMessage.id);
}, },
onBeforeTool(tool: ChatMessageTool) {
(botMessage.tools = botMessage?.tools || []).push(tool);
get().updateCurrentSession((session) => {
session.messages = session.messages.concat();
});
},
onAfterTool(tool: ChatMessageTool) {
botMessage?.tools?.forEach((t, i, tools) => {
if (tool.id == t.id) {
tools[i] = { ...tool };
}
});
get().updateCurrentSession((session) => {
session.messages = session.messages.concat();
});
},
onError(error) { onError(error) {
const isAborted = error.message.includes("aborted"); const isAborted = error.message?.includes?.("aborted");
botMessage.content += botMessage.content +=
"\n\n" + "\n\n" +
prettyObject({ prettyObject({
@ -446,7 +454,8 @@ export const useChatStore = createPersistStore(
// system prompts, to get close to OpenAI Web ChatGPT // system prompts, to get close to OpenAI Web ChatGPT
const shouldInjectSystemPrompts = const shouldInjectSystemPrompts =
modelConfig.enableInjectSystemPrompts && modelConfig.enableInjectSystemPrompts &&
session.mask.modelConfig.model.startsWith("gpt-"); (session.mask.modelConfig.model.startsWith("gpt-") ||
session.mask.modelConfig.model.startsWith("chatgpt-"));
var systemPrompts: ChatMessage[] = []; var systemPrompts: ChatMessage[] = [];
systemPrompts = shouldInjectSystemPrompts systemPrompts = shouldInjectSystemPrompts
@ -538,7 +547,7 @@ export const useChatStore = createPersistStore(
}); });
}, },
summarizeSession() { summarizeSession(refreshTitle: boolean = false) {
const config = useAppConfig.getState(); const config = useAppConfig.getState();
const session = get().currentSession(); const session = get().currentSession();
const modelConfig = session.mask.modelConfig; const modelConfig = session.mask.modelConfig;
@ -547,7 +556,7 @@ export const useChatStore = createPersistStore(
return; return;
} }
const providerName = modelConfig.providerName; const providerName = modelConfig.compressProviderName;
const api: ClientApi = getClientApi(providerName); const api: ClientApi = getClientApi(providerName);
// remove error messages if any // remove error messages if any
@ -556,20 +565,30 @@ export const useChatStore = createPersistStore(
// should summarize topic after chating more than 50 words // should summarize topic after chating more than 50 words
const SUMMARIZE_MIN_LEN = 50; const SUMMARIZE_MIN_LEN = 50;
if ( if (
config.enableAutoGenerateTitle && (config.enableAutoGenerateTitle &&
session.topic === DEFAULT_TOPIC && session.topic === DEFAULT_TOPIC &&
countMessages(messages) >= SUMMARIZE_MIN_LEN countMessages(messages) >= SUMMARIZE_MIN_LEN) ||
refreshTitle
) { ) {
const topicMessages = messages.concat( const startIndex = Math.max(
createMessage({ 0,
role: "user", messages.length - modelConfig.historyMessageCount,
content: Locale.Store.Prompt.Topic,
}),
); );
const topicMessages = messages
.slice(
startIndex < messages.length ? startIndex : messages.length - 1,
messages.length,
)
.concat(
createMessage({
role: "user",
content: Locale.Store.Prompt.Topic,
}),
);
api.llm.chat({ api.llm.chat({
messages: topicMessages, messages: topicMessages,
config: { config: {
model: getSummarizeModel(session.mask.modelConfig.model), model: modelConfig.compressModel,
stream: false, stream: false,
providerName, providerName,
}, },
@ -632,7 +651,7 @@ export const useChatStore = createPersistStore(
config: { config: {
...modelcfg, ...modelcfg,
stream: true, stream: true,
model: getSummarizeModel(session.mask.modelConfig.model), model: modelConfig.compressModel,
}, },
onUpdate(message) { onUpdate(message) {
session.memoryPrompt = message; session.memoryPrompt = message;
@ -665,17 +684,23 @@ export const useChatStore = createPersistStore(
set(() => ({ sessions })); set(() => ({ sessions }));
}, },
clearAllData() { async clearAllData() {
await indexedDBStorage.clear();
localStorage.clear(); localStorage.clear();
location.reload(); location.reload();
}, },
setLastInput(lastInput: string) {
set({
lastInput,
});
},
}; };
return methods; return methods;
}, },
{ {
name: StoreKey.Chat, name: StoreKey.Chat,
version: 3.1, version: 3.2,
migrate(persistedState, version) { migrate(persistedState, version) {
const state = persistedState as any; const state = persistedState as any;
const newState = JSON.parse( const newState = JSON.parse(
@ -722,6 +747,16 @@ export const useChatStore = createPersistStore(
}); });
} }
// add default summarize model for every session
if (version < 3.2) {
newState.sessions.forEach((s) => {
const config = useAppConfig.getState();
s.mask.modelConfig.compressModel = config.modelConfig.compressModel;
s.mask.modelConfig.compressProviderName =
config.modelConfig.compressProviderName;
});
}
return newState as any; return newState as any;
}, },
}, },

View File

@ -63,7 +63,7 @@ export const DEFAULT_CONFIG = {
models: DEFAULT_MODELS as any as LLMModel[], models: DEFAULT_MODELS as any as LLMModel[],
modelConfig: { modelConfig: {
model: "gpt-3.5-turbo" as ModelType, model: "gpt-4o-mini" as ModelType,
providerName: "OpenAI" as ServiceProvider, providerName: "OpenAI" as ServiceProvider,
temperature: 0.5, temperature: 0.5,
top_p: 1, top_p: 1,
@ -73,6 +73,8 @@ export const DEFAULT_CONFIG = {
sendMemory: true, sendMemory: true,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
compressModel: "gpt-4o-mini" as ModelType,
compressProviderName: "OpenAI" as ServiceProvider,
enableInjectSystemPrompts: true, enableInjectSystemPrompts: true,
template: config?.template ?? DEFAULT_INPUT_TEMPLATE, template: config?.template ?? DEFAULT_INPUT_TEMPLATE,
size: "1024x1024" as DalleSize, size: "1024x1024" as DalleSize,
@ -189,7 +191,7 @@ export const useAppConfig = createPersistStore(
}), }),
{ {
name: StoreKey.Config, name: StoreKey.Config,
version: 3.9, version: 4,
migrate(persistedState, version) { migrate(persistedState, version) {
const state = persistedState as ChatConfig; const state = persistedState as ChatConfig;
@ -227,6 +229,13 @@ export const useAppConfig = createPersistStore(
: config?.template ?? DEFAULT_INPUT_TEMPLATE; : config?.template ?? DEFAULT_INPUT_TEMPLATE;
} }
if (version < 4) {
state.modelConfig.compressModel =
DEFAULT_CONFIG.modelConfig.compressModel;
state.modelConfig.compressProviderName =
DEFAULT_CONFIG.modelConfig.compressProviderName;
}
return state as any; return state as any;
}, },
}, },

View File

@ -2,3 +2,4 @@ export * from "./chat";
export * from "./update"; export * from "./update";
export * from "./access"; export * from "./access";
export * from "./config"; export * from "./config";
export * from "./plugin";

View File

@ -2,7 +2,7 @@ import { BUILTIN_MASKS } from "../masks";
import { getLang, Lang } from "../locales"; import { getLang, Lang } from "../locales";
import { DEFAULT_TOPIC, ChatMessage } from "./chat"; import { DEFAULT_TOPIC, ChatMessage } from "./chat";
import { ModelConfig, useAppConfig } from "./config"; import { ModelConfig, useAppConfig } from "./config";
import { StoreKey, Plugin } from "../constant"; import { StoreKey } from "../constant";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { createPersistStore } from "../utils/store"; import { createPersistStore } from "../utils/store";
@ -17,14 +17,18 @@ export type Mask = {
modelConfig: ModelConfig; modelConfig: ModelConfig;
lang: Lang; lang: Lang;
builtin: boolean; builtin: boolean;
plugin?: Plugin[]; plugin?: string[];
enableArtifacts?: boolean;
}; };
export const DEFAULT_MASK_STATE = { export const DEFAULT_MASK_STATE = {
masks: {} as Record<string, Mask>, masks: {} as Record<string, Mask>,
language: undefined as Lang | undefined,
}; };
export type MaskState = typeof DEFAULT_MASK_STATE; export type MaskState = typeof DEFAULT_MASK_STATE & {
language?: Lang | undefined;
};
export const DEFAULT_MASK_AVATAR = "gpt-bot"; export const DEFAULT_MASK_AVATAR = "gpt-bot";
export const createEmptyMask = () => export const createEmptyMask = () =>
@ -38,7 +42,7 @@ export const createEmptyMask = () =>
lang: getLang(), lang: getLang(),
builtin: false, builtin: false,
createdAt: Date.now(), createdAt: Date.now(),
plugin: [Plugin.Artifacts], plugin: [],
}) as Mask; }) as Mask;
export const useMaskStore = createPersistStore( export const useMaskStore = createPersistStore(
@ -101,6 +105,11 @@ export const useMaskStore = createPersistStore(
search(text: string) { search(text: string) {
return Object.values(get().masks); return Object.values(get().masks);
}, },
setLanguage(language: Lang | undefined) {
set({
language,
});
},
}), }),
{ {
name: StoreKey.Mask, name: StoreKey.Mask,

225
app/store/plugin.ts Normal file
View File

@ -0,0 +1,225 @@
import OpenAPIClientAxios from "openapi-client-axios";
import { getLang, Lang } from "../locales";
import { StoreKey } from "../constant";
import { nanoid } from "nanoid";
import { createPersistStore } from "../utils/store";
import yaml from "js-yaml";
import { adapter } from "../utils";
export type Plugin = {
id: string;
createdAt: number;
title: string;
version: string;
content: string;
builtin: boolean;
authType?: string;
authLocation?: string;
authHeader?: string;
authToken?: string;
usingProxy?: boolean;
};
export type FunctionToolItem = {
type: string;
function: {
name: string;
description?: string;
parameters: Object;
};
};
type FunctionToolServiceItem = {
api: OpenAPIClientAxios;
length: number;
tools: FunctionToolItem[];
funcs: Record<string, Function>;
};
export const FunctionToolService = {
tools: {} as Record<string, FunctionToolServiceItem>,
add(plugin: Plugin, replace = false) {
if (!replace && this.tools[plugin.id]) return this.tools[plugin.id];
const headerName = (
plugin?.authType == "custom" ? plugin?.authHeader : "Authorization"
) as string;
const tokenValue =
plugin?.authType == "basic"
? `Basic ${plugin?.authToken}`
: plugin?.authType == "bearer"
? ` 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 headers: Record<string, string | undefined> = {
"X-Base-URL": !!plugin?.usingProxy ? serverURL : undefined,
};
if (authLocation == "header") {
headers[headerName] = tokenValue;
}
const api = new OpenAPIClientAxios({
definition: yaml.load(plugin.content) as any,
axiosConfigDefaults: {
adapter: (window.__TAURI__ ? adapter : ["xhr"]) as any,
baseURL,
headers,
},
});
try {
api.initSync();
} catch (e) {}
const operations = api.getOperations();
return (this.tools[plugin.id] = {
api,
length: operations.length,
tools: operations.map((o) => {
// @ts-ignore
const parameters = o?.requestBody?.content["application/json"]
?.schema || {
type: "object",
properties: {},
};
if (!parameters["required"]) {
parameters["required"] = [];
}
if (o.parameters instanceof Array) {
o.parameters.forEach((p) => {
// @ts-ignore
if (p?.in == "query" || p?.in == "path") {
// const name = `${p.in}__${p.name}`
// @ts-ignore
const name = p?.name;
parameters["properties"][name] = {
// @ts-ignore
type: p.schema.type,
// @ts-ignore
description: p.description,
};
// @ts-ignore
if (p.required) {
parameters["required"].push(name);
}
}
});
}
return {
type: "function",
function: {
name: o.operationId,
description: o.description || o.summary,
parameters: parameters,
},
} as FunctionToolItem;
}),
funcs: operations.reduce((s, o) => {
// @ts-ignore
s[o.operationId] = function (args) {
const parameters: Record<string, any> = {};
if (o.parameters instanceof Array) {
o.parameters.forEach((p) => {
// @ts-ignore
parameters[p?.name] = args[p?.name];
// @ts-ignore
delete args[p?.name];
});
}
if (authLocation == "query") {
parameters[headerName] = tokenValue;
} else if (authLocation == "body") {
args[headerName] = tokenValue;
}
// @ts-ignore
return api.client[o.operationId](
parameters,
args,
api.axiosConfigDefaults,
);
};
return s;
}, {}),
});
},
get(id: string) {
return this.tools[id];
},
};
export const createEmptyPlugin = () =>
({
id: nanoid(),
title: "",
version: "1.0.0",
content: "",
builtin: false,
createdAt: Date.now(),
}) as Plugin;
export const DEFAULT_PLUGIN_STATE = {
plugins: {} as Record<string, Plugin>,
};
export const usePluginStore = createPersistStore(
{ ...DEFAULT_PLUGIN_STATE },
(set, get) => ({
create(plugin?: Partial<Plugin>) {
const plugins = get().plugins;
const id = nanoid();
plugins[id] = {
...createEmptyPlugin(),
...plugin,
id,
builtin: false,
};
set(() => ({ plugins }));
get().markUpdate();
return plugins[id];
},
updatePlugin(id: string, updater: (plugin: Plugin) => void) {
const plugins = get().plugins;
const plugin = plugins[id];
if (!plugin) return;
const updatePlugin = { ...plugin };
updater(updatePlugin);
plugins[id] = updatePlugin;
FunctionToolService.add(updatePlugin, true);
set(() => ({ plugins }));
get().markUpdate();
},
delete(id: string) {
const plugins = get().plugins;
delete plugins[id];
set(() => ({ plugins }));
get().markUpdate();
},
getAsTools(ids: string[]) {
const plugins = get().plugins;
const selected = (ids || [])
.map((id) => plugins[id])
.filter((i) => i)
.map((p) => FunctionToolService.add(p));
return [
// @ts-ignore
selected.reduce((s, i) => s.concat(i.tools), []),
selected.reduce((s, i) => Object.assign(s, i.funcs), {}),
];
},
get(id?: string) {
return get().plugins[id ?? 1145141919810];
},
getAll() {
return Object.values(get().plugins).sort(
(a, b) => b.createdAt - a.createdAt,
);
},
}),
{
name: StoreKey.Plugin,
version: 1,
},
);

View File

@ -2,6 +2,9 @@ import { useEffect, useState } from "react";
import { showToast } from "./components/ui-lib"; import { showToast } from "./components/ui-lib";
import Locale from "./locales"; import Locale from "./locales";
import { RequestMessage } from "./client/api"; import { RequestMessage } from "./client/api";
import { ServiceProvider, REQUEST_TIMEOUT_MS } from "./constant";
import isObject from "lodash-es/isObject";
import { fetch as tauriFetch, Body, ResponseType } from "@tauri-apps/api/http";
export function trimTopic(topic: string) { export function trimTopic(topic: string) {
// Fix an issue where double quotes still show in the Indonesian language // Fix an issue where double quotes still show in the Indonesian language
@ -270,3 +273,108 @@ export function isVisionModel(model: string) {
export function isDalle3(model: string) { export function isDalle3(model: string) {
return "dall-e-3" === model; return "dall-e-3" === model;
} }
export function showPlugins(provider: ServiceProvider, model: string) {
if (
provider == ServiceProvider.OpenAI ||
provider == ServiceProvider.Azure ||
provider == ServiceProvider.Moonshot
) {
return true;
}
if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) {
return true;
}
return false;
}
export function fetch(
url: string,
options?: Record<string, unknown>,
): Promise<any> {
if (window.__TAURI__) {
const payload = options?.body || options?.data;
return tauriFetch(url, {
...options,
body:
payload &&
({
type: "Text",
payload,
} as any),
timeout: ((options?.timeout as number) || REQUEST_TIMEOUT_MS) / 1000,
responseType:
options?.responseType == "text" ? ResponseType.Text : ResponseType.JSON,
} as any);
}
return window.fetch(url, options);
}
export function adapter(config: Record<string, unknown>) {
const { baseURL, url, params, ...rest } = config;
const path = baseURL ? `${baseURL}${url}` : url;
const fetchUrl = params
? `${path}?${new URLSearchParams(params as any).toString()}`
: path;
return fetch(fetchUrl as string, { ...rest, responseType: "text" });
}
export function safeLocalStorage(): {
getItem: (key: string) => string | null;
setItem: (key: string, value: string) => void;
removeItem: (key: string) => void;
clear: () => void;
} {
let storage: Storage | null;
try {
if (typeof window !== "undefined" && window.localStorage) {
storage = window.localStorage;
} else {
storage = null;
}
} catch (e) {
console.error("localStorage is not available:", e);
storage = null;
}
return {
getItem(key: string): string | null {
if (storage) {
return storage.getItem(key);
} else {
console.warn(
`Attempted to get item "${key}" from localStorage, but localStorage is not available.`,
);
return null;
}
},
setItem(key: string, value: string): void {
if (storage) {
storage.setItem(key, value);
} else {
console.warn(
`Attempted to set item "${key}" in localStorage, but localStorage is not available.`,
);
}
},
removeItem(key: string): void {
if (storage) {
storage.removeItem(key);
} else {
console.warn(
`Attempted to remove item "${key}" from localStorage, but localStorage is not available.`,
);
}
},
clear(): void {
if (storage) {
storage.clear();
} else {
console.warn(
"Attempted to clear localStorage, but localStorage is not available.",
);
}
},
};
}

View File

@ -1,5 +1,15 @@
import { CACHE_URL_PREFIX, UPLOAD_URL } from "@/app/constant"; import {
CACHE_URL_PREFIX,
UPLOAD_URL,
REQUEST_TIMEOUT_MS,
} from "@/app/constant";
import { RequestMessage } from "@/app/client/api"; import { RequestMessage } from "@/app/client/api";
import Locale from "@/app/locales";
import {
EventStreamContentType,
fetchEventSource,
} from "@fortaine/fetch-event-source";
import { prettyObject } from "./format";
export function compressImage(file: Blob, maxSize: number): Promise<string> { export function compressImage(file: Blob, maxSize: number): Promise<string> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -142,3 +152,203 @@ export function removeImage(imageUrl: string) {
credentials: "include", credentials: "include",
}); });
} }
export function stream(
chatPath: string,
requestPayload: any,
headers: any,
tools: any[],
funcs: Record<string, Function>,
controller: AbortController,
parseSSE: (text: string, runTools: any[]) => string | undefined,
processToolMessage: (
requestPayload: any,
toolCallMessage: any,
toolCallResult: any[],
) => void,
options: any,
) {
let responseText = "";
let remainText = "";
let finished = false;
let running = false;
let runTools: any[] = [];
// animate response to make it looks smooth
function animateResponseText() {
if (finished || controller.signal.aborted) {
responseText += remainText;
console.log("[Response Animation] finished");
if (responseText?.length === 0) {
options.onError?.(new Error("empty response from server"));
}
return;
}
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();
const finish = () => {
if (!finished) {
if (!running && runTools.length > 0) {
const toolCallMessage = {
role: "assistant",
tool_calls: [...runTools],
};
running = true;
runTools.splice(0, runTools.length); // empty runTools
return Promise.all(
toolCallMessage.tool_calls.map((tool) => {
options?.onBeforeTool?.(tool);
return Promise.resolve(
// @ts-ignore
funcs[tool.function.name](
// @ts-ignore
tool?.function?.arguments
? JSON.parse(tool?.function?.arguments)
: {},
),
)
.then((res) => {
const content = JSON.stringify(res.data);
if (res.status >= 300) {
return Promise.reject(content);
}
return content;
})
.then((content) => {
options?.onAfterTool?.({
...tool,
content,
isError: false,
});
return content;
})
.catch((e) => {
options?.onAfterTool?.({ ...tool, isError: true });
return e.toString();
})
.then((content) => ({
role: "tool",
content,
tool_call_id: tool.id,
}));
}),
).then((toolCallResult) => {
processToolMessage(requestPayload, toolCallMessage, toolCallResult);
setTimeout(() => {
// call again
console.debug("[ChatAPI] restart");
running = false;
chatApi(chatPath, headers, requestPayload, tools); // call fetchEventSource
}, 60);
});
return;
}
if (running) {
return;
}
console.debug("[ChatAPI] end");
finished = true;
options.onFinish(responseText + remainText);
}
};
controller.signal.onabort = finish;
function chatApi(
chatPath: string,
headers: any,
requestPayload: any,
tools: any,
) {
const chatPayload = {
method: "POST",
body: JSON.stringify({
...requestPayload,
tools: tools && tools.length ? tools : undefined,
}),
signal: controller.signal,
headers,
};
const requestTimeoutId = setTimeout(
() => controller.abort(),
REQUEST_TIMEOUT_MS,
);
fetchEventSource(chatPath, {
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
console.log("[Request] response content type: ", contentType);
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 chunk = parseSSE(msg.data, runTools);
if (chunk) {
remainText += chunk;
}
} catch (e) {
console.error("[Request] parse error", text, msg, e);
}
},
onclose() {
finish();
},
onerror(e) {
options?.onError?.(e);
throw e;
},
openWhenHidden: true,
});
}
console.debug("[ChatAPI] start");
chatApi(chatPath, headers, requestPayload, tools); // call fetchEventSource
}

View File

@ -0,0 +1,47 @@
import { StateStorage } from "zustand/middleware";
import { get, set, del, clear } from "idb-keyval";
import { safeLocalStorage } from "@/app/utils";
const localStorage = safeLocalStorage();
class IndexedDBStorage implements StateStorage {
public async getItem(name: string): Promise<string | null> {
try {
const value = (await get(name)) || localStorage.getItem(name);
return value;
} catch (error) {
return localStorage.getItem(name);
}
}
public async setItem(name: string, value: string): Promise<void> {
try {
const _value = JSON.parse(value);
if (!_value?.state?._hasHydrated) {
console.warn("skip setItem", name);
return;
}
await set(name, value);
} catch (error) {
localStorage.setItem(name, value);
}
}
public async removeItem(name: string): Promise<void> {
try {
await del(name);
} catch (error) {
localStorage.removeItem(name);
}
}
public async clear(): Promise<void> {
try {
await clear();
} catch (error) {
localStorage.clear();
}
}
}
export const indexedDBStorage = new IndexedDBStorage();

View File

@ -1,7 +1,8 @@
import { create } from "zustand"; import { create } from "zustand";
import { combine, persist } from "zustand/middleware"; import { combine, persist, createJSONStorage } from "zustand/middleware";
import { Updater } from "../typing"; import { Updater } from "../typing";
import { deepClone } from "./clone"; import { deepClone } from "./clone";
import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
type SecondParam<T> = T extends ( type SecondParam<T> = T extends (
_f: infer _F, _f: infer _F,
@ -13,9 +14,11 @@ type SecondParam<T> = T extends (
type MakeUpdater<T> = { type MakeUpdater<T> = {
lastUpdateTime: number; lastUpdateTime: number;
_hasHydrated: boolean;
markUpdate: () => void; markUpdate: () => void;
update: Updater<T>; update: Updater<T>;
setHasHydrated: (state: boolean) => void;
}; };
type SetStoreState<T> = ( type SetStoreState<T> = (
@ -31,12 +34,20 @@ export function createPersistStore<T extends object, M>(
) => M, ) => M,
persistOptions: SecondParam<typeof persist<T & M & MakeUpdater<T>>>, persistOptions: SecondParam<typeof persist<T & M & MakeUpdater<T>>>,
) { ) {
persistOptions.storage = createJSONStorage(() => indexedDBStorage);
const oldOonRehydrateStorage = persistOptions?.onRehydrateStorage;
persistOptions.onRehydrateStorage = (state) => {
oldOonRehydrateStorage?.(state);
return () => state.setHasHydrated(true);
};
return create( return create(
persist( persist(
combine( combine(
{ {
...state, ...state,
lastUpdateTime: 0, lastUpdateTime: 0,
_hasHydrated: false,
}, },
(set, get) => { (set, get) => {
return { return {
@ -55,6 +66,9 @@ export function createPersistStore<T extends object, M>(
lastUpdateTime: Date.now(), lastUpdateTime: Date.now(),
}); });
}, },
setHasHydrated: (state: boolean) => {
set({ _hasHydrated: state } as Partial<T & M & MakeUpdater<T>>);
},
} as M & MakeUpdater<T>; } as M & MakeUpdater<T>;
}, },
), ),

View File

@ -65,10 +65,10 @@ if (mode !== "export") {
nextConfig.rewrites = async () => { nextConfig.rewrites = async () => {
const ret = [ const ret = [
// adjust for previous version directly using "/api/proxy/" as proxy base route // adjust for previous version directly using "/api/proxy/" as proxy base route
{ // {
source: "/api/proxy/v1/:path*", // source: "/api/proxy/v1/:path*",
destination: "https://api.openai.com/v1/:path*", // destination: "https://api.openai.com/v1/:path*",
}, // },
{ {
// https://{resource_name}.openai.azure.com/openai/deployments/{deploy_name}/chat/completions // https://{resource_name}.openai.azure.com/openai/deployments/{deploy_name}/chat/completions
source: "/api/proxy/azure/:resource_name/deployments/:deploy_name/:path*", source: "/api/proxy/azure/:resource_name/deployments/:deploy_name/:path*",

View File

@ -24,16 +24,19 @@
"@svgr/webpack": "^6.5.1", "@svgr/webpack": "^6.5.1",
"@vercel/analytics": "^0.1.11", "@vercel/analytics": "^0.1.11",
"@vercel/speed-insights": "^1.0.2", "@vercel/speed-insights": "^1.0.2",
"axios": "^1.7.5",
"emoji-picker-react": "^4.9.2", "emoji-picker-react": "^4.9.2",
"fuse.js": "^7.0.0", "fuse.js": "^7.0.0",
"heic2any": "^0.0.4", "heic2any": "^0.0.4",
"html-to-image": "^1.11.11", "html-to-image": "^1.11.11",
"idb-keyval": "^6.2.1",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"mermaid": "^10.6.1", "mermaid": "^10.6.1",
"markdown-to-txt": "^2.0.1", "markdown-to-txt": "^2.0.1",
"nanoid": "^5.0.3", "nanoid": "^5.0.3",
"next": "^14.1.1", "next": "^14.1.1",
"node-fetch": "^3.3.1", "node-fetch": "^3.3.1",
"openapi-client-axios": "^7.5.5",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
@ -49,7 +52,9 @@
"zustand": "^4.3.8" "zustand": "^4.3.8"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/api": "^1.6.0",
"@tauri-apps/cli": "1.5.11", "@tauri-apps/cli": "1.5.11",
"@types/js-yaml": "4.0.9",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.30", "@types/node": "^20.11.30",
"@types/react": "^18.2.70", "@types/react": "^18.2.70",

View File

@ -17,7 +17,7 @@ tauri-build = { version = "1.5.1", features = [] }
[dependencies] [dependencies]
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
tauri = { version = "1.5.4", features = [ tauri = { version = "1.5.4", features = [ "http-all",
"notification-all", "notification-all",
"fs-all", "fs-all",
"clipboard-all", "clipboard-all",

View File

@ -9,7 +9,7 @@
}, },
"package": { "package": {
"productName": "NextChat", "productName": "NextChat",
"version": "2.14.2" "version": "2.15.2"
}, },
"tauri": { "tauri": {
"allowlist": { "allowlist": {
@ -50,6 +50,11 @@
}, },
"notification": { "notification": {
"all": true "all": true
},
"http": {
"all": true,
"request": true,
"scope": ["https://*", "http://*"]
} }
}, },
"bundle": { "bundle": {

View File

@ -1553,6 +1553,11 @@
dependencies: dependencies:
tslib "^2.4.0" tslib "^2.4.0"
"@tauri-apps/api@^1.6.0":
version "1.6.0"
resolved "https://registry.npmjs.org/@tauri-apps/api/-/api-1.6.0.tgz#745b7e4e26782c3b2ad9510d558fa5bb2cf29186"
integrity sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg==
"@tauri-apps/cli-darwin-arm64@1.5.11": "@tauri-apps/cli-darwin-arm64@1.5.11":
version "1.5.11" version "1.5.11"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.5.11.tgz#a831f98f685148e46e8050dbdddbf4bcdda9ddc6" resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.5.11.tgz#a831f98f685148e46e8050dbdddbf4bcdda9ddc6"
@ -1684,6 +1689,11 @@
"@types/react" "*" "@types/react" "*"
hoist-non-react-statics "^3.3.0" hoist-non-react-statics "^3.3.0"
"@types/js-yaml@4.0.9":
version "4.0.9"
resolved "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2"
integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==
"@types/json-schema@*", "@types/json-schema@^7.0.8": "@types/json-schema@*", "@types/json-schema@^7.0.8":
version "7.0.12" version "7.0.12"
resolved "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" resolved "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
@ -2138,6 +2148,11 @@ astral-regex@^2.0.0:
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
available-typed-arrays@^1.0.5: available-typed-arrays@^1.0.5:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
@ -2148,6 +2163,15 @@ axe-core@^4.6.2:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.3.tgz#fc0db6fdb65cc7a80ccf85286d91d64ababa3ece" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.3.tgz#fc0db6fdb65cc7a80ccf85286d91d64ababa3ece"
integrity sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg== integrity sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg==
axios@^1.7.5:
version "1.7.5"
resolved "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz#21eed340eb5daf47d29b6e002424b3e88c8c54b1"
integrity sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
axobject-query@^3.1.1: axobject-query@^3.1.1:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1"
@ -2189,6 +2213,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
bath-es5@^3.0.3:
version "3.0.3"
resolved "https://registry.npmjs.org/bath-es5/-/bath-es5-3.0.3.tgz#4e2808e8b33b4a5e3328ec1e9032f370f042193d"
integrity sha512-PdCioDToH3t84lP40kUFCKWCOCH389Dl1kbC8FGoqOwamxsmqxxnJSXdkTOsPoNHXjem4+sJ+bbNoQm5zeCqxg==
binary-extensions@^2.0.0: binary-extensions@^2.0.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
@ -2392,6 +2421,13 @@ colorette@^2.0.19:
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
comma-separated-tokens@^2.0.0: comma-separated-tokens@^2.0.0:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
@ -2925,11 +2961,21 @@ delaunator@5:
dependencies: dependencies:
robust-predicates "^3.0.0" robust-predicates "^3.0.0"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
dequal@^2.0.0: dequal@^2.0.0:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
dereference-json-schema@^0.2.1:
version "0.2.1"
resolved "https://registry.npmjs.org/dereference-json-schema/-/dereference-json-schema-0.2.1.tgz#fcad3c98e0116f7124b0989d39d947fa318cae09"
integrity sha512-uzJsrg225owJyRQ8FNTPHIuBOdSzIZlHhss9u6W8mp7jJldHqGuLv9cULagP/E26QVJDnjtG8U7Dw139mM1ydA==
diff@^5.0.0: diff@^5.0.0:
version "5.1.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40"
@ -3548,6 +3594,11 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
follow-redirects@^1.15.6:
version "1.15.6"
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
for-each@^0.3.3: for-each@^0.3.3:
version "0.3.3" version "0.3.3"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
@ -3555,6 +3606,15 @@ for-each@^0.3.3:
dependencies: dependencies:
is-callable "^1.1.3" is-callable "^1.1.3"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
format@^0.2.0: format@^0.2.0:
version "0.2.2" version "0.2.2"
resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
@ -3926,6 +3986,11 @@ iconv-lite@0.6:
dependencies: dependencies:
safer-buffer ">= 2.1.2 < 3.0.0" safer-buffer ">= 2.1.2 < 3.0.0"
idb-keyval@^6.2.1:
version "6.2.1"
resolved "https://registry.npmmirror.com/idb-keyval/-/idb-keyval-6.2.1.tgz#94516d625346d16f56f3b33855da11bfded2db33"
integrity sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==
ignore@^5.2.0: ignore@^5.2.0:
version "5.2.4" version "5.2.4"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324"
@ -4961,7 +5026,7 @@ mime-db@1.52.0:
resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
mime-types@^2.1.27: mime-types@^2.1.12, mime-types@^2.1.27:
version "2.1.35" version "2.1.35"
resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
@ -5185,6 +5250,20 @@ onetime@^6.0.0:
dependencies: dependencies:
mimic-fn "^4.0.0" mimic-fn "^4.0.0"
openapi-client-axios@^7.5.5:
version "7.5.5"
resolved "https://registry.npmjs.org/openapi-client-axios/-/openapi-client-axios-7.5.5.tgz#4cb2bb7484ff9d1c92d9ff509db235cc35d64f38"
integrity sha512-pgCo1z+rxtYmGQXzB+N5DiXvRurTP6JqV+Ao/wtaGUMIIIM+znh3nTztps+FZS8mZgWnDHpdEzL9bWtZuWuvoA==
dependencies:
bath-es5 "^3.0.3"
dereference-json-schema "^0.2.1"
openapi-types "^12.1.3"
openapi-types@^12.1.3:
version "12.1.3"
resolved "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz#471995eb26c4b97b7bd356aacf7b91b73e777dd3"
integrity sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==
optionator@^0.9.3: optionator@^0.9.3:
version "0.9.3" version "0.9.3"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64"
@ -5327,6 +5406,11 @@ property-information@^6.0.0:
resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.2.0.tgz#b74f522c31c097b5149e3c3cb8d7f3defd986a1d" resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.2.0.tgz#b74f522c31c097b5149e3c3cb8d7f3defd986a1d"
integrity sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg== integrity sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg==
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
punycode@^2.1.0: punycode@^2.1.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.0.tgz#f67fa67c94da8f4d0cfff981aee4118064199b8f"