Merge branch 'ChatGPTNextWeb:main' into personal
This commit is contained in:
commit
16fec82d08
14
README.md
14
README.md
|
@ -30,6 +30,8 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
|
||||||
|
|
||||||
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
||||||
|
|
||||||
|
[<img src="https://github.com/user-attachments/assets/903482d4-3e87-4134-9af1-f2588fa90659" height="60" width="288" >](https://monica.im/?utm=nxcrp)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## Enterprise Edition
|
## Enterprise Edition
|
||||||
|
@ -89,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.
|
||||||
|
@ -126,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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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";
|
||||||
|
@ -56,6 +62,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 {
|
||||||
|
|
|
@ -1,6 +1,12 @@
|
||||||
import { ACCESS_CODE_PREFIX, Anthropic, ApiPath } from "@/app/constant";
|
import { ACCESS_CODE_PREFIX, Anthropic, ApiPath } from "@/app/constant";
|
||||||
import { ChatOptions, getHeaders, LLMApi, MultimodalContent } from "../api";
|
import { ChatOptions, getHeaders, LLMApi, MultimodalContent } 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 {
|
||||||
|
@ -11,8 +17,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";
|
||||||
|
@ -191,112 +198,126 @@ export class ClaudeApi implements LLMApi {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
options.onController?.(controller);
|
options.onController?.(controller);
|
||||||
|
|
||||||
const payload = {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(requestBody),
|
|
||||||
signal: controller.signal,
|
|
||||||
headers: {
|
|
||||||
...getHeaders(), // get common headers
|
|
||||||
"anthropic-version": accessStore.anthropicApiVersion,
|
|
||||||
// do not send `anthropicApiKey` in browser!!!
|
|
||||||
// Authorization: getAuthKey(accessStore.anthropicApiKey),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if (shouldStream) {
|
if (shouldStream) {
|
||||||
try {
|
let index = -1;
|
||||||
const context = {
|
const [tools, funcs] = usePluginStore
|
||||||
text: "",
|
.getState()
|
||||||
finished: false,
|
.getAsTools(
|
||||||
};
|
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||||
|
);
|
||||||
const finish = () => {
|
return stream(
|
||||||
if (!context.finished) {
|
path,
|
||||||
options.onFinish(context.text);
|
requestBody,
|
||||||
context.finished = true;
|
{
|
||||||
}
|
...getHeaders(),
|
||||||
};
|
"anthropic-version": accessStore.anthropicApiVersion,
|
||||||
|
},
|
||||||
controller.signal.onabort = finish;
|
// @ts-ignore
|
||||||
fetchEventSource(path, {
|
tools.map((tool) => ({
|
||||||
...payload,
|
name: tool?.function?.name,
|
||||||
async onopen(res) {
|
description: tool?.function?.description,
|
||||||
const contentType = res.headers.get("content-type");
|
input_schema: tool?.function?.parameters,
|
||||||
console.log("response content type: ", contentType);
|
})),
|
||||||
|
funcs,
|
||||||
if (contentType?.startsWith("text/plain")) {
|
controller,
|
||||||
context.text = await res.clone().text();
|
// parseSSE
|
||||||
return finish();
|
(text: string, runTools: ChatMessageTool[]) => {
|
||||||
}
|
// console.log("parseSSE", text, runTools);
|
||||||
|
let chunkJson:
|
||||||
if (
|
| undefined
|
||||||
!res.ok ||
|
| {
|
||||||
!res.headers
|
type: "content_block_delta" | "content_block_stop";
|
||||||
.get("content-type")
|
content_block?: {
|
||||||
?.startsWith(EventStreamContentType) ||
|
type: "tool_use";
|
||||||
res.status !== 200
|
id: string;
|
||||||
) {
|
name: string;
|
||||||
const responseTexts = [context.text];
|
|
||||||
let extraInfo = await res.clone().text();
|
|
||||||
try {
|
|
||||||
const resJson = await res.clone().json();
|
|
||||||
extraInfo = prettyObject(resJson);
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
if (res.status === 401) {
|
|
||||||
responseTexts.push(Locale.Error.Unauthorized);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extraInfo) {
|
|
||||||
responseTexts.push(extraInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.text = responseTexts.join("\n\n");
|
|
||||||
|
|
||||||
return finish();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onmessage(msg) {
|
|
||||||
let chunkJson:
|
|
||||||
| undefined
|
|
||||||
| {
|
|
||||||
type: "content_block_delta" | "content_block_stop";
|
|
||||||
delta?: {
|
|
||||||
type: "text_delta";
|
|
||||||
text: string;
|
|
||||||
};
|
|
||||||
index: number;
|
|
||||||
};
|
};
|
||||||
try {
|
delta?: {
|
||||||
chunkJson = JSON.parse(msg.data);
|
type: "text_delta" | "input_json_delta";
|
||||||
} catch (e) {
|
text?: string;
|
||||||
console.error("[Response] parse error", msg.data);
|
partial_json?: string;
|
||||||
}
|
};
|
||||||
|
index: number;
|
||||||
|
};
|
||||||
|
chunkJson = JSON.parse(text);
|
||||||
|
|
||||||
if (!chunkJson || chunkJson.type === "content_block_stop") {
|
if (chunkJson?.content_block?.type == "tool_use") {
|
||||||
return finish();
|
index += 1;
|
||||||
}
|
const id = chunkJson?.content_block.id;
|
||||||
|
const name = chunkJson?.content_block.name;
|
||||||
const { delta } = chunkJson;
|
runTools.push({
|
||||||
if (delta?.text) {
|
id,
|
||||||
context.text += delta.text;
|
type: "function",
|
||||||
options.onUpdate?.(context.text, delta.text);
|
function: {
|
||||||
}
|
name,
|
||||||
},
|
arguments: "",
|
||||||
onclose() {
|
},
|
||||||
finish();
|
});
|
||||||
},
|
}
|
||||||
onerror(e) {
|
if (
|
||||||
options.onError?.(e);
|
chunkJson?.delta?.type == "input_json_delta" &&
|
||||||
throw e;
|
chunkJson?.delta?.partial_json
|
||||||
},
|
) {
|
||||||
openWhenHidden: true,
|
// @ts-ignore
|
||||||
});
|
runTools[index]["function"]["arguments"] +=
|
||||||
} catch (e) {
|
chunkJson?.delta?.partial_json;
|
||||||
console.error("failed to chat", e);
|
}
|
||||||
options.onError?.(e as Error);
|
return chunkJson?.delta?.text;
|
||||||
}
|
},
|
||||||
|
// processToolMessage, include tool_calls message and tool call results
|
||||||
|
(
|
||||||
|
requestPayload: RequestPayload,
|
||||||
|
toolCallMessage: any,
|
||||||
|
toolCallResult: any[],
|
||||||
|
) => {
|
||||||
|
// reset index value
|
||||||
|
index = -1;
|
||||||
|
// @ts-ignore
|
||||||
|
requestPayload?.messages?.splice(
|
||||||
|
// @ts-ignore
|
||||||
|
requestPayload?.messages?.length,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
role: "assistant",
|
||||||
|
content: toolCallMessage.tool_calls.map(
|
||||||
|
(tool: ChatMessageTool) => ({
|
||||||
|
type: "tool_use",
|
||||||
|
id: tool.id,
|
||||||
|
name: tool?.function?.name,
|
||||||
|
input: tool?.function?.arguments
|
||||||
|
? JSON.parse(tool?.function?.arguments)
|
||||||
|
: {},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
// @ts-ignore
|
||||||
|
...toolCallResult.map((result) => ({
|
||||||
|
role: "user",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "tool_result",
|
||||||
|
tool_use_id: result.tool_call_id,
|
||||||
|
content: result.content,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
options,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
|
const payload = {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(requestBody),
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: {
|
||||||
|
...getHeaders(), // get common headers
|
||||||
|
"anthropic-version": accessStore.anthropicApiVersion,
|
||||||
|
// do not send `anthropicApiKey` in browser!!!
|
||||||
|
// Authorization: getAuthKey(accessStore.anthropicApiKey),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
controller.signal.onabort = () => options.onFinish("");
|
controller.signal.onabort = () => options.onFinish("");
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
@ -116,115 +122,66 @@ export class MoonshotApi implements LLMApi {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (shouldStream) {
|
if (shouldStream) {
|
||||||
let responseText = "";
|
const [tools, funcs] = usePluginStore
|
||||||
let remainText = "";
|
.getState()
|
||||||
let finished = false;
|
.getAsTools(
|
||||||
|
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||||
// animate response to make it looks smooth
|
);
|
||||||
function animateResponseText() {
|
return stream(
|
||||||
if (finished || controller.signal.aborted) {
|
chatPath,
|
||||||
responseText += remainText;
|
requestPayload,
|
||||||
console.log("[Response Animation] finished");
|
getHeaders(),
|
||||||
if (responseText?.length === 0) {
|
tools as any,
|
||||||
options.onError?.(new Error("empty response from server"));
|
funcs,
|
||||||
|
controller,
|
||||||
|
// parseSSE
|
||||||
|
(text: string, runTools: ChatMessageTool[]) => {
|
||||||
|
// console.log("parseSSE", text, runTools);
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
const choices = json.choices as Array<{
|
||||||
|
delta: {
|
||||||
|
content: string;
|
||||||
|
tool_calls: ChatMessageTool[];
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
const tool_calls = choices[0]?.delta?.tool_calls;
|
||||||
|
if (tool_calls?.length > 0) {
|
||||||
|
const index = tool_calls[0]?.index;
|
||||||
|
const id = tool_calls[0]?.id;
|
||||||
|
const args = tool_calls[0]?.function?.arguments;
|
||||||
|
if (id) {
|
||||||
|
runTools.push({
|
||||||
|
id,
|
||||||
|
type: tool_calls[0]?.type,
|
||||||
|
function: {
|
||||||
|
name: tool_calls[0]?.function?.name as string,
|
||||||
|
arguments: args,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
runTools[index]["function"]["arguments"] += args;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return choices[0]?.delta?.content;
|
||||||
}
|
},
|
||||||
|
// processToolMessage, include tool_calls message and tool call results
|
||||||
if (remainText.length > 0) {
|
(
|
||||||
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
|
requestPayload: RequestPayload,
|
||||||
const fetchText = remainText.slice(0, fetchCount);
|
toolCallMessage: any,
|
||||||
responseText += fetchText;
|
toolCallResult: any[],
|
||||||
remainText = remainText.slice(fetchCount);
|
) => {
|
||||||
options.onUpdate?.(responseText, fetchText);
|
// @ts-ignore
|
||||||
}
|
requestPayload?.messages?.splice(
|
||||||
|
// @ts-ignore
|
||||||
requestAnimationFrame(animateResponseText);
|
requestPayload?.messages?.length,
|
||||||
}
|
0,
|
||||||
|
toolCallMessage,
|
||||||
// start animaion
|
...toolCallResult,
|
||||||
animateResponseText();
|
|
||||||
|
|
||||||
const finish = () => {
|
|
||||||
if (!finished) {
|
|
||||||
finished = true;
|
|
||||||
options.onFinish(responseText + remainText);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
controller.signal.onabort = finish;
|
|
||||||
|
|
||||||
fetchEventSource(chatPath, {
|
|
||||||
...chatPayload,
|
|
||||||
async onopen(res) {
|
|
||||||
clearTimeout(requestTimeoutId);
|
|
||||||
const contentType = res.headers.get("content-type");
|
|
||||||
console.log(
|
|
||||||
"[OpenAI] request response content type: ",
|
|
||||||
contentType,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (contentType?.startsWith("text/plain")) {
|
|
||||||
responseText = await res.clone().text();
|
|
||||||
return finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!res.ok ||
|
|
||||||
!res.headers
|
|
||||||
.get("content-type")
|
|
||||||
?.startsWith(EventStreamContentType) ||
|
|
||||||
res.status !== 200
|
|
||||||
) {
|
|
||||||
const responseTexts = [responseText];
|
|
||||||
let extraInfo = await res.clone().text();
|
|
||||||
try {
|
|
||||||
const resJson = await res.clone().json();
|
|
||||||
extraInfo = prettyObject(resJson);
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
if (res.status === 401) {
|
|
||||||
responseTexts.push(Locale.Error.Unauthorized);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extraInfo) {
|
|
||||||
responseTexts.push(extraInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
responseText = responseTexts.join("\n\n");
|
|
||||||
|
|
||||||
return finish();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onmessage(msg) {
|
options,
|
||||||
if (msg.data === "[DONE]" || finished) {
|
);
|
||||||
return finish();
|
|
||||||
}
|
|
||||||
const text = msg.data;
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(text);
|
|
||||||
const choices = json.choices as Array<{
|
|
||||||
delta: { content: string };
|
|
||||||
}>;
|
|
||||||
const delta = choices[0]?.delta?.content;
|
|
||||||
const textmoderation = json?.prompt_filter_results;
|
|
||||||
|
|
||||||
if (delta) {
|
|
||||||
remainText += delta;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[Request] parse error", text, msg);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onclose() {
|
|
||||||
finish();
|
|
||||||
},
|
|
||||||
onerror(e) {
|
|
||||||
options.onError?.(e);
|
|
||||||
throw e;
|
|
||||||
},
|
|
||||||
openWhenHidden: true,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
const res = await fetch(chatPath, chatPayload);
|
const res = await fetch(chatPath, chatPayload);
|
||||||
clearTimeout(requestTimeoutId);
|
clearTimeout(requestTimeoutId);
|
||||||
|
|
|
@ -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";
|
||||||
|
@ -233,143 +240,82 @@ export class ChatGPTApi implements LLMApi {
|
||||||
isDalle3 ? OpenaiPath.ImagePath : OpenaiPath.ChatPath,
|
isDalle3 ? OpenaiPath.ImagePath : OpenaiPath.ChatPath,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const chatPayload = {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify(requestPayload),
|
|
||||||
signal: controller.signal,
|
|
||||||
headers: getHeaders(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// make a fetch request
|
|
||||||
const requestTimeoutId = setTimeout(
|
|
||||||
() => controller.abort(),
|
|
||||||
isDalle3 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
|
|
||||||
);
|
|
||||||
|
|
||||||
if (shouldStream) {
|
if (shouldStream) {
|
||||||
let responseText = "";
|
const [tools, funcs] = usePluginStore
|
||||||
let remainText = "";
|
.getState()
|
||||||
let finished = false;
|
.getAsTools(
|
||||||
|
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||||
// animate response to make it looks smooth
|
);
|
||||||
function animateResponseText() {
|
// console.log("getAsTools", tools, funcs);
|
||||||
if (finished || controller.signal.aborted) {
|
stream(
|
||||||
responseText += remainText;
|
chatPath,
|
||||||
console.log("[Response Animation] finished");
|
requestPayload,
|
||||||
if (responseText?.length === 0) {
|
getHeaders(),
|
||||||
options.onError?.(new Error("empty response from server"));
|
tools as any,
|
||||||
|
funcs,
|
||||||
|
controller,
|
||||||
|
// parseSSE
|
||||||
|
(text: string, runTools: ChatMessageTool[]) => {
|
||||||
|
// console.log("parseSSE", text, runTools);
|
||||||
|
const json = JSON.parse(text);
|
||||||
|
const choices = json.choices as Array<{
|
||||||
|
delta: {
|
||||||
|
content: string;
|
||||||
|
tool_calls: ChatMessageTool[];
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
const tool_calls = choices[0]?.delta?.tool_calls;
|
||||||
|
if (tool_calls?.length > 0) {
|
||||||
|
const index = tool_calls[0]?.index;
|
||||||
|
const id = tool_calls[0]?.id;
|
||||||
|
const args = tool_calls[0]?.function?.arguments;
|
||||||
|
if (id) {
|
||||||
|
runTools.push({
|
||||||
|
id,
|
||||||
|
type: tool_calls[0]?.type,
|
||||||
|
function: {
|
||||||
|
name: tool_calls[0]?.function?.name as string,
|
||||||
|
arguments: args,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// @ts-ignore
|
||||||
|
runTools[index]["function"]["arguments"] += args;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return;
|
return choices[0]?.delta?.content;
|
||||||
}
|
},
|
||||||
|
// processToolMessage, include tool_calls message and tool call results
|
||||||
if (remainText.length > 0) {
|
(
|
||||||
const fetchCount = Math.max(1, Math.round(remainText.length / 60));
|
requestPayload: RequestPayload,
|
||||||
const fetchText = remainText.slice(0, fetchCount);
|
toolCallMessage: any,
|
||||||
responseText += fetchText;
|
toolCallResult: any[],
|
||||||
remainText = remainText.slice(fetchCount);
|
) => {
|
||||||
options.onUpdate?.(responseText, fetchText);
|
// @ts-ignore
|
||||||
}
|
requestPayload?.messages?.splice(
|
||||||
|
// @ts-ignore
|
||||||
requestAnimationFrame(animateResponseText);
|
requestPayload?.messages?.length,
|
||||||
}
|
0,
|
||||||
|
toolCallMessage,
|
||||||
// start animaion
|
...toolCallResult,
|
||||||
animateResponseText();
|
);
|
||||||
|
},
|
||||||
const finish = () => {
|
options,
|
||||||
if (!finished) {
|
);
|
||||||
finished = true;
|
} else {
|
||||||
options.onFinish(responseText + remainText);
|
const chatPayload = {
|
||||||
}
|
method: "POST",
|
||||||
|
body: JSON.stringify(requestPayload),
|
||||||
|
signal: controller.signal,
|
||||||
|
headers: getHeaders(),
|
||||||
};
|
};
|
||||||
|
|
||||||
controller.signal.onabort = finish;
|
// make a fetch request
|
||||||
|
const requestTimeoutId = setTimeout(
|
||||||
|
() => controller.abort(),
|
||||||
|
isDalle3 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
|
||||||
|
);
|
||||||
|
|
||||||
fetchEventSource(chatPath, {
|
|
||||||
...chatPayload,
|
|
||||||
async onopen(res) {
|
|
||||||
clearTimeout(requestTimeoutId);
|
|
||||||
const contentType = res.headers.get("content-type");
|
|
||||||
console.log(
|
|
||||||
"[OpenAI] request response content type: ",
|
|
||||||
contentType,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (contentType?.startsWith("text/plain")) {
|
|
||||||
responseText = await res.clone().text();
|
|
||||||
return finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!res.ok ||
|
|
||||||
!res.headers
|
|
||||||
.get("content-type")
|
|
||||||
?.startsWith(EventStreamContentType) ||
|
|
||||||
res.status !== 200
|
|
||||||
) {
|
|
||||||
const responseTexts = [responseText];
|
|
||||||
let extraInfo = await res.clone().text();
|
|
||||||
try {
|
|
||||||
const resJson = await res.clone().json();
|
|
||||||
extraInfo = prettyObject(resJson);
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
if (res.status === 401) {
|
|
||||||
responseTexts.push(Locale.Error.Unauthorized);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (extraInfo) {
|
|
||||||
responseTexts.push(extraInfo);
|
|
||||||
}
|
|
||||||
|
|
||||||
responseText = responseTexts.join("\n\n");
|
|
||||||
|
|
||||||
return finish();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onmessage(msg) {
|
|
||||||
if (msg.data === "[DONE]" || finished) {
|
|
||||||
return finish();
|
|
||||||
}
|
|
||||||
const text = msg.data;
|
|
||||||
try {
|
|
||||||
const json = JSON.parse(text);
|
|
||||||
const choices = json.choices as Array<{
|
|
||||||
delta: { content: string };
|
|
||||||
}>;
|
|
||||||
const delta = choices[0]?.delta?.content;
|
|
||||||
const textmoderation = json?.prompt_filter_results;
|
|
||||||
|
|
||||||
if (delta) {
|
|
||||||
remainText += delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
textmoderation &&
|
|
||||||
textmoderation.length > 0 &&
|
|
||||||
ServiceProvider.Azure
|
|
||||||
) {
|
|
||||||
const contentFilterResults =
|
|
||||||
textmoderation[0]?.content_filter_results;
|
|
||||||
console.log(
|
|
||||||
`[${ServiceProvider.Azure}] [Text Moderation] flagged categories result:`,
|
|
||||||
contentFilterResults,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("[Request] parse error", text, msg);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onclose() {
|
|
||||||
finish();
|
|
||||||
},
|
|
||||||
onerror(e) {
|
|
||||||
options.onError?.(e);
|
|
||||||
throw e;
|
|
||||||
},
|
|
||||||
openWhenHidden: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const res = await fetch(chatPath, chatPayload);
|
const res = await fetch(chatPath, chatPayload);
|
||||||
clearTimeout(requestTimeoutId);
|
clearTimeout(requestTimeoutId);
|
||||||
|
|
||||||
|
@ -461,7 +407,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) {
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
import { useEffect, useState, useRef, useMemo } from "react";
|
import {
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useRef,
|
||||||
|
useMemo,
|
||||||
|
forwardRef,
|
||||||
|
useImperativeHandle,
|
||||||
|
} from "react";
|
||||||
import { useParams } from "react-router";
|
import { useParams } from "react-router";
|
||||||
import { useWindowSize } from "@/app/utils";
|
import { useWindowSize } from "@/app/utils";
|
||||||
import { IconButton } from "./button";
|
import { IconButton } from "./button";
|
||||||
|
@ -8,6 +15,7 @@ import CopyIcon from "../icons/copy.svg";
|
||||||
import DownloadIcon from "../icons/download.svg";
|
import DownloadIcon from "../icons/download.svg";
|
||||||
import GithubIcon from "../icons/github.svg";
|
import GithubIcon from "../icons/github.svg";
|
||||||
import LoadingButtonIcon from "../icons/loading.svg";
|
import LoadingButtonIcon from "../icons/loading.svg";
|
||||||
|
import ReloadButtonIcon from "../icons/reload.svg";
|
||||||
import Locale from "../locales";
|
import Locale from "../locales";
|
||||||
import { Modal, showToast } from "./ui-lib";
|
import { Modal, showToast } from "./ui-lib";
|
||||||
import { copyToClipboard, downloadAs } from "../utils";
|
import { copyToClipboard, downloadAs } from "../utils";
|
||||||
|
@ -15,73 +23,89 @@ import { Path, ApiPath, REPO_URL } from "@/app/constant";
|
||||||
import { Loading } from "./home";
|
import { Loading } from "./home";
|
||||||
import styles from "./artifacts.module.scss";
|
import styles from "./artifacts.module.scss";
|
||||||
|
|
||||||
export function HTMLPreview(props: {
|
type HTMLPreviewProps = {
|
||||||
code: string;
|
code: string;
|
||||||
autoHeight?: boolean;
|
autoHeight?: boolean;
|
||||||
height?: number | string;
|
height?: number | string;
|
||||||
onLoad?: (title?: string) => void;
|
onLoad?: (title?: string) => void;
|
||||||
}) {
|
};
|
||||||
const ref = useRef<HTMLIFrameElement>(null);
|
|
||||||
const frameId = useRef<string>(nanoid());
|
|
||||||
const [iframeHeight, setIframeHeight] = useState(600);
|
|
||||||
const [title, setTitle] = useState("");
|
|
||||||
/*
|
|
||||||
* https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an
|
|
||||||
* 1. using srcdoc
|
|
||||||
* 2. using src with dataurl:
|
|
||||||
* easy to share
|
|
||||||
* length limit (Data URIs cannot be larger than 32,768 characters.)
|
|
||||||
*/
|
|
||||||
|
|
||||||
useEffect(() => {
|
export type HTMLPreviewHander = {
|
||||||
const handleMessage = (e: any) => {
|
reload: () => void;
|
||||||
const { id, height, title } = e.data;
|
};
|
||||||
setTitle(title);
|
|
||||||
if (id == frameId.current) {
|
export const HTMLPreview = forwardRef<HTMLPreviewHander, HTMLPreviewProps>(
|
||||||
setIframeHeight(height);
|
function HTMLPreview(props, ref) {
|
||||||
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
||||||
|
const [frameId, setFrameId] = useState<string>(nanoid());
|
||||||
|
const [iframeHeight, setIframeHeight] = useState(600);
|
||||||
|
const [title, setTitle] = useState("");
|
||||||
|
/*
|
||||||
|
* https://stackoverflow.com/questions/19739001/what-is-the-difference-between-srcdoc-and-src-datatext-html-in-an
|
||||||
|
* 1. using srcdoc
|
||||||
|
* 2. using src with dataurl:
|
||||||
|
* easy to share
|
||||||
|
* length limit (Data URIs cannot be larger than 32,768 characters.)
|
||||||
|
*/
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleMessage = (e: any) => {
|
||||||
|
const { id, height, title } = e.data;
|
||||||
|
setTitle(title);
|
||||||
|
if (id == frameId) {
|
||||||
|
setIframeHeight(height);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("message", handleMessage);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("message", handleMessage);
|
||||||
|
};
|
||||||
|
}, [frameId]);
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
reload: () => {
|
||||||
|
setFrameId(nanoid());
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const height = useMemo(() => {
|
||||||
|
if (!props.autoHeight) return props.height || 600;
|
||||||
|
if (typeof props.height === "string") {
|
||||||
|
return props.height;
|
||||||
|
}
|
||||||
|
const parentHeight = props.height || 600;
|
||||||
|
return iframeHeight + 40 > parentHeight
|
||||||
|
? parentHeight
|
||||||
|
: iframeHeight + 40;
|
||||||
|
}, [props.autoHeight, props.height, iframeHeight]);
|
||||||
|
|
||||||
|
const srcDoc = useMemo(() => {
|
||||||
|
const script = `<script>new ResizeObserver((entries) => parent.postMessage({id: '${frameId}', height: entries[0].target.clientHeight}, '*')).observe(document.body)</script>`;
|
||||||
|
if (props.code.includes("<!DOCTYPE html>")) {
|
||||||
|
props.code.replace("<!DOCTYPE html>", "<!DOCTYPE html>" + script);
|
||||||
|
}
|
||||||
|
return script + props.code;
|
||||||
|
}, [props.code, frameId]);
|
||||||
|
|
||||||
|
const handleOnLoad = () => {
|
||||||
|
if (props?.onLoad) {
|
||||||
|
props.onLoad(title);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.addEventListener("message", handleMessage);
|
|
||||||
return () => {
|
|
||||||
window.removeEventListener("message", handleMessage);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const height = useMemo(() => {
|
return (
|
||||||
if (!props.autoHeight) return props.height || 600;
|
<iframe
|
||||||
if (typeof props.height === "string") {
|
className={styles["artifacts-iframe"]}
|
||||||
return props.height;
|
key={frameId}
|
||||||
}
|
ref={iframeRef}
|
||||||
const parentHeight = props.height || 600;
|
sandbox="allow-forms allow-modals allow-scripts"
|
||||||
return iframeHeight + 40 > parentHeight ? parentHeight : iframeHeight + 40;
|
style={{ height }}
|
||||||
}, [props.autoHeight, props.height, iframeHeight]);
|
srcDoc={srcDoc}
|
||||||
|
onLoad={handleOnLoad}
|
||||||
const srcDoc = useMemo(() => {
|
/>
|
||||||
const script = `<script>new ResizeObserver((entries) => parent.postMessage({id: '${frameId.current}', height: entries[0].target.clientHeight}, '*')).observe(document.body)</script>`;
|
);
|
||||||
if (props.code.includes("</head>")) {
|
},
|
||||||
props.code.replace("</head>", "</head>" + script);
|
);
|
||||||
}
|
|
||||||
return props.code + script;
|
|
||||||
}, [props.code]);
|
|
||||||
|
|
||||||
const handleOnLoad = () => {
|
|
||||||
if (props?.onLoad) {
|
|
||||||
props.onLoad(title);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<iframe
|
|
||||||
className={styles["artifacts-iframe"]}
|
|
||||||
id={frameId.current}
|
|
||||||
ref={ref}
|
|
||||||
sandbox="allow-forms allow-modals allow-scripts"
|
|
||||||
style={{ height }}
|
|
||||||
srcDoc={srcDoc}
|
|
||||||
onLoad={handleOnLoad}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ArtifactsShareButton({
|
export function ArtifactsShareButton({
|
||||||
getCode,
|
getCode,
|
||||||
|
@ -184,6 +208,7 @@ export function Artifacts() {
|
||||||
const [code, setCode] = useState("");
|
const [code, setCode] = useState("");
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [fileName, setFileName] = useState("");
|
const [fileName, setFileName] = useState("");
|
||||||
|
const previewRef = useRef<HTMLPreviewHander>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
|
@ -208,6 +233,13 @@ export function Artifacts() {
|
||||||
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
|
<a href={REPO_URL} target="_blank" rel="noopener noreferrer">
|
||||||
<IconButton bordered icon={<GithubIcon />} shadow />
|
<IconButton bordered icon={<GithubIcon />} shadow />
|
||||||
</a>
|
</a>
|
||||||
|
<IconButton
|
||||||
|
bordered
|
||||||
|
style={{ marginLeft: 20 }}
|
||||||
|
icon={<ReloadButtonIcon />}
|
||||||
|
shadow
|
||||||
|
onClick={() => previewRef.current?.reload()}
|
||||||
|
/>
|
||||||
<div className={styles["artifacts-title"]}>NextChat Artifacts</div>
|
<div className={styles["artifacts-title"]}>NextChat Artifacts</div>
|
||||||
<ArtifactsShareButton
|
<ArtifactsShareButton
|
||||||
id={id}
|
id={id}
|
||||||
|
@ -220,6 +252,7 @@ export function Artifacts() {
|
||||||
{code && (
|
{code && (
|
||||||
<HTMLPreview
|
<HTMLPreview
|
||||||
code={code}
|
code={code}
|
||||||
|
ref={previewRef}
|
||||||
autoHeight={false}
|
autoHeight={false}
|
||||||
height={"100%"}
|
height={"100%"}
|
||||||
onLoad={(title) => {
|
onLoad={(title) => {
|
||||||
|
|
|
@ -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%;
|
||||||
|
@ -630,4 +645,4 @@
|
||||||
.chat-input-send {
|
.chat-input-send {
|
||||||
bottom: 30px;
|
bottom: 30px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,6 +28,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";
|
||||||
|
|
||||||
|
@ -53,6 +54,7 @@ import {
|
||||||
useAppConfig,
|
useAppConfig,
|
||||||
DEFAULT_TOPIC,
|
DEFAULT_TOPIC,
|
||||||
ModelType,
|
ModelType,
|
||||||
|
usePluginStore,
|
||||||
} from "../store";
|
} from "../store";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
@ -64,6 +66,8 @@ import {
|
||||||
getMessageImages,
|
getMessageImages,
|
||||||
isVisionModel,
|
isVisionModel,
|
||||||
isDalle3,
|
isDalle3,
|
||||||
|
showPlugins,
|
||||||
|
safeLocalStorage,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
|
||||||
import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
|
import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
|
||||||
|
@ -95,7 +99,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";
|
||||||
|
@ -107,6 +110,8 @@ 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();
|
||||||
|
|
||||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||||
loading: () => <LoadingIcon />,
|
loading: () => <LoadingIcon />,
|
||||||
});
|
});
|
||||||
|
@ -439,6 +444,7 @@ export function ChatActions(props: {
|
||||||
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;
|
||||||
|
@ -723,30 +729,32 @@ export function ChatActions(props: {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ChatAction
|
{showPlugins(currentProviderName, currentModel) && (
|
||||||
onClick={() => setShowPluginSelector(true)}
|
<ChatAction
|
||||||
text={Locale.Plugin.Name}
|
onClick={() => {
|
||||||
icon={<PluginIcon />}
|
if (pluginStore.getAll().length == 0) {
|
||||||
/>
|
navigate(Path.Plugins);
|
||||||
|
} else {
|
||||||
|
setShowPluginSelector(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
text={Locale.Plugin.Name}
|
||||||
|
icon={<PluginIcon />}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{showPluginSelector && (
|
{showPluginSelector && (
|
||||||
<Selector
|
<Selector
|
||||||
multiple
|
multiple
|
||||||
defaultSelectedValue={chatStore.currentSession().mask?.plugin}
|
defaultSelectedValue={chatStore.currentSession().mask?.plugin}
|
||||||
items={[
|
items={pluginStore.getAll().map((item) => ({
|
||||||
{
|
title: `${item?.title}@${item?.version}`,
|
||||||
title: Locale.Plugin.Artifacts,
|
value: item?.id,
|
||||||
value: Plugin.Artifacts,
|
}))}
|
||||||
},
|
|
||||||
]}
|
|
||||||
onClose={() => setShowPluginSelector(false)}
|
onClose={() => setShowPluginSelector(false)}
|
||||||
onSelection={(s) => {
|
onSelection={(s) => {
|
||||||
const plugin = s[0];
|
|
||||||
chatStore.updateCurrentSession((session) => {
|
chatStore.updateCurrentSession((session) => {
|
||||||
session.mask.plugin = s;
|
session.mask.plugin = s as string[];
|
||||||
});
|
});
|
||||||
if (plugin) {
|
|
||||||
showToast(plugin);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -936,7 +944,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();
|
||||||
|
@ -1002,7 +1010,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;
|
||||||
}
|
}
|
||||||
|
@ -1573,11 +1581,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"}
|
||||||
|
@ -1587,7 +1615,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));
|
||||||
|
|
|
@ -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,17 @@ 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(
|
||||||
|
async () => (await import("./search-chat")).SearchChatPage,
|
||||||
|
{
|
||||||
|
loading: () => <Loading noLogo />,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const Sd = dynamic(async () => (await import("./sd")).Sd, {
|
const Sd = dynamic(async () => (await import("./sd")).Sd, {
|
||||||
loading: () => <Loading noLogo />,
|
loading: () => <Loading noLogo />,
|
||||||
});
|
});
|
||||||
|
@ -174,6 +185,8 @@ 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.Chat} element={<Chat />} />
|
<Route path={Path.Chat} element={<Chat />} />
|
||||||
<Route path={Path.Settings} element={<Settings />} />
|
<Route path={Path.Settings} element={<Settings />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
|
@ -8,14 +8,20 @@ import RehypeHighlight from "rehype-highlight";
|
||||||
import { useRef, useState, RefObject, useEffect, useMemo } from "react";
|
import { useRef, useState, RefObject, useEffect, useMemo } from "react";
|
||||||
import { copyToClipboard, useWindowSize } from "../utils";
|
import { copyToClipboard, useWindowSize } from "../utils";
|
||||||
import mermaid from "mermaid";
|
import mermaid from "mermaid";
|
||||||
|
import Locale from "../locales";
|
||||||
import LoadingIcon from "../icons/three-dots.svg";
|
import LoadingIcon from "../icons/three-dots.svg";
|
||||||
|
import ReloadButtonIcon from "../icons/reload.svg";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import { showImageModal, FullScreen } from "./ui-lib";
|
import { showImageModal, FullScreen } from "./ui-lib";
|
||||||
import { ArtifactsShareButton, HTMLPreview } from "./artifacts";
|
import {
|
||||||
import { Plugin } from "../constant";
|
ArtifactsShareButton,
|
||||||
|
HTMLPreview,
|
||||||
|
HTMLPreviewHander,
|
||||||
|
} from "./artifacts";
|
||||||
import { useChatStore } from "../store";
|
import { useChatStore } from "../store";
|
||||||
|
import { IconButton } from "./button";
|
||||||
|
|
||||||
export function Mermaid(props: { code: string }) {
|
export function Mermaid(props: { code: string }) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const [hasError, setHasError] = useState(false);
|
const [hasError, setHasError] = useState(false);
|
||||||
|
@ -64,13 +70,12 @@ export function Mermaid(props: { code: string }) {
|
||||||
|
|
||||||
export function PreCode(props: { children: any }) {
|
export function PreCode(props: { children: any }) {
|
||||||
const ref = useRef<HTMLPreElement>(null);
|
const ref = useRef<HTMLPreElement>(null);
|
||||||
const refText = ref.current?.innerText;
|
const previewRef = useRef<HTMLPreviewHander>(null);
|
||||||
const [mermaidCode, setMermaidCode] = useState("");
|
const [mermaidCode, setMermaidCode] = useState("");
|
||||||
const [htmlCode, setHtmlCode] = useState("");
|
const [htmlCode, setHtmlCode] = useState("");
|
||||||
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;
|
||||||
|
@ -79,6 +84,7 @@ export function PreCode(props: { children: any }) {
|
||||||
setMermaidCode((mermaidDom as HTMLElement).innerText);
|
setMermaidCode((mermaidDom as HTMLElement).innerText);
|
||||||
}
|
}
|
||||||
const htmlDom = ref.current.querySelector("code.language-html");
|
const htmlDom = ref.current.querySelector("code.language-html");
|
||||||
|
const refText = ref.current.querySelector("code")?.innerText;
|
||||||
if (htmlDom) {
|
if (htmlDom) {
|
||||||
setHtmlCode((htmlDom as HTMLElement).innerText);
|
setHtmlCode((htmlDom as HTMLElement).innerText);
|
||||||
} else if (refText?.startsWith("<!DOCTYPE")) {
|
} else if (refText?.startsWith("<!DOCTYPE")) {
|
||||||
|
@ -86,15 +92,7 @@ export function PreCode(props: { children: any }) {
|
||||||
}
|
}
|
||||||
}, 600);
|
}, 600);
|
||||||
|
|
||||||
useEffect(() => {
|
const enableArtifacts = session.mask?.enableArtifacts !== false;
|
||||||
setTimeout(renderArtifacts, 1);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [refText]);
|
|
||||||
|
|
||||||
const enableArtifacts = useMemo(
|
|
||||||
() => plugins?.includes(Plugin.Artifacts),
|
|
||||||
[plugins],
|
|
||||||
);
|
|
||||||
|
|
||||||
//Wrap the paragraph for plain-text
|
//Wrap the paragraph for plain-text
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -119,6 +117,7 @@ export function PreCode(props: { children: any }) {
|
||||||
codeElement.style.whiteSpace = "pre-wrap";
|
codeElement.style.whiteSpace = "pre-wrap";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
setTimeout(renderArtifacts, 1);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -145,7 +144,15 @@ export function PreCode(props: { children: any }) {
|
||||||
style={{ position: "absolute", right: 20, top: 10 }}
|
style={{ position: "absolute", right: 20, top: 10 }}
|
||||||
getCode={() => htmlCode}
|
getCode={() => htmlCode}
|
||||||
/>
|
/>
|
||||||
|
<IconButton
|
||||||
|
style={{ position: "absolute", right: 120, top: 10 }}
|
||||||
|
bordered
|
||||||
|
icon={<ReloadButtonIcon />}
|
||||||
|
shadow
|
||||||
|
onClick={() => previewRef.current?.reload()}
|
||||||
|
/>
|
||||||
<HTMLPreview
|
<HTMLPreview
|
||||||
|
ref={previewRef}
|
||||||
code={htmlCode}
|
code={htmlCode}
|
||||||
autoHeight={!document.fullscreenElement}
|
autoHeight={!document.fullscreenElement}
|
||||||
height={!document.fullscreenElement ? 600 : height}
|
height={!document.fullscreenElement ? 600 : height}
|
||||||
|
@ -156,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);
|
||||||
|
@ -175,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",
|
||||||
|
@ -182,16 +190,14 @@ function CustomCode(props: { children: any }) {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{props.children}
|
{props.children}
|
||||||
{showToggle && collapsed && (
|
|
||||||
<div
|
|
||||||
className={`show-hide-button ${
|
|
||||||
collapsed ? "collapsed" : "expanded"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<button onClick={toggleCollapsed}>查看全部</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</code>
|
</code>
|
||||||
|
{showToggle && collapsed && (
|
||||||
|
<div
|
||||||
|
className={`show-hide-button ${collapsed ? "collapsed" : "expanded"}`}
|
||||||
|
>
|
||||||
|
<button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,167 @@
|
||||||
|
import { useState, useEffect, useRef, useCallback } from "react";
|
||||||
|
import { ErrorBoundary } from "./error";
|
||||||
|
import styles from "./mask.module.scss";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { IconButton } from "./button";
|
||||||
|
import CloseIcon from "../icons/close.svg";
|
||||||
|
import EyeIcon from "../icons/eye.svg";
|
||||||
|
import Locale from "../locales";
|
||||||
|
import { Path } from "../constant";
|
||||||
|
|
||||||
|
import { useChatStore } from "../store";
|
||||||
|
|
||||||
|
type Item = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
content: string;
|
||||||
|
};
|
||||||
|
export function SearchChatPage() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const chatStore = useChatStore();
|
||||||
|
|
||||||
|
const sessions = chatStore.sessions;
|
||||||
|
const selectSession = chatStore.selectSession;
|
||||||
|
|
||||||
|
const [searchResults, setSearchResults] = useState<Item[]>([]);
|
||||||
|
|
||||||
|
const previousValueRef = useRef<string>("");
|
||||||
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const doSearch = useCallback((text: string) => {
|
||||||
|
const lowerCaseText = text.toLowerCase();
|
||||||
|
const results: Item[] = [];
|
||||||
|
|
||||||
|
sessions.forEach((session, index) => {
|
||||||
|
const fullTextContents: string[] = [];
|
||||||
|
|
||||||
|
session.messages.forEach((message) => {
|
||||||
|
const content = message.content as string;
|
||||||
|
if (!content.toLowerCase || content === "") return;
|
||||||
|
const lowerCaseContent = content.toLowerCase();
|
||||||
|
|
||||||
|
// full text search
|
||||||
|
let pos = lowerCaseContent.indexOf(lowerCaseText);
|
||||||
|
while (pos !== -1) {
|
||||||
|
const start = Math.max(0, pos - 35);
|
||||||
|
const end = Math.min(content.length, pos + lowerCaseText.length + 35);
|
||||||
|
fullTextContents.push(content.substring(start, end));
|
||||||
|
pos = lowerCaseContent.indexOf(
|
||||||
|
lowerCaseText,
|
||||||
|
pos + lowerCaseText.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fullTextContents.length > 0) {
|
||||||
|
results.push({
|
||||||
|
id: index,
|
||||||
|
name: session.topic,
|
||||||
|
content: fullTextContents.join("... "), // concat content with...
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// sort by length of matching content
|
||||||
|
results.sort((a, b) => b.content.length - a.content.length);
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const intervalId = setInterval(() => {
|
||||||
|
if (searchInputRef.current) {
|
||||||
|
const currentValue = searchInputRef.current.value;
|
||||||
|
if (currentValue !== previousValueRef.current) {
|
||||||
|
if (currentValue.length > 0) {
|
||||||
|
const result = doSearch(currentValue);
|
||||||
|
setSearchResults(result);
|
||||||
|
}
|
||||||
|
previousValueRef.current = currentValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
// Cleanup the interval on component unmount
|
||||||
|
return () => clearInterval(intervalId);
|
||||||
|
}, [doSearch]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ErrorBoundary>
|
||||||
|
<div className={styles["mask-page"]}>
|
||||||
|
{/* header */}
|
||||||
|
<div className="window-header">
|
||||||
|
<div className="window-header-title">
|
||||||
|
<div className="window-header-main-title">
|
||||||
|
{Locale.SearchChat.Page.Title}
|
||||||
|
</div>
|
||||||
|
<div className="window-header-submai-title">
|
||||||
|
{Locale.SearchChat.Page.SubTitle(searchResults.length)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="window-actions">
|
||||||
|
<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.SearchChat.Page.Search}
|
||||||
|
autoFocus
|
||||||
|
ref={searchInputRef}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
|
const searchText = e.currentTarget.value;
|
||||||
|
if (searchText.length > 0) {
|
||||||
|
const result = doSearch(searchText);
|
||||||
|
setSearchResults(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{searchResults.map((item) => (
|
||||||
|
<div
|
||||||
|
className={styles["mask-item"]}
|
||||||
|
key={item.id}
|
||||||
|
onClick={() => {
|
||||||
|
navigate(Path.Chat);
|
||||||
|
selectSession(item.id);
|
||||||
|
}}
|
||||||
|
style={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
{/** 搜索匹配的文本 */}
|
||||||
|
<div className={styles["mask-header"]}>
|
||||||
|
<div className={styles["mask-title"]}>
|
||||||
|
<div className={styles["mask-name"]}>{item.name}</div>
|
||||||
|
{item.content.slice(0, 70)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/** 操作按钮 */}
|
||||||
|
<div className={styles["mask-actions"]}>
|
||||||
|
<IconButton
|
||||||
|
icon={<EyeIcon />}
|
||||||
|
text={Locale.SearchChat.Item.View}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ErrorBoundary>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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 = "";
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
|
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`;
|
||||||
|
@ -37,10 +40,12 @@ 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",
|
||||||
Artifacts = "/artifacts",
|
Artifacts = "/artifacts",
|
||||||
|
SearchChat = "/search-chat",
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum ApiPath {
|
export enum ApiPath {
|
||||||
|
@ -69,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",
|
||||||
|
@ -244,6 +246,7 @@ 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",
|
||||||
|
@ -266,6 +269,7 @@ 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",
|
||||||
|
@ -475,4 +479,8 @@ export const internalAllowedWebDavEndpoints = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export const DEFAULT_GA_ID = "G-89WN60ZK2E";
|
export const DEFAULT_GA_ID = "G-89WN60ZK2E";
|
||||||
export const PLUGINS = [{ name: "Stable Diffusion", path: Path.Sd }];
|
export const PLUGINS = [
|
||||||
|
{ name: "Plugins", path: Path.Plugins },
|
||||||
|
{ name: "Stable Diffusion", path: Path.Sd },
|
||||||
|
{ 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 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1.2rem" height="1.2rem" viewBox="0 0 24 24"><g fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line></g></svg>
|
After Width: | Height: | Size: 285 B |
|
@ -41,7 +41,7 @@ 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>
|
||||||
|
|
|
@ -459,6 +459,21 @@ const ar: PartialLocaleType = {
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "أنت مساعد",
|
Sysmessage: "أنت مساعد",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "بحث",
|
||||||
|
Page: {
|
||||||
|
Title: "البحث في سجلات الدردشة",
|
||||||
|
Search: "أدخل كلمات البحث",
|
||||||
|
NoResult: "لم يتم العثور على نتائج",
|
||||||
|
NoData: "لا توجد بيانات",
|
||||||
|
Loading: "جارٍ التحميل",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `تم العثور على ${count} نتائج`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "عرض",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "القناع",
|
Name: "القناع",
|
||||||
Page: {
|
Page: {
|
||||||
|
|
|
@ -466,6 +466,21 @@ const bn: PartialLocaleType = {
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "আপনি একজন সহকারী",
|
Sysmessage: "আপনি একজন সহকারী",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "অনুসন্ধান",
|
||||||
|
Page: {
|
||||||
|
Title: "চ্যাট রেকর্ড অনুসন্ধান করুন",
|
||||||
|
Search: "অনুসন্ধান কীওয়ার্ড লিখুন",
|
||||||
|
NoResult: "কোন ফলাফল পাওয়া যায়নি",
|
||||||
|
NoData: "কোন তথ্য নেই",
|
||||||
|
Loading: "লোড হচ্ছে",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `${count} টি ফলাফল পাওয়া গেছে`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "দেখুন",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "মাস্ক",
|
Name: "মাস্ক",
|
||||||
Page: {
|
Page: {
|
||||||
|
|
|
@ -509,16 +509,67 @@ const cn = {
|
||||||
Clear: "上下文已清除",
|
Clear: "上下文已清除",
|
||||||
Revert: "恢复上下文",
|
Revert: "恢复上下文",
|
||||||
},
|
},
|
||||||
Plugin: {
|
|
||||||
Name: "插件",
|
|
||||||
Artifacts: "Artifacts",
|
|
||||||
},
|
|
||||||
Discovery: {
|
Discovery: {
|
||||||
Name: "发现",
|
Name: "发现",
|
||||||
},
|
},
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "你是一个助手",
|
Sysmessage: "你是一个助手",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "搜索",
|
||||||
|
Page: {
|
||||||
|
Title: "搜索聊天记录",
|
||||||
|
Search: "输入搜索关键词",
|
||||||
|
NoResult: "没有找到结果",
|
||||||
|
NoData: "没有数据",
|
||||||
|
Loading: "加载中",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `搜索到 ${count} 条结果`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
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: {
|
||||||
|
@ -553,6 +604,10 @@ const cn = {
|
||||||
Title: "隐藏预设对话",
|
Title: "隐藏预设对话",
|
||||||
SubTitle: "隐藏后预设对话不会出现在聊天界面",
|
SubTitle: "隐藏后预设对话不会出现在聊天界面",
|
||||||
},
|
},
|
||||||
|
Artifacts: {
|
||||||
|
Title: "启用Artifacts",
|
||||||
|
SubTitle: "启用之后可以直接渲染HTML页面",
|
||||||
|
},
|
||||||
Share: {
|
Share: {
|
||||||
Title: "分享此面具",
|
Title: "分享此面具",
|
||||||
SubTitle: "生成此面具的直达链接",
|
SubTitle: "生成此面具的直达链接",
|
||||||
|
|
|
@ -467,6 +467,21 @@ const cs: PartialLocaleType = {
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Jste asistent",
|
Sysmessage: "Jste asistent",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Hledat",
|
||||||
|
Page: {
|
||||||
|
Title: "Hledat v historii chatu",
|
||||||
|
Search: "Zadejte hledané klíčové slovo",
|
||||||
|
NoResult: "Nebyly nalezeny žádné výsledky",
|
||||||
|
NoData: "Žádná data",
|
||||||
|
Loading: "Načítání",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `Nalezeno ${count} výsledků`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Zobrazit",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Maska",
|
Name: "Maska",
|
||||||
Page: {
|
Page: {
|
||||||
|
|
|
@ -482,6 +482,21 @@ const de: PartialLocaleType = {
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Du bist ein Assistent",
|
Sysmessage: "Du bist ein Assistent",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Suche",
|
||||||
|
Page: {
|
||||||
|
Title: "Chatverlauf durchsuchen",
|
||||||
|
Search: "Suchbegriff eingeben",
|
||||||
|
NoResult: "Keine Ergebnisse gefunden",
|
||||||
|
NoData: "Keine Daten",
|
||||||
|
Loading: "Laden",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `${count} Ergebnisse gefunden`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Ansehen",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Masken",
|
Name: "Masken",
|
||||||
Page: {
|
Page: {
|
||||||
|
|
|
@ -517,16 +517,68 @@ const en: LocaleType = {
|
||||||
Clear: "Context Cleared",
|
Clear: "Context Cleared",
|
||||||
Revert: "Revert",
|
Revert: "Revert",
|
||||||
},
|
},
|
||||||
Plugin: {
|
|
||||||
Name: "Plugin",
|
|
||||||
Artifacts: "Artifacts",
|
|
||||||
},
|
|
||||||
Discovery: {
|
Discovery: {
|
||||||
Name: "Discovery",
|
Name: "Discovery",
|
||||||
},
|
},
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "You are an assistant that",
|
Sysmessage: "You are an assistant that",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Search",
|
||||||
|
Page: {
|
||||||
|
Title: "Search Chat History",
|
||||||
|
Search: "Enter search query to search chat history",
|
||||||
|
NoResult: "No results found",
|
||||||
|
NoData: "No data",
|
||||||
|
Loading: "Loading...",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `Found ${count} results`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
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: {
|
||||||
|
@ -561,6 +613,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",
|
||||||
|
|
|
@ -480,6 +480,21 @@ const es: PartialLocaleType = {
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Eres un asistente",
|
Sysmessage: "Eres un asistente",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Buscar",
|
||||||
|
Page: {
|
||||||
|
Title: "Buscar en el historial de chat",
|
||||||
|
Search: "Ingrese la palabra clave de búsqueda",
|
||||||
|
NoResult: "No se encontraron resultados",
|
||||||
|
NoData: "Sin datos",
|
||||||
|
Loading: "Cargando",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `Se encontraron ${count} resultados`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Ver",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Máscara",
|
Name: "Máscara",
|
||||||
Page: {
|
Page: {
|
||||||
|
|
|
@ -480,6 +480,21 @@ const fr: PartialLocaleType = {
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Vous êtes un assistant",
|
Sysmessage: "Vous êtes un assistant",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Recherche",
|
||||||
|
Page: {
|
||||||
|
Title: "Rechercher dans l'historique des discussions",
|
||||||
|
Search: "Entrez le mot-clé de recherche",
|
||||||
|
NoResult: "Aucun résultat trouvé",
|
||||||
|
NoData: "Aucune donnée",
|
||||||
|
Loading: "Chargement",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `${count} résultats trouvés`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Voir",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Masque",
|
Name: "Masque",
|
||||||
Page: {
|
Page: {
|
||||||
|
|
|
@ -470,6 +470,21 @@ const id: PartialLocaleType = {
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Anda adalah seorang asisten",
|
Sysmessage: "Anda adalah seorang asisten",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Cari",
|
||||||
|
Page: {
|
||||||
|
Title: "Cari riwayat obrolan",
|
||||||
|
Search: "Masukkan kata kunci pencarian",
|
||||||
|
NoResult: "Tidak ada hasil ditemukan",
|
||||||
|
NoData: "Tidak ada data",
|
||||||
|
Loading: "Memuat",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `Ditemukan ${count} hasil`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Lihat",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Masker",
|
Name: "Masker",
|
||||||
Page: {
|
Page: {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -481,6 +481,21 @@ const it: PartialLocaleType = {
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Sei un assistente",
|
Sysmessage: "Sei un assistente",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Cerca",
|
||||||
|
Page: {
|
||||||
|
Title: "Cerca nei messaggi",
|
||||||
|
Search: "Inserisci parole chiave per la ricerca",
|
||||||
|
NoResult: "Nessun risultato trovato",
|
||||||
|
NoData: "Nessun dato",
|
||||||
|
Loading: "Caricamento in corso",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `Trovati ${count} risultati`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Visualizza",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Maschera",
|
Name: "Maschera",
|
||||||
Page: {
|
Page: {
|
||||||
|
|
|
@ -460,9 +460,27 @@ const jp: PartialLocaleType = {
|
||||||
Plugin: {
|
Plugin: {
|
||||||
Name: "プラグイン",
|
Name: "プラグイン",
|
||||||
},
|
},
|
||||||
|
Discovery: {
|
||||||
|
Name: "発見",
|
||||||
|
},
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "あなたはアシスタントです",
|
Sysmessage: "あなたはアシスタントです",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "検索",
|
||||||
|
Page: {
|
||||||
|
Title: "チャット履歴を検索",
|
||||||
|
Search: "検索キーワードを入力",
|
||||||
|
NoResult: "結果が見つかりませんでした",
|
||||||
|
NoData: "データがありません",
|
||||||
|
Loading: "読み込み中",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `${count} 件の結果が見つかりました`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "表示",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "マスク",
|
Name: "マスク",
|
||||||
Page: {
|
Page: {
|
||||||
|
|
|
@ -458,6 +458,21 @@ const ko: PartialLocaleType = {
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "당신은 보조자입니다.",
|
Sysmessage: "당신은 보조자입니다.",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "검색",
|
||||||
|
Page: {
|
||||||
|
Title: "채팅 기록 검색",
|
||||||
|
Search: "검색어 입력",
|
||||||
|
NoResult: "결과를 찾을 수 없습니다",
|
||||||
|
NoData: "데이터가 없습니다",
|
||||||
|
Loading: "로딩 중",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `${count}개의 결과를 찾았습니다`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "보기",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "마스크",
|
Name: "마스크",
|
||||||
Page: {
|
Page: {
|
||||||
|
|
|
@ -474,6 +474,21 @@ const no: PartialLocaleType = {
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Du er en assistent",
|
Sysmessage: "Du er en assistent",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Søk",
|
||||||
|
Page: {
|
||||||
|
Title: "Søk i chatthistorikk",
|
||||||
|
Search: "Skriv inn søkeord",
|
||||||
|
NoResult: "Ingen resultater funnet",
|
||||||
|
NoData: "Ingen data",
|
||||||
|
Loading: "Laster inn",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `Fant ${count} resultater`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Vis",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Maske",
|
Name: "Maske",
|
||||||
Page: {
|
Page: {
|
||||||
|
|
|
@ -405,6 +405,21 @@ const pt: PartialLocaleType = {
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Você é um assistente que",
|
Sysmessage: "Você é um assistente que",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Pesquisar",
|
||||||
|
Page: {
|
||||||
|
Title: "Pesquisar histórico de chat",
|
||||||
|
Search: "Digite palavras-chave para pesquisa",
|
||||||
|
NoResult: "Nenhum resultado encontrado",
|
||||||
|
NoData: "Sem dados",
|
||||||
|
Loading: "Carregando",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `Encontrado ${count} resultados`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Ver",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Máscara",
|
Name: "Máscara",
|
||||||
Page: {
|
Page: {
|
||||||
|
|
|
@ -471,6 +471,21 @@ const ru: PartialLocaleType = {
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Вы - помощник",
|
Sysmessage: "Вы - помощник",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Поиск",
|
||||||
|
Page: {
|
||||||
|
Title: "Поиск в истории чатов",
|
||||||
|
Search: "Введите ключевые слова для поиска",
|
||||||
|
NoResult: "Результатов не найдено",
|
||||||
|
NoData: "Нет данных",
|
||||||
|
Loading: "Загрузка",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `Найдено ${count} результатов`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Просмотр",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Маска",
|
Name: "Маска",
|
||||||
Page: {
|
Page: {
|
||||||
|
|
|
@ -423,6 +423,21 @@ const sk: PartialLocaleType = {
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Ste asistent, ktorý",
|
Sysmessage: "Ste asistent, ktorý",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Hľadať",
|
||||||
|
Page: {
|
||||||
|
Title: "Hľadať v histórii chatu",
|
||||||
|
Search: "Zadajte kľúčové slová na vyhľadávanie",
|
||||||
|
NoResult: "Nenašli sa žiadne výsledky",
|
||||||
|
NoData: "Žiadne údaje",
|
||||||
|
Loading: "Načítava sa",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `Nájdených ${count} výsledkov`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Zobraziť",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Maska",
|
Name: "Maska",
|
||||||
Page: {
|
Page: {
|
||||||
|
|
|
@ -470,6 +470,21 @@ const tr: PartialLocaleType = {
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Sen bir asistansın",
|
Sysmessage: "Sen bir asistansın",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Ara",
|
||||||
|
Page: {
|
||||||
|
Title: "Sohbet geçmişini ara",
|
||||||
|
Search: "Arama anahtar kelimelerini girin",
|
||||||
|
NoResult: "Sonuç bulunamadı",
|
||||||
|
NoData: "Veri yok",
|
||||||
|
Loading: "Yükleniyor",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `${count} sonuç bulundu`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Görüntüle",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Maske",
|
Name: "Maske",
|
||||||
Page: {
|
Page: {
|
||||||
|
|
|
@ -452,6 +452,21 @@ const tw = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "搜索",
|
||||||
|
Page: {
|
||||||
|
Title: "搜索聊天記錄",
|
||||||
|
Search: "輸入搜索關鍵詞",
|
||||||
|
NoResult: "沒有找到結果",
|
||||||
|
NoData: "沒有數據",
|
||||||
|
Loading: "加載中",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `找到 ${count} 條結果`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "查看",
|
||||||
|
},
|
||||||
|
},
|
||||||
NewChat: {
|
NewChat: {
|
||||||
Return: "返回",
|
Return: "返回",
|
||||||
Skip: "跳過",
|
Skip: "跳過",
|
||||||
|
|
|
@ -466,6 +466,21 @@ const vi: PartialLocaleType = {
|
||||||
FineTuned: {
|
FineTuned: {
|
||||||
Sysmessage: "Bạn là một trợ lý",
|
Sysmessage: "Bạn là một trợ lý",
|
||||||
},
|
},
|
||||||
|
SearchChat: {
|
||||||
|
Name: "Tìm kiếm",
|
||||||
|
Page: {
|
||||||
|
Title: "Tìm kiếm lịch sử trò chuyện",
|
||||||
|
Search: "Nhập từ khóa tìm kiếm",
|
||||||
|
NoResult: "Không tìm thấy kết quả",
|
||||||
|
NoData: "Không có dữ liệu",
|
||||||
|
Loading: "Đang tải",
|
||||||
|
|
||||||
|
SubTitle: (count: number) => `Tìm thấy ${count} kết quả`,
|
||||||
|
},
|
||||||
|
Item: {
|
||||||
|
View: "Xem",
|
||||||
|
},
|
||||||
|
},
|
||||||
Mask: {
|
Mask: {
|
||||||
Name: "Mặt nạ",
|
Name: "Mặt nạ",
|
||||||
Page: {
|
Page: {
|
||||||
|
|
|
@ -183,7 +183,7 @@ export const useAccessStore = createPersistStore(
|
||||||
this.isValidBaidu() ||
|
this.isValidBaidu() ||
|
||||||
this.isValidByteDance() ||
|
this.isValidByteDance() ||
|
||||||
this.isValidAlibaba() ||
|
this.isValidAlibaba() ||
|
||||||
this.isValidTencent ||
|
this.isValidTencent() ||
|
||||||
this.isValidMoonshot() ||
|
this.isValidMoonshot() ||
|
||||||
this.isValidIflytek() ||
|
this.isValidIflytek() ||
|
||||||
!this.enabledAccessControl() ||
|
!this.enabledAccessControl() ||
|
||||||
|
|
|
@ -26,7 +26,22 @@ import { nanoid } from "nanoid";
|
||||||
import { createPersistStore } from "../utils/store";
|
import { createPersistStore } from "../utils/store";
|
||||||
import { collectModelsWithDefaultModel } from "../utils/model";
|
import { collectModelsWithDefaultModel } from "../utils/model";
|
||||||
import { useAccessStore } from "./access";
|
import { useAccessStore } from "./access";
|
||||||
import { isDalle3 } from "../utils";
|
import { isDalle3, safeLocalStorage } from "../utils";
|
||||||
|
import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
|
||||||
|
|
||||||
|
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 +49,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 {
|
||||||
|
@ -92,7 +108,7 @@ function createEmptySession(): ChatSession {
|
||||||
|
|
||||||
function getSummarizeModel(currentModel: string) {
|
function getSummarizeModel(currentModel: string) {
|
||||||
// if it is using gpt-* models, force to use 4o-mini to summarize
|
// if it is using gpt-* models, force to use 4o-mini to summarize
|
||||||
if (currentModel.startsWith("gpt")) {
|
if (currentModel.startsWith("gpt") || currentModel.startsWith("chatgpt")) {
|
||||||
const configStore = useAppConfig.getState();
|
const configStore = useAppConfig.getState();
|
||||||
const accessStore = useAccessStore.getState();
|
const accessStore = useAccessStore.getState();
|
||||||
const allModel = collectModelsWithDefaultModel(
|
const allModel = collectModelsWithDefaultModel(
|
||||||
|
@ -165,6 +181,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 +406,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 +479,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
|
||||||
|
@ -665,10 +699,16 @@ 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;
|
||||||
|
|
|
@ -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,
|
||||||
|
},
|
||||||
|
);
|
|
@ -305,7 +305,7 @@ pre {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
code{
|
pre {
|
||||||
.show-hide-button {
|
.show-hide-button {
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -315,7 +315,9 @@ code{
|
||||||
height: fit-content;
|
height: fit-content;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
pointer-events: none;
|
||||||
button{
|
button{
|
||||||
|
pointer-events: auto;
|
||||||
margin-top: 3em;
|
margin-top: 3em;
|
||||||
margin-bottom: 4em;
|
margin-bottom: 4em;
|
||||||
padding: 5px 16px;
|
padding: 5px 16px;
|
||||||
|
|
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,15 +24,18 @@
|
||||||
"@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",
|
||||||
"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",
|
||||||
|
@ -48,7 +51,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.1"
|
||||||
},
|
},
|
||||||
"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"
|
||||||
|
@ -4937,7 +5002,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==
|
||||||
|
@ -5161,6 +5226,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"
|
||||||
|
@ -5303,6 +5382,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