Merge branch 'main' into tts-stt
This commit is contained in:
commit
212605a7e3
12
README.md
12
README.md
|
@ -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 服务了。
|
||||||
|
|
|
@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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") ||
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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,6 +212,114 @@ export class ClaudeApi implements LLMApi {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
options.onController?.(controller);
|
options.onController?.(controller);
|
||||||
|
|
||||||
|
if (shouldStream) {
|
||||||
|
let index = -1;
|
||||||
|
const [tools, funcs] = usePluginStore
|
||||||
|
.getState()
|
||||||
|
.getAsTools(
|
||||||
|
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||||
|
);
|
||||||
|
return stream(
|
||||||
|
path,
|
||||||
|
requestBody,
|
||||||
|
{
|
||||||
|
...getHeaders(),
|
||||||
|
"anthropic-version": accessStore.anthropicApiVersion,
|
||||||
|
},
|
||||||
|
// @ts-ignore
|
||||||
|
tools.map((tool) => ({
|
||||||
|
name: tool?.function?.name,
|
||||||
|
description: tool?.function?.description,
|
||||||
|
input_schema: tool?.function?.parameters,
|
||||||
|
})),
|
||||||
|
funcs,
|
||||||
|
controller,
|
||||||
|
// parseSSE
|
||||||
|
(text: string, runTools: ChatMessageTool[]) => {
|
||||||
|
// console.log("parseSSE", text, runTools);
|
||||||
|
let chunkJson:
|
||||||
|
| undefined
|
||||||
|
| {
|
||||||
|
type: "content_block_delta" | "content_block_stop";
|
||||||
|
content_block?: {
|
||||||
|
type: "tool_use";
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
delta?: {
|
||||||
|
type: "text_delta" | "input_json_delta";
|
||||||
|
text?: string;
|
||||||
|
partial_json?: string;
|
||||||
|
};
|
||||||
|
index: number;
|
||||||
|
};
|
||||||
|
chunkJson = JSON.parse(text);
|
||||||
|
|
||||||
|
if (chunkJson?.content_block?.type == "tool_use") {
|
||||||
|
index += 1;
|
||||||
|
const id = chunkJson?.content_block.id;
|
||||||
|
const name = chunkJson?.content_block.name;
|
||||||
|
runTools.push({
|
||||||
|
id,
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name,
|
||||||
|
arguments: "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
chunkJson?.delta?.type == "input_json_delta" &&
|
||||||
|
chunkJson?.delta?.partial_json
|
||||||
|
) {
|
||||||
|
// @ts-ignore
|
||||||
|
runTools[index]["function"]["arguments"] +=
|
||||||
|
chunkJson?.delta?.partial_json;
|
||||||
|
}
|
||||||
|
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 {
|
||||||
const payload = {
|
const payload = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(requestBody),
|
body: JSON.stringify(requestBody),
|
||||||
|
@ -217,100 +332,6 @@ export class ClaudeApi implements LLMApi {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
if (shouldStream) {
|
|
||||||
try {
|
|
||||||
const context = {
|
|
||||||
text: "",
|
|
||||||
finished: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const finish = () => {
|
|
||||||
if (!context.finished) {
|
|
||||||
options.onFinish(context.text);
|
|
||||||
context.finished = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
controller.signal.onabort = finish;
|
|
||||||
fetchEventSource(path, {
|
|
||||||
...payload,
|
|
||||||
async onopen(res) {
|
|
||||||
const contentType = res.headers.get("content-type");
|
|
||||||
console.log("response content type: ", contentType);
|
|
||||||
|
|
||||||
if (contentType?.startsWith("text/plain")) {
|
|
||||||
context.text = await res.clone().text();
|
|
||||||
return finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!res.ok ||
|
|
||||||
!res.headers
|
|
||||||
.get("content-type")
|
|
||||||
?.startsWith(EventStreamContentType) ||
|
|
||||||
res.status !== 200
|
|
||||||
) {
|
|
||||||
const responseTexts = [context.text];
|
|
||||||
let extraInfo = await res.clone().text();
|
|
||||||
try {
|
|
||||||
const resJson = await res.clone().json();
|
|
||||||
extraInfo = prettyObject(resJson);
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
if (res.status === 401) {
|
|
||||||
responseTexts.push(Locale.Error.Unauthorized);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extraInfo) {
|
|
||||||
responseTexts.push(extraInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.text = responseTexts.join("\n\n");
|
|
||||||
|
|
||||||
return finish();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onmessage(msg) {
|
|
||||||
let chunkJson:
|
|
||||||
| undefined
|
|
||||||
| {
|
|
||||||
type: "content_block_delta" | "content_block_stop";
|
|
||||||
delta?: {
|
|
||||||
type: "text_delta";
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
index: number;
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
chunkJson = JSON.parse(msg.data);
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[Response] parse error", msg.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!chunkJson || chunkJson.type === "content_block_stop") {
|
|
||||||
return finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { delta } = chunkJson;
|
|
||||||
if (delta?.text) {
|
|
||||||
context.text += delta.text;
|
|
||||||
options.onUpdate?.(context.text, delta.text);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onclose() {
|
|
||||||
finish();
|
|
||||||
},
|
|
||||||
onerror(e) {
|
|
||||||
options.onError?.(e);
|
|
||||||
throw e;
|
|
||||||
},
|
|
||||||
openWhenHidden: true,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error("failed to chat", e);
|
|
||||||
options.onError?.(e as Error);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
try {
|
try {
|
||||||
controller.signal.onabort = () => options.onFinish("");
|
controller.signal.onabort = () => options.onFinish("");
|
||||||
|
|
||||||
|
|
|
@ -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() {
|
|
||||||
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) {
|
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
|
return stream(
|
||||||
if (contentType?.startsWith("text/plain")) {
|
chatPath,
|
||||||
responseText = await res.clone().text();
|
requestPayload,
|
||||||
return finish();
|
getHeaders(),
|
||||||
}
|
tools as any,
|
||||||
|
funcs,
|
||||||
if (
|
controller,
|
||||||
!res.ok ||
|
// parseSSE
|
||||||
!res.headers
|
(text: string, runTools: ChatMessageTool[]) => {
|
||||||
.get("content-type")
|
// console.log("parseSSE", text, runTools);
|
||||||
?.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 json = JSON.parse(text);
|
||||||
const choices = json.choices as Array<{
|
const choices = json.choices as Array<{
|
||||||
delta: { content: string };
|
delta: {
|
||||||
|
content: string;
|
||||||
|
tool_calls: ChatMessageTool[];
|
||||||
|
};
|
||||||
}>;
|
}>;
|
||||||
const delta = choices[0]?.delta?.content;
|
const tool_calls = choices[0]?.delta?.tool_calls;
|
||||||
const textmoderation = json?.prompt_filter_results;
|
if (tool_calls?.length > 0) {
|
||||||
|
const index = tool_calls[0]?.index;
|
||||||
if (delta) {
|
const id = tool_calls[0]?.id;
|
||||||
remainText += delta;
|
const args = tool_calls[0]?.function?.arguments;
|
||||||
}
|
if (id) {
|
||||||
} catch (e) {
|
runTools.push({
|
||||||
console.error("[Request] parse error", text, msg);
|
id,
|
||||||
}
|
type: tool_calls[0]?.type,
|
||||||
|
function: {
|
||||||
|
name: tool_calls[0]?.function?.name as string,
|
||||||
|
arguments: args,
|
||||||
},
|
},
|
||||||
onclose() {
|
|
||||||
finish();
|
|
||||||
},
|
|
||||||
onerror(e) {
|
|
||||||
options.onError?.(e);
|
|
||||||
throw e;
|
|
||||||
},
|
|
||||||
openWhenHidden: true,
|
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
runTools[index]["function"]["arguments"] += args;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return choices[0]?.delta?.content;
|
||||||
|
},
|
||||||
|
// processToolMessage, include tool_calls message and tool call results
|
||||||
|
(
|
||||||
|
requestPayload: RequestPayload,
|
||||||
|
toolCallMessage: any,
|
||||||
|
toolCallResult: any[],
|
||||||
|
) => {
|
||||||
|
// @ts-ignore
|
||||||
|
requestPayload?.messages?.splice(
|
||||||
|
// @ts-ignore
|
||||||
|
requestPayload?.messages?.length,
|
||||||
|
0,
|
||||||
|
toolCallMessage,
|
||||||
|
...toolCallResult,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
const res = await fetch(chatPath, chatPayload);
|
const res = await fetch(chatPath, chatPayload);
|
||||||
clearTimeout(requestTimeoutId);
|
clearTimeout(requestTimeoutId);
|
||||||
|
|
|
@ -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);
|
||||||
|
if (!(isO1 && v.role === "system"))
|
||||||
messages.push({ role: v.role, content });
|
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,6 +324,69 @@ export class ChatGPTApi implements LLMApi {
|
||||||
isDalle3 ? OpenaiPath.ImagePath : OpenaiPath.ChatPath,
|
isDalle3 ? OpenaiPath.ImagePath : OpenaiPath.ChatPath,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (shouldStream) {
|
||||||
|
const [tools, funcs] = usePluginStore
|
||||||
|
.getState()
|
||||||
|
.getAsTools(
|
||||||
|
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||||
|
);
|
||||||
|
// console.log("getAsTools", tools, funcs);
|
||||||
|
stream(
|
||||||
|
chatPath,
|
||||||
|
requestPayload,
|
||||||
|
getHeaders(),
|
||||||
|
tools as any,
|
||||||
|
funcs,
|
||||||
|
controller,
|
||||||
|
// parseSSE
|
||||||
|
(text: string, runTools: ChatMessageTool[]) => {
|
||||||
|
// console.log("parseSSE", text, runTools);
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
const choices = json.choices as Array<{
|
||||||
|
delta: {
|
||||||
|
content: string;
|
||||||
|
tool_calls: ChatMessageTool[];
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
const tool_calls = choices[0]?.delta?.tool_calls;
|
||||||
|
if (tool_calls?.length > 0) {
|
||||||
|
const index = tool_calls[0]?.index;
|
||||||
|
const id = tool_calls[0]?.id;
|
||||||
|
const args = tool_calls[0]?.function?.arguments;
|
||||||
|
if (id) {
|
||||||
|
runTools.push({
|
||||||
|
id,
|
||||||
|
type: tool_calls[0]?.type,
|
||||||
|
function: {
|
||||||
|
name: tool_calls[0]?.function?.name as string,
|
||||||
|
arguments: args,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
runTools[index]["function"]["arguments"] += args;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return choices[0]?.delta?.content;
|
||||||
|
},
|
||||||
|
// processToolMessage, include tool_calls message and tool call results
|
||||||
|
(
|
||||||
|
requestPayload: RequestPayload,
|
||||||
|
toolCallMessage: any,
|
||||||
|
toolCallResult: any[],
|
||||||
|
) => {
|
||||||
|
// @ts-ignore
|
||||||
|
requestPayload?.messages?.splice(
|
||||||
|
// @ts-ignore
|
||||||
|
requestPayload?.messages?.length,
|
||||||
|
0,
|
||||||
|
toolCallMessage,
|
||||||
|
...toolCallResult,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
const chatPayload = {
|
const chatPayload = {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
body: JSON.stringify(requestPayload),
|
body: JSON.stringify(requestPayload),
|
||||||
|
@ -324,133 +397,9 @@ export class ChatGPTApi implements LLMApi {
|
||||||
// make a fetch request
|
// make a fetch request
|
||||||
const requestTimeoutId = setTimeout(
|
const requestTimeoutId = setTimeout(
|
||||||
() => controller.abort(),
|
() => controller.abort(),
|
||||||
isDalle3 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
|
isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
|
||||||
);
|
);
|
||||||
|
|
||||||
if (shouldStream) {
|
|
||||||
let responseText = "";
|
|
||||||
let remainText = "";
|
|
||||||
let finished = false;
|
|
||||||
|
|
||||||
// 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) {
|
|
||||||
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) {
|
|
||||||
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) {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
|
@ -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: {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{showPlugins(currentProviderName, currentModel) && (
|
||||||
<ChatAction
|
<ChatAction
|
||||||
onClick={() => setShowPluginSelector(true)}
|
onClick={() => {
|
||||||
|
if (pluginStore.getAll().length == 0) {
|
||||||
|
navigate(Path.Plugins);
|
||||||
|
} else {
|
||||||
|
setShowPluginSelector(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
text={Locale.Plugin.Name}
|
text={Locale.Plugin.Name}
|
||||||
icon={<PluginIcon />}
|
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 />} />
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 = "";
|
||||||
|
|
|
@ -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 },
|
||||||
];
|
];
|
||||||
|
|
|
@ -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>>;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 |
|
@ -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>
|
||||||
|
|
|
@ -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: "كلما زادت القيمة، زادت العشوائية في الردود",
|
||||||
|
|
|
@ -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: "মান বাড়ালে উত্তর বেশি এলোমেলো হবে",
|
||||||
|
|
|
@ -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: "生成此面具的直达链接",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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: "値が大きいほど応答がランダムになります",
|
||||||
|
|
|
@ -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: "값이 클수록 응답이 더 무작위적",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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: "Чем больше значение, тем более случайные ответы",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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: "值越大,回應越隨機",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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,11 +565,21 @@ 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(
|
||||||
|
0,
|
||||||
|
messages.length - modelConfig.historyMessageCount,
|
||||||
|
);
|
||||||
|
const topicMessages = messages
|
||||||
|
.slice(
|
||||||
|
startIndex < messages.length ? startIndex : messages.length - 1,
|
||||||
|
messages.length,
|
||||||
|
)
|
||||||
|
.concat(
|
||||||
createMessage({
|
createMessage({
|
||||||
role: "user",
|
role: "user",
|
||||||
content: Locale.Store.Prompt.Topic,
|
content: Locale.Store.Prompt.Topic,
|
||||||
|
@ -569,7 +588,7 @@ export const useChatStore = createPersistStore(
|
||||||
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;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -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";
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
);
|
108
app/utils.ts
108
app/utils.ts
|
@ -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.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
|
@ -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();
|
|
@ -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>;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
|
@ -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*",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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": {
|
||||||
|
|
86
yarn.lock
86
yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Reference in New Issue