Merge pull request #5331 from ConnectAI-E/feature/plugin

Feature plugin (GPTs like action based on function call)
This commit is contained in:
Dogtiti 2024-09-06 20:06:50 +08:00 committed by GitHub
commit 2bd799fac6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
31 changed files with 1570 additions and 408 deletions

View File

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

View File

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

View File

@ -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;

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

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

View File

@ -5,7 +5,13 @@ import {
ModelProvider, ModelProvider,
ServiceProvider, ServiceProvider,
} from "../constant"; } from "../constant";
import { ChatMessage, ModelType, useAccessStore, useChatStore } from "../store"; import {
ChatMessageTool,
ChatMessage,
ModelType,
useAccessStore,
useChatStore,
} from "../store";
import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai"; import { ChatGPTApi, DalleRequestPayload } from "./platforms/openai";
import { GeminiProApi } from "./platforms/google"; import { GeminiProApi } from "./platforms/google";
import { ClaudeApi } from "./platforms/anthropic"; import { ClaudeApi } from "./platforms/anthropic";
@ -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 {

View File

@ -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 as string[],
);
const finish = () => { return stream(
if (!context.finished) { path,
options.onFinish(context.text); requestBody,
context.finished = true; {
} ...getHeaders(),
}; "anthropic-version": accessStore.anthropicApiVersion,
},
controller.signal.onabort = finish; // @ts-ignore
fetchEventSource(path, { tools.map((tool) => ({
...payload, name: tool?.function?.name,
async onopen(res) { description: tool?.function?.description,
const contentType = res.headers.get("content-type"); input_schema: tool?.function?.parameters,
console.log("response content type: ", contentType); })),
funcs,
if (contentType?.startsWith("text/plain")) { controller,
context.text = await res.clone().text(); // parseSSE
return finish(); (text: string, runTools: ChatMessageTool[]) => {
} // console.log("parseSSE", text, runTools);
let chunkJson:
if ( | undefined
!res.ok || | {
!res.headers type: "content_block_delta" | "content_block_stop";
.get("content-type") content_block?: {
?.startsWith(EventStreamContentType) || type: "tool_use";
res.status !== 200 id: string;
) { name: string;
const responseTexts = [context.text];
let extraInfo = await res.clone().text();
try {
const resJson = await res.clone().json();
extraInfo = prettyObject(resJson);
} catch {}
if (res.status === 401) {
responseTexts.push(Locale.Error.Unauthorized);
}
if (extraInfo) {
responseTexts.push(extraInfo);
}
context.text = responseTexts.join("\n\n");
return finish();
}
},
onmessage(msg) {
let chunkJson:
| undefined
| {
type: "content_block_delta" | "content_block_stop";
delta?: {
type: "text_delta";
text: string;
};
index: number;
}; };
try { delta?: {
chunkJson = JSON.parse(msg.data); type: "text_delta" | "input_json_delta";
} catch (e) { text?: string;
console.error("[Response] parse error", msg.data); partial_json?: string;
} };
index: number;
};
chunkJson = JSON.parse(text);
if (!chunkJson || chunkJson.type === "content_block_stop") { if (chunkJson?.content_block?.type == "tool_use") {
return finish(); index += 1;
} const id = chunkJson?.content_block.id;
const name = chunkJson?.content_block.name;
const { delta } = chunkJson; runTools.push({
if (delta?.text) { id,
context.text += delta.text; type: "function",
options.onUpdate?.(context.text, delta.text); function: {
} name,
}, arguments: "",
onclose() { },
finish(); });
}, }
onerror(e) { if (
options.onError?.(e); chunkJson?.delta?.type == "input_json_delta" &&
throw e; chunkJson?.delta?.partial_json
}, ) {
openWhenHidden: true, // @ts-ignore
}); runTools[index]["function"]["arguments"] +=
} catch (e) { chunkJson?.delta?.partial_json;
console.error("failed to chat", e); }
options.onError?.(e as Error); return chunkJson?.delta?.text;
} },
// processToolMessage, include tool_calls message and tool call results
(
requestPayload: RequestPayload,
toolCallMessage: any,
toolCallResult: any[],
) => {
// reset index value
index = -1;
// @ts-ignore
requestPayload?.messages?.splice(
// @ts-ignore
requestPayload?.messages?.length,
0,
{
role: "assistant",
content: toolCallMessage.tool_calls.map(
(tool: ChatMessageTool) => ({
type: "tool_use",
id: tool.id,
name: tool?.function?.name,
input: tool?.function?.arguments
? JSON.parse(tool?.function?.arguments)
: {},
}),
),
},
// @ts-ignore
...toolCallResult.map((result) => ({
role: "user",
content: [
{
type: "tool_result",
tool_use_id: result.tool_call_id,
content: result.content,
},
],
})),
);
},
options,
);
} else { } else {
const payload = {
method: "POST",
body: JSON.stringify(requestBody),
signal: controller.signal,
headers: {
...getHeaders(), // get common headers
"anthropic-version": accessStore.anthropicApiVersion,
// do not send `anthropicApiKey` in browser!!!
// Authorization: getAuthKey(accessStore.anthropicApiKey),
},
};
try { try {
controller.signal.onabort = () => options.onFinish(""); controller.signal.onabort = () => options.onFinish("");

View File

@ -8,9 +8,15 @@ import {
REQUEST_TIMEOUT_MS, REQUEST_TIMEOUT_MS,
ServiceProvider, ServiceProvider,
} from "@/app/constant"; } from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import {
useAccessStore,
useAppConfig,
useChatStore,
ChatMessageTool,
usePluginStore,
} from "@/app/store";
import { collectModelsWithDefaultModel } from "@/app/utils/model"; import { collectModelsWithDefaultModel } from "@/app/utils/model";
import { preProcessImageContent } from "@/app/utils/chat"; import { preProcessImageContent, stream } from "@/app/utils/chat";
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
import { import {
@ -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 as string[],
// animate response to make it looks smooth );
function animateResponseText() { return stream(
if (finished || controller.signal.aborted) { chatPath,
responseText += remainText; requestPayload,
console.log("[Response Animation] finished"); getHeaders(),
if (responseText?.length === 0) { tools as any,
options.onError?.(new Error("empty response from server")); funcs,
controller,
// parseSSE
(text: string, runTools: ChatMessageTool[]) => {
// console.log("parseSSE", text, runTools);
const json = JSON.parse(text);
const choices = json.choices as Array<{
delta: {
content: string;
tool_calls: ChatMessageTool[];
};
}>;
const tool_calls = choices[0]?.delta?.tool_calls;
if (tool_calls?.length > 0) {
const index = tool_calls[0]?.index;
const id = tool_calls[0]?.id;
const args = tool_calls[0]?.function?.arguments;
if (id) {
runTools.push({
id,
type: tool_calls[0]?.type,
function: {
name: tool_calls[0]?.function?.name as string,
arguments: args,
},
});
} else {
// @ts-ignore
runTools[index]["function"]["arguments"] += args;
}
} }
return; return choices[0]?.delta?.content;
} },
// processToolMessage, include tool_calls message and tool call results
if (remainText.length > 0) { (
const fetchCount = Math.max(1, Math.round(remainText.length / 60)); requestPayload: RequestPayload,
const fetchText = remainText.slice(0, fetchCount); toolCallMessage: any,
responseText += fetchText; toolCallResult: any[],
remainText = remainText.slice(fetchCount); ) => {
options.onUpdate?.(responseText, fetchText); // @ts-ignore
} requestPayload?.messages?.splice(
// @ts-ignore
requestAnimationFrame(animateResponseText); requestPayload?.messages?.length,
} 0,
toolCallMessage,
// start animaion ...toolCallResult,
animateResponseText();
const finish = () => {
if (!finished) {
finished = true;
options.onFinish(responseText + remainText);
}
};
controller.signal.onabort = finish;
fetchEventSource(chatPath, {
...chatPayload,
async onopen(res) {
clearTimeout(requestTimeoutId);
const contentType = res.headers.get("content-type");
console.log(
"[OpenAI] request response content type: ",
contentType,
); );
if (contentType?.startsWith("text/plain")) {
responseText = await res.clone().text();
return finish();
}
if (
!res.ok ||
!res.headers
.get("content-type")
?.startsWith(EventStreamContentType) ||
res.status !== 200
) {
const responseTexts = [responseText];
let extraInfo = await res.clone().text();
try {
const resJson = await res.clone().json();
extraInfo = prettyObject(resJson);
} catch {}
if (res.status === 401) {
responseTexts.push(Locale.Error.Unauthorized);
}
if (extraInfo) {
responseTexts.push(extraInfo);
}
responseText = responseTexts.join("\n\n");
return finish();
}
}, },
onmessage(msg) { options,
if (msg.data === "[DONE]" || finished) { );
return finish();
}
const text = msg.data;
try {
const json = JSON.parse(text);
const choices = json.choices as Array<{
delta: { content: string };
}>;
const delta = choices[0]?.delta?.content;
const textmoderation = json?.prompt_filter_results;
if (delta) {
remainText += delta;
}
} catch (e) {
console.error("[Request] parse error", text, msg);
}
},
onclose() {
finish();
},
onerror(e) {
options.onError?.(e);
throw e;
},
openWhenHidden: true,
});
} else { } else {
const res = await fetch(chatPath, chatPayload); const res = await fetch(chatPath, chatPayload);
clearTimeout(requestTimeoutId); clearTimeout(requestTimeoutId);

View File

@ -9,12 +9,19 @@ import {
REQUEST_TIMEOUT_MS, REQUEST_TIMEOUT_MS,
ServiceProvider, ServiceProvider,
} from "@/app/constant"; } from "@/app/constant";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import {
ChatMessageTool,
useAccessStore,
useAppConfig,
useChatStore,
usePluginStore,
} from "@/app/store";
import { collectModelsWithDefaultModel } from "@/app/utils/model"; import { collectModelsWithDefaultModel } from "@/app/utils/model";
import { import {
preProcessImageContent, preProcessImageContent,
uploadImage, uploadImage,
base64Image2Blob, base64Image2Blob,
stream,
} from "@/app/utils/chat"; } from "@/app/utils/chat";
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare"; import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
import { DalleSize, DalleQuality, DalleStyle } from "@/app/typing"; import { DalleSize, DalleQuality, DalleStyle } from "@/app/typing";
@ -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 as string[],
// 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);

View File

@ -413,6 +413,21 @@
margin-top: 5px; margin-top: 5px;
} }
.chat-message-tools {
font-size: 12px;
color: #aaa;
line-height: 1.5;
margin-top: 5px;
.chat-message-tool {
display: flex;
align-items: end;
svg {
margin-left: 5px;
margin-right: 5px;
}
}
}
.chat-message-item { .chat-message-item {
box-sizing: border-box; box-sizing: border-box;
max-width: 100%; max-width: 100%;
@ -630,4 +645,4 @@
.chat-input-send { .chat-input-send {
bottom: 30px; bottom: 30px;
} }
} }

View File

@ -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,7 @@ import {
getMessageImages, getMessageImages,
isVisionModel, isVisionModel,
isDalle3, isDalle3,
showPlugins,
} from "../utils"; } from "../utils";
import { uploadImage as uploadImageRemote } from "@/app/utils/chat"; import { uploadImage as uploadImageRemote } from "@/app/utils/chat";
@ -95,7 +98,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";
@ -439,6 +441,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 +726,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);
}
}} }}
/> />
)} )}
@ -1573,11 +1578,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"}

View File

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

View File

@ -19,7 +19,6 @@ import {
HTMLPreview, HTMLPreview,
HTMLPreviewHander, HTMLPreviewHander,
} from "./artifacts"; } from "./artifacts";
import { Plugin } from "../constant";
import { useChatStore } from "../store"; import { useChatStore } from "../store";
import { IconButton } from "./button"; import { IconButton } from "./button";
@ -77,7 +76,6 @@ export function PreCode(props: { children: any }) {
const { height } = useWindowSize(); const { height } = useWindowSize();
const chatStore = useChatStore(); const chatStore = useChatStore();
const session = chatStore.currentSession(); const session = chatStore.currentSession();
const plugins = session.mask?.plugin;
const renderArtifacts = useDebouncedCallback(() => { const renderArtifacts = useDebouncedCallback(() => {
if (!ref.current) return; if (!ref.current) return;
@ -94,10 +92,7 @@ export function PreCode(props: { children: any }) {
} }
}, 600); }, 600);
const enableArtifacts = useMemo( const enableArtifacts = session.mask?.enableArtifacts !== false;
() => plugins?.includes(Plugin.Artifacts),
[plugins],
);
//Wrap the paragraph for plain-text //Wrap the paragraph for plain-text
useEffect(() => { useEffect(() => {

View File

@ -167,6 +167,22 @@ export function MaskConfig(props: {
></input> ></input>
</ListItem> </ListItem>
<ListItem
title={Locale.Mask.Config.Artifacts.Title}
subTitle={Locale.Mask.Config.Artifacts.SubTitle}
>
<input
aria-label={Locale.Mask.Config.Artifacts.Title}
type="checkbox"
checked={props.mask.enableArtifacts !== false}
onChange={(e) => {
props.updateMask((mask) => {
mask.enableArtifacts = e.currentTarget.checked;
});
}}
></input>
</ListItem>
{!props.shouldSyncFromGlobal ? ( {!props.shouldSyncFromGlobal ? (
<ListItem <ListItem
title={Locale.Mask.Config.Share.Title} title={Locale.Mask.Config.Share.Title}

View File

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

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

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

View File

@ -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;

View File

@ -3,6 +3,7 @@ import path from "path";
export const OWNER = "ChatGPTNextWeb"; export const OWNER = "ChatGPTNextWeb";
export const REPO = "ChatGPT-Next-Web"; export const REPO = "ChatGPT-Next-Web";
export const REPO_URL = `https://github.com/${OWNER}/${REPO}`; export const REPO_URL = `https://github.com/${OWNER}/${REPO}`;
export const PLUGINS_REPO_URL = `https://github.com/${OWNER}/NextChat-Awesome-Plugins`;
export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`; export const ISSUE_URL = `https://github.com/${OWNER}/${REPO}/issues`;
export const UPDATE_URL = `${REPO_URL}#keep-updated`; export const UPDATE_URL = `${REPO_URL}#keep-updated`;
export const RELEASE_URL = `${REPO_URL}/releases`; export const RELEASE_URL = `${REPO_URL}/releases`;
@ -39,6 +40,7 @@ export enum Path {
Settings = "/settings", Settings = "/settings",
NewChat = "/new-chat", NewChat = "/new-chat",
Masks = "/masks", Masks = "/masks",
Plugins = "/plugins",
Auth = "/auth", Auth = "/auth",
Sd = "/sd", Sd = "/sd",
SdNew = "/sd-new", SdNew = "/sd-new",
@ -72,12 +74,9 @@ export enum FileName {
Prompts = "prompts.json", Prompts = "prompts.json",
} }
export enum Plugin {
Artifacts = "artifacts",
}
export enum StoreKey { export enum StoreKey {
Chat = "chat-next-web-store", Chat = "chat-next-web-store",
Plugin = "chat-next-web-plugin",
Access = "access-control", Access = "access-control",
Config = "app-config", Config = "app-config",
Mask = "mask-store", Mask = "mask-store",
@ -479,6 +478,7 @@ export const internalAllowedWebDavEndpoints = [
export const DEFAULT_GA_ID = "G-89WN60ZK2E"; export const DEFAULT_GA_ID = "G-89WN60ZK2E";
export const PLUGINS = [ export const PLUGINS = [
{ name: "Plugins", path: Path.Plugins },
{ name: "Stable Diffusion", path: Path.Sd }, { name: "Stable Diffusion", path: Path.Sd },
{ name: "Search Chat", path: Path.SearchChat }, { name: "Search Chat", path: Path.SearchChat },
]; ];

8
app/global.d.ts vendored
View File

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

View File

@ -509,10 +509,6 @@ const cn = {
Clear: "上下文已清除", Clear: "上下文已清除",
Revert: "恢复上下文", Revert: "恢复上下文",
}, },
Plugin: {
Name: "插件",
Artifacts: "Artifacts",
},
Discovery: { Discovery: {
Name: "发现", Name: "发现",
}, },
@ -534,6 +530,46 @@ const cn = {
View: "查看", View: "查看",
}, },
}, },
Plugin: {
Name: "插件",
Page: {
Title: "插件",
SubTitle: (count: number) => `${count} 个插件`,
Search: "搜索插件",
Create: "新建",
Find: "您可以在Github上找到优秀的插件",
},
Item: {
Info: (count: number) => `${count} 方法`,
View: "查看",
Edit: "编辑",
Delete: "删除",
DeleteConfirm: "确认删除?",
},
Auth: {
None: "不需要授权",
Basic: "Basic",
Bearer: "Bearer",
Custom: "自定义",
CustomHeader: "自定义参数名称",
Token: "Token",
Proxy: "使用代理",
ProxyDescription: "使用代理解决 CORS 错误",
Location: "位置",
LocationHeader: "Header",
LocationQuery: "Query",
LocationBody: "Body",
},
EditModal: {
Title: (readonly: boolean) => `编辑插件 ${readonly ? "(只读)" : ""}`,
Download: "下载",
Auth: "授权方式",
Content: "OpenAPI Schema",
Load: "从网页加载",
Method: "方法",
Error: "格式错误",
},
},
Mask: { Mask: {
Name: "面具", Name: "面具",
Page: { Page: {
@ -568,6 +604,10 @@ const cn = {
Title: "隐藏预设对话", Title: "隐藏预设对话",
SubTitle: "隐藏后预设对话不会出现在聊天界面", SubTitle: "隐藏后预设对话不会出现在聊天界面",
}, },
Artifacts: {
Title: "启用Artifacts",
SubTitle: "启用之后可以直接渲染HTML页面",
},
Share: { Share: {
Title: "分享此面具", Title: "分享此面具",
SubTitle: "生成此面具的直达链接", SubTitle: "生成此面具的直达链接",

View File

@ -517,10 +517,6 @@ const en: LocaleType = {
Clear: "Context Cleared", Clear: "Context Cleared",
Revert: "Revert", Revert: "Revert",
}, },
Plugin: {
Name: "Plugin",
Artifacts: "Artifacts",
},
Discovery: { Discovery: {
Name: "Discovery", Name: "Discovery",
}, },
@ -542,6 +538,47 @@ const en: LocaleType = {
View: "View", View: "View",
}, },
}, },
Plugin: {
Name: "Plugin",
Page: {
Title: "Plugins",
SubTitle: (count: number) => `${count} plugins`,
Search: "Search Plugin",
Create: "Create",
Find: "You can find awesome plugins on github: ",
},
Item: {
Info: (count: number) => `${count} method`,
View: "View",
Edit: "Edit",
Delete: "Delete",
DeleteConfirm: "Confirm to delete?",
},
Auth: {
None: "None",
Basic: "Basic",
Bearer: "Bearer",
Custom: "Custom",
CustomHeader: "Parameter Name",
Token: "Token",
Proxy: "Using Proxy",
ProxyDescription: "Using proxies to solve CORS error",
Location: "Location",
LocationHeader: "Header",
LocationQuery: "Query",
LocationBody: "Body",
},
EditModal: {
Title: (readonly: boolean) =>
`Edit Plugin ${readonly ? "(readonly)" : ""}`,
Download: "Download",
Auth: "Authentication Type",
Content: "OpenAPI Schema",
Load: "Load From URL",
Method: "Method",
Error: "OpenAPI Schema Error",
},
},
Mask: { Mask: {
Name: "Mask", Name: "Mask",
Page: { Page: {
@ -576,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",

View File

@ -29,12 +29,25 @@ import { useAccessStore } from "./access";
import { isDalle3 } from "../utils"; import { isDalle3 } from "../utils";
import { indexedDBStorage } from "@/app/utils/indexedDB-storage"; import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
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;
streaming?: boolean; streaming?: boolean;
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 {
@ -390,8 +403,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({

View File

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

View File

@ -2,7 +2,7 @@ import { BUILTIN_MASKS } from "../masks";
import { getLang, Lang } from "../locales"; import { getLang, Lang } from "../locales";
import { DEFAULT_TOPIC, ChatMessage } from "./chat"; import { DEFAULT_TOPIC, ChatMessage } from "./chat";
import { ModelConfig, useAppConfig } from "./config"; import { ModelConfig, useAppConfig } from "./config";
import { StoreKey, Plugin } from "../constant"; import { StoreKey } from "../constant";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
import { createPersistStore } from "../utils/store"; import { createPersistStore } from "../utils/store";
@ -17,7 +17,8 @@ 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 = {
@ -38,7 +39,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(

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

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

View File

@ -2,6 +2,9 @@ import { useEffect, useState } from "react";
import { showToast } from "./components/ui-lib"; import { showToast } from "./components/ui-lib";
import Locale from "./locales"; import Locale from "./locales";
import { RequestMessage } from "./client/api"; import { RequestMessage } from "./client/api";
import { ServiceProvider, REQUEST_TIMEOUT_MS } from "./constant";
import isObject from "lodash-es/isObject";
import { fetch as tauriFetch, Body, ResponseType } from "@tauri-apps/api/http";
export function trimTopic(topic: string) { export function trimTopic(topic: string) {
// Fix an issue where double quotes still show in the Indonesian language // Fix an issue where double quotes still show in the Indonesian language
@ -270,3 +273,48 @@ 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" });
}

View File

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

View File

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

View File

@ -24,6 +24,7 @@
"@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",
@ -34,6 +35,7 @@
"nanoid": "^5.0.3", "nanoid": "^5.0.3",
"next": "^14.1.1", "next": "^14.1.1",
"node-fetch": "^3.3.1", "node-fetch": "^3.3.1",
"openapi-client-axios": "^7.5.5",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-markdown": "^8.0.7", "react-markdown": "^8.0.7",
@ -49,7 +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",

View File

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

View File

@ -50,6 +50,11 @@
}, },
"notification": { "notification": {
"all": true "all": true
},
"http": {
"all": true,
"request": true,
"scope": ["https://*", "http://*"]
} }
}, },
"bundle": { "bundle": {

View File

@ -1553,6 +1553,11 @@
dependencies: dependencies:
tslib "^2.4.0" tslib "^2.4.0"
"@tauri-apps/api@^1.6.0":
version "1.6.0"
resolved "https://registry.npmjs.org/@tauri-apps/api/-/api-1.6.0.tgz#745b7e4e26782c3b2ad9510d558fa5bb2cf29186"
integrity sha512-rqI++FWClU5I2UBp4HXFvl+sBWkdigBkxnpJDQUWttNyG7IZP4FwQGhTNL5EOw0vI8i6eSAJ5frLqO7n7jbJdg==
"@tauri-apps/cli-darwin-arm64@1.5.11": "@tauri-apps/cli-darwin-arm64@1.5.11":
version "1.5.11" version "1.5.11"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.5.11.tgz#a831f98f685148e46e8050dbdddbf4bcdda9ddc6" resolved "https://registry.yarnpkg.com/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-1.5.11.tgz#a831f98f685148e46e8050dbdddbf4bcdda9ddc6"
@ -1684,6 +1689,11 @@
"@types/react" "*" "@types/react" "*"
hoist-non-react-statics "^3.3.0" hoist-non-react-statics "^3.3.0"
"@types/js-yaml@4.0.9":
version "4.0.9"
resolved "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz#cd82382c4f902fed9691a2ed79ec68c5898af4c2"
integrity sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==
"@types/json-schema@*", "@types/json-schema@^7.0.8": "@types/json-schema@*", "@types/json-schema@^7.0.8":
version "7.0.12" version "7.0.12"
resolved "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb" resolved "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
@ -2138,6 +2148,11 @@ astral-regex@^2.0.0:
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ== integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
asynckit@^0.4.0:
version "0.4.0"
resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
available-typed-arrays@^1.0.5: available-typed-arrays@^1.0.5:
version "1.0.5" version "1.0.5"
resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7"
@ -2148,6 +2163,15 @@ axe-core@^4.6.2:
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.3.tgz#fc0db6fdb65cc7a80ccf85286d91d64ababa3ece" resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.6.3.tgz#fc0db6fdb65cc7a80ccf85286d91d64ababa3ece"
integrity sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg== integrity sha512-/BQzOX780JhsxDnPpH4ZiyrJAzcd8AfzFPkv+89veFSr1rcMjuq2JDCwypKaPeB6ljHp9KjXhPpjgCvQlWYuqg==
axios@^1.7.5:
version "1.7.5"
resolved "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz#21eed340eb5daf47d29b6e002424b3e88c8c54b1"
integrity sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
axobject-query@^3.1.1: axobject-query@^3.1.1:
version "3.1.1" version "3.1.1"
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1" resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.1.1.tgz#3b6e5c6d4e43ca7ba51c5babf99d22a9c68485e1"
@ -2189,6 +2213,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
bath-es5@^3.0.3:
version "3.0.3"
resolved "https://registry.npmjs.org/bath-es5/-/bath-es5-3.0.3.tgz#4e2808e8b33b4a5e3328ec1e9032f370f042193d"
integrity sha512-PdCioDToH3t84lP40kUFCKWCOCH389Dl1kbC8FGoqOwamxsmqxxnJSXdkTOsPoNHXjem4+sJ+bbNoQm5zeCqxg==
binary-extensions@^2.0.0: binary-extensions@^2.0.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
@ -2392,6 +2421,13 @@ colorette@^2.0.19:
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798"
integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==
combined-stream@^1.0.8:
version "1.0.8"
resolved "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
dependencies:
delayed-stream "~1.0.0"
comma-separated-tokens@^2.0.0: comma-separated-tokens@^2.0.0:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee"
@ -2925,11 +2961,21 @@ delaunator@5:
dependencies: dependencies:
robust-predicates "^3.0.0" robust-predicates "^3.0.0"
delayed-stream@~1.0.0:
version "1.0.0"
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
dequal@^2.0.0: dequal@^2.0.0:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
dereference-json-schema@^0.2.1:
version "0.2.1"
resolved "https://registry.npmjs.org/dereference-json-schema/-/dereference-json-schema-0.2.1.tgz#fcad3c98e0116f7124b0989d39d947fa318cae09"
integrity sha512-uzJsrg225owJyRQ8FNTPHIuBOdSzIZlHhss9u6W8mp7jJldHqGuLv9cULagP/E26QVJDnjtG8U7Dw139mM1ydA==
diff@^5.0.0: diff@^5.0.0:
version "5.1.0" version "5.1.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40"
@ -3548,6 +3594,11 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
follow-redirects@^1.15.6:
version "1.15.6"
resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
for-each@^0.3.3: for-each@^0.3.3:
version "0.3.3" version "0.3.3"
resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e"
@ -3555,6 +3606,15 @@ for-each@^0.3.3:
dependencies: dependencies:
is-callable "^1.1.3" is-callable "^1.1.3"
form-data@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452"
integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==
dependencies:
asynckit "^0.4.0"
combined-stream "^1.0.8"
mime-types "^2.1.12"
format@^0.2.0: format@^0.2.0:
version "0.2.2" version "0.2.2"
resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b" resolved "https://registry.yarnpkg.com/format/-/format-0.2.2.tgz#d6170107e9efdc4ed30c9dc39016df942b5cb58b"
@ -4942,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==
@ -5166,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"
@ -5308,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"