mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2025-11-14 13:03:49 +08:00
Merge branch 'main' into feature/add-disable-autoscroll-option
This commit is contained in:
@@ -1,20 +1,63 @@
|
||||
import {
|
||||
ApiPath,
|
||||
DEFAULT_API_HOST,
|
||||
GoogleSafetySettingsThreshold,
|
||||
ServiceProvider,
|
||||
StoreKey,
|
||||
ApiPath,
|
||||
OPENAI_BASE_URL,
|
||||
ANTHROPIC_BASE_URL,
|
||||
GEMINI_BASE_URL,
|
||||
BAIDU_BASE_URL,
|
||||
BYTEDANCE_BASE_URL,
|
||||
ALIBABA_BASE_URL,
|
||||
TENCENT_BASE_URL,
|
||||
MOONSHOT_BASE_URL,
|
||||
STABILITY_BASE_URL,
|
||||
IFLYTEK_BASE_URL,
|
||||
DEEPSEEK_BASE_URL,
|
||||
XAI_BASE_URL,
|
||||
CHATGLM_BASE_URL,
|
||||
SILICONFLOW_BASE_URL,
|
||||
} from "../constant";
|
||||
import { getHeaders } from "../client/api";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import { ensure } from "../utils/clone";
|
||||
import { DEFAULT_CONFIG } from "./config";
|
||||
import { getModelProvider } from "../utils/model";
|
||||
|
||||
let fetchState = 0; // 0 not fetch, 1 fetching, 2 done
|
||||
|
||||
const DEFAULT_OPENAI_URL =
|
||||
getClientConfig()?.buildMode === "export"
|
||||
? DEFAULT_API_HOST + "/api/proxy/openai"
|
||||
: ApiPath.OpenAI;
|
||||
const isApp = getClientConfig()?.buildMode === "export";
|
||||
|
||||
const DEFAULT_OPENAI_URL = isApp ? OPENAI_BASE_URL : ApiPath.OpenAI;
|
||||
|
||||
const DEFAULT_GOOGLE_URL = isApp ? GEMINI_BASE_URL : ApiPath.Google;
|
||||
|
||||
const DEFAULT_ANTHROPIC_URL = isApp ? ANTHROPIC_BASE_URL : ApiPath.Anthropic;
|
||||
|
||||
const DEFAULT_BAIDU_URL = isApp ? BAIDU_BASE_URL : ApiPath.Baidu;
|
||||
|
||||
const DEFAULT_BYTEDANCE_URL = isApp ? BYTEDANCE_BASE_URL : ApiPath.ByteDance;
|
||||
|
||||
const DEFAULT_ALIBABA_URL = isApp ? ALIBABA_BASE_URL : ApiPath.Alibaba;
|
||||
|
||||
const DEFAULT_TENCENT_URL = isApp ? TENCENT_BASE_URL : ApiPath.Tencent;
|
||||
|
||||
const DEFAULT_MOONSHOT_URL = isApp ? MOONSHOT_BASE_URL : ApiPath.Moonshot;
|
||||
|
||||
const DEFAULT_STABILITY_URL = isApp ? STABILITY_BASE_URL : ApiPath.Stability;
|
||||
|
||||
const DEFAULT_IFLYTEK_URL = isApp ? IFLYTEK_BASE_URL : ApiPath.Iflytek;
|
||||
|
||||
const DEFAULT_DEEPSEEK_URL = isApp ? DEEPSEEK_BASE_URL : ApiPath.DeepSeek;
|
||||
|
||||
const DEFAULT_XAI_URL = isApp ? XAI_BASE_URL : ApiPath.XAI;
|
||||
|
||||
const DEFAULT_CHATGLM_URL = isApp ? CHATGLM_BASE_URL : ApiPath.ChatGLM;
|
||||
|
||||
const DEFAULT_SILICONFLOW_URL = isApp
|
||||
? SILICONFLOW_BASE_URL
|
||||
: ApiPath.SiliconFlow;
|
||||
|
||||
const DEFAULT_ACCESS_STATE = {
|
||||
accessCode: "",
|
||||
@@ -32,9 +75,62 @@ const DEFAULT_ACCESS_STATE = {
|
||||
azureApiVersion: "2023-08-01-preview",
|
||||
|
||||
// google ai studio
|
||||
googleUrl: "",
|
||||
googleUrl: DEFAULT_GOOGLE_URL,
|
||||
googleApiKey: "",
|
||||
googleApiVersion: "v1",
|
||||
googleSafetySettings: GoogleSafetySettingsThreshold.BLOCK_ONLY_HIGH,
|
||||
|
||||
// anthropic
|
||||
anthropicUrl: DEFAULT_ANTHROPIC_URL,
|
||||
anthropicApiKey: "",
|
||||
anthropicApiVersion: "2023-06-01",
|
||||
|
||||
// baidu
|
||||
baiduUrl: DEFAULT_BAIDU_URL,
|
||||
baiduApiKey: "",
|
||||
baiduSecretKey: "",
|
||||
|
||||
// bytedance
|
||||
bytedanceUrl: DEFAULT_BYTEDANCE_URL,
|
||||
bytedanceApiKey: "",
|
||||
|
||||
// alibaba
|
||||
alibabaUrl: DEFAULT_ALIBABA_URL,
|
||||
alibabaApiKey: "",
|
||||
|
||||
// moonshot
|
||||
moonshotUrl: DEFAULT_MOONSHOT_URL,
|
||||
moonshotApiKey: "",
|
||||
|
||||
//stability
|
||||
stabilityUrl: DEFAULT_STABILITY_URL,
|
||||
stabilityApiKey: "",
|
||||
|
||||
// tencent
|
||||
tencentUrl: DEFAULT_TENCENT_URL,
|
||||
tencentSecretKey: "",
|
||||
tencentSecretId: "",
|
||||
|
||||
// iflytek
|
||||
iflytekUrl: DEFAULT_IFLYTEK_URL,
|
||||
iflytekApiKey: "",
|
||||
iflytekApiSecret: "",
|
||||
|
||||
// deepseek
|
||||
deepseekUrl: DEFAULT_DEEPSEEK_URL,
|
||||
deepseekApiKey: "",
|
||||
|
||||
// xai
|
||||
xaiUrl: DEFAULT_XAI_URL,
|
||||
xaiApiKey: "",
|
||||
|
||||
// chatglm
|
||||
chatglmUrl: DEFAULT_CHATGLM_URL,
|
||||
chatglmApiKey: "",
|
||||
|
||||
// siliconflow
|
||||
siliconflowUrl: DEFAULT_SILICONFLOW_URL,
|
||||
siliconflowApiKey: "",
|
||||
|
||||
// server config
|
||||
needCode: true,
|
||||
@@ -43,6 +139,11 @@ const DEFAULT_ACCESS_STATE = {
|
||||
disableGPT4: false,
|
||||
disableFastLink: false,
|
||||
customModels: "",
|
||||
defaultModel: "",
|
||||
visionModels: "",
|
||||
|
||||
// tts config
|
||||
edgeTTSVoiceName: "zh-CN-YunxiNeural",
|
||||
};
|
||||
|
||||
export const useAccessStore = createPersistStore(
|
||||
@@ -54,6 +155,15 @@ export const useAccessStore = createPersistStore(
|
||||
|
||||
return get().needCode;
|
||||
},
|
||||
getVisionModels() {
|
||||
this.fetch();
|
||||
return get().visionModels;
|
||||
},
|
||||
edgeVoiceName() {
|
||||
this.fetch();
|
||||
|
||||
return get().edgeTTSVoiceName;
|
||||
},
|
||||
|
||||
isValidOpenAI() {
|
||||
return ensure(get(), ["openaiApiKey"]);
|
||||
@@ -67,6 +177,48 @@ export const useAccessStore = createPersistStore(
|
||||
return ensure(get(), ["googleApiKey"]);
|
||||
},
|
||||
|
||||
isValidAnthropic() {
|
||||
return ensure(get(), ["anthropicApiKey"]);
|
||||
},
|
||||
|
||||
isValidBaidu() {
|
||||
return ensure(get(), ["baiduApiKey", "baiduSecretKey"]);
|
||||
},
|
||||
|
||||
isValidByteDance() {
|
||||
return ensure(get(), ["bytedanceApiKey"]);
|
||||
},
|
||||
|
||||
isValidAlibaba() {
|
||||
return ensure(get(), ["alibabaApiKey"]);
|
||||
},
|
||||
|
||||
isValidTencent() {
|
||||
return ensure(get(), ["tencentSecretKey", "tencentSecretId"]);
|
||||
},
|
||||
|
||||
isValidMoonshot() {
|
||||
return ensure(get(), ["moonshotApiKey"]);
|
||||
},
|
||||
isValidIflytek() {
|
||||
return ensure(get(), ["iflytekApiKey"]);
|
||||
},
|
||||
isValidDeepSeek() {
|
||||
return ensure(get(), ["deepseekApiKey"]);
|
||||
},
|
||||
|
||||
isValidXAI() {
|
||||
return ensure(get(), ["xaiApiKey"]);
|
||||
},
|
||||
|
||||
isValidChatGLM() {
|
||||
return ensure(get(), ["chatglmApiKey"]);
|
||||
},
|
||||
|
||||
isValidSiliconFlow() {
|
||||
return ensure(get(), ["siliconflowApiKey"]);
|
||||
},
|
||||
|
||||
isAuthorized() {
|
||||
this.fetch();
|
||||
|
||||
@@ -75,6 +227,17 @@ export const useAccessStore = createPersistStore(
|
||||
this.isValidOpenAI() ||
|
||||
this.isValidAzure() ||
|
||||
this.isValidGoogle() ||
|
||||
this.isValidAnthropic() ||
|
||||
this.isValidBaidu() ||
|
||||
this.isValidByteDance() ||
|
||||
this.isValidAlibaba() ||
|
||||
this.isValidTencent() ||
|
||||
this.isValidMoonshot() ||
|
||||
this.isValidIflytek() ||
|
||||
this.isValidDeepSeek() ||
|
||||
this.isValidXAI() ||
|
||||
this.isValidChatGLM() ||
|
||||
this.isValidSiliconFlow() ||
|
||||
!this.enabledAccessControl() ||
|
||||
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
|
||||
);
|
||||
@@ -90,6 +253,16 @@ export const useAccessStore = createPersistStore(
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
const defaultModel = res.defaultModel ?? "";
|
||||
if (defaultModel !== "") {
|
||||
const [model, providerName] = getModelProvider(defaultModel);
|
||||
DEFAULT_CONFIG.modelConfig.model = model;
|
||||
DEFAULT_CONFIG.modelConfig.providerName = providerName as any;
|
||||
}
|
||||
|
||||
return res;
|
||||
})
|
||||
.then((res: DangerConfig) => {
|
||||
console.log("[Config] got config from server", res);
|
||||
set(() => ({ ...res }));
|
||||
|
||||
@@ -1,24 +1,58 @@
|
||||
import { trimTopic } from "../utils";
|
||||
import {
|
||||
getMessageTextContent,
|
||||
isDalle3,
|
||||
safeLocalStorage,
|
||||
trimTopic,
|
||||
} from "../utils";
|
||||
|
||||
import Locale, { getLang } from "../locales";
|
||||
import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
|
||||
import { nanoid } from "nanoid";
|
||||
import type {
|
||||
ClientApi,
|
||||
MultimodalContent,
|
||||
RequestMessage,
|
||||
} from "../client/api";
|
||||
import { getClientApi } from "../client/api";
|
||||
import { ChatControllerPool } from "../client/controller";
|
||||
import { showToast } from "../components/ui-lib";
|
||||
import { ModelConfig, ModelType, useAppConfig } from "./config";
|
||||
import { createEmptyMask, Mask } from "./mask";
|
||||
import {
|
||||
DEFAULT_INPUT_TEMPLATE,
|
||||
DEFAULT_MODELS,
|
||||
DEFAULT_SYSTEM_TEMPLATE,
|
||||
GEMINI_SUMMARIZE_MODEL,
|
||||
DEEPSEEK_SUMMARIZE_MODEL,
|
||||
KnowledgeCutOffDate,
|
||||
ModelProvider,
|
||||
MCP_SYSTEM_TEMPLATE,
|
||||
MCP_TOOLS_TEMPLATE,
|
||||
ServiceProvider,
|
||||
StoreKey,
|
||||
SUMMARIZE_MODEL,
|
||||
} from "../constant";
|
||||
import { ClientApi, RequestMessage } from "../client/api";
|
||||
import { ChatControllerPool } from "../client/controller";
|
||||
import Locale, { getLang } from "../locales";
|
||||
import { prettyObject } from "../utils/format";
|
||||
import { estimateTokenLength } from "../utils/token";
|
||||
import { nanoid } from "nanoid";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import { estimateTokenLength } from "../utils/token";
|
||||
import { ModelConfig, ModelType, useAppConfig } from "./config";
|
||||
import { useAccessStore } from "./access";
|
||||
import { collectModelsWithDefaultModel } from "../utils/model";
|
||||
import { createEmptyMask, Mask } from "./mask";
|
||||
import { executeMcpAction, getAllTools, isMcpEnabled } from "../mcp/actions";
|
||||
import { extractMcpJson, isMcpJson } from "../mcp/utils";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
|
||||
export type ChatMessageTool = {
|
||||
id: string;
|
||||
index?: number;
|
||||
type?: string;
|
||||
function?: {
|
||||
name: string;
|
||||
arguments?: string;
|
||||
};
|
||||
content?: string;
|
||||
isError?: boolean;
|
||||
errorMsg?: string;
|
||||
};
|
||||
|
||||
export type ChatMessage = RequestMessage & {
|
||||
date: string;
|
||||
@@ -26,6 +60,9 @@ export type ChatMessage = RequestMessage & {
|
||||
isError?: boolean;
|
||||
id: string;
|
||||
model?: ModelType;
|
||||
tools?: ChatMessageTool[];
|
||||
audio_url?: string;
|
||||
isMcpResponse?: boolean;
|
||||
};
|
||||
|
||||
export function createMessage(override: Partial<ChatMessage>): ChatMessage {
|
||||
@@ -82,13 +119,43 @@ function createEmptySession(): ChatSession {
|
||||
};
|
||||
}
|
||||
|
||||
function getSummarizeModel(currentModel: string) {
|
||||
// if it is using gpt-* models, force to use 3.5 to summarize
|
||||
return currentModel.startsWith("gpt") ? SUMMARIZE_MODEL : currentModel;
|
||||
function getSummarizeModel(
|
||||
currentModel: string,
|
||||
providerName: string,
|
||||
): string[] {
|
||||
// if it is using gpt-* models, force to use 4o-mini to summarize
|
||||
if (currentModel.startsWith("gpt") || currentModel.startsWith("chatgpt")) {
|
||||
const configStore = useAppConfig.getState();
|
||||
const accessStore = useAccessStore.getState();
|
||||
const allModel = collectModelsWithDefaultModel(
|
||||
configStore.models,
|
||||
[configStore.customModels, accessStore.customModels].join(","),
|
||||
accessStore.defaultModel,
|
||||
);
|
||||
const summarizeModel = allModel.find(
|
||||
(m) => m.name === SUMMARIZE_MODEL && m.available,
|
||||
);
|
||||
if (summarizeModel) {
|
||||
return [
|
||||
summarizeModel.name,
|
||||
summarizeModel.provider?.providerName as string,
|
||||
];
|
||||
}
|
||||
}
|
||||
if (currentModel.startsWith("gemini")) {
|
||||
return [GEMINI_SUMMARIZE_MODEL, ServiceProvider.Google];
|
||||
} else if (currentModel.startsWith("deepseek-")) {
|
||||
return [DEEPSEEK_SUMMARIZE_MODEL, ServiceProvider.DeepSeek];
|
||||
}
|
||||
|
||||
return [currentModel, providerName];
|
||||
}
|
||||
|
||||
function countMessages(msgs: ChatMessage[]) {
|
||||
return msgs.reduce((pre, cur) => pre + estimateTokenLength(cur.content), 0);
|
||||
return msgs.reduce(
|
||||
(pre, cur) => pre + estimateTokenLength(getMessageTextContent(cur)),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
function fillTemplateWith(input: string, modelConfig: ModelConfig) {
|
||||
@@ -109,13 +176,18 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
|
||||
ServiceProvider: serviceProvider,
|
||||
cutoff,
|
||||
model: modelConfig.model,
|
||||
time: new Date().toLocaleString(),
|
||||
time: new Date().toString(),
|
||||
lang: getLang(),
|
||||
input: input,
|
||||
};
|
||||
|
||||
let output = modelConfig.template ?? DEFAULT_INPUT_TEMPLATE;
|
||||
|
||||
// remove duplicate
|
||||
if (input.startsWith(output)) {
|
||||
output = "";
|
||||
}
|
||||
|
||||
// must contains {{input}}
|
||||
const inputVar = "{{input}}";
|
||||
if (!output.includes(inputVar)) {
|
||||
@@ -130,9 +202,31 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
|
||||
return output;
|
||||
}
|
||||
|
||||
async function getMcpSystemPrompt(): Promise<string> {
|
||||
const tools = await getAllTools();
|
||||
|
||||
let toolsStr = "";
|
||||
|
||||
tools.forEach((i) => {
|
||||
// error client has no tools
|
||||
if (!i.tools) return;
|
||||
|
||||
toolsStr += MCP_TOOLS_TEMPLATE.replace(
|
||||
"{{ clientId }}",
|
||||
i.clientId,
|
||||
).replace(
|
||||
"{{ tools }}",
|
||||
i.tools.tools.map((p: object) => JSON.stringify(p, null, 2)).join("\n"),
|
||||
);
|
||||
});
|
||||
|
||||
return MCP_SYSTEM_TEMPLATE.replace("{{ MCP_TOOLS }}", toolsStr);
|
||||
}
|
||||
|
||||
const DEFAULT_CHAT_STATE = {
|
||||
sessions: [createEmptySession()],
|
||||
currentSessionIndex: 0,
|
||||
lastInput: "",
|
||||
};
|
||||
|
||||
export const useChatStore = createPersistStore(
|
||||
@@ -146,6 +240,32 @@ export const useChatStore = createPersistStore(
|
||||
}
|
||||
|
||||
const methods = {
|
||||
forkSession() {
|
||||
// 获取当前会话
|
||||
const currentSession = get().currentSession();
|
||||
if (!currentSession) return;
|
||||
|
||||
const newSession = createEmptySession();
|
||||
|
||||
newSession.topic = currentSession.topic;
|
||||
// 深拷贝消息
|
||||
newSession.messages = currentSession.messages.map((msg) => ({
|
||||
...msg,
|
||||
id: nanoid(), // 生成新的消息 ID
|
||||
}));
|
||||
newSession.mask = {
|
||||
...currentSession.mask,
|
||||
modelConfig: {
|
||||
...currentSession.mask.modelConfig,
|
||||
},
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
currentSessionIndex: 0,
|
||||
sessions: [newSession, ...state.sessions],
|
||||
}));
|
||||
},
|
||||
|
||||
clearSessions() {
|
||||
set(() => ({
|
||||
sessions: [createEmptySession()],
|
||||
@@ -271,25 +391,46 @@ export const useChatStore = createPersistStore(
|
||||
return session;
|
||||
},
|
||||
|
||||
onNewMessage(message: ChatMessage) {
|
||||
get().updateCurrentSession((session) => {
|
||||
onNewMessage(message: ChatMessage, targetSession: ChatSession) {
|
||||
get().updateTargetSession(targetSession, (session) => {
|
||||
session.messages = session.messages.concat();
|
||||
session.lastUpdate = Date.now();
|
||||
});
|
||||
get().updateStat(message);
|
||||
get().summarizeSession();
|
||||
|
||||
get().updateStat(message, targetSession);
|
||||
|
||||
get().checkMcpJson(message);
|
||||
|
||||
get().summarizeSession(false, targetSession);
|
||||
},
|
||||
|
||||
async onUserInput(content: string) {
|
||||
async onUserInput(
|
||||
content: string,
|
||||
attachImages?: string[],
|
||||
isMcpResponse?: boolean,
|
||||
) {
|
||||
const session = get().currentSession();
|
||||
const modelConfig = session.mask.modelConfig;
|
||||
|
||||
const userContent = fillTemplateWith(content, modelConfig);
|
||||
console.log("[User Input] after template: ", userContent);
|
||||
// MCP Response no need to fill template
|
||||
let mContent: string | MultimodalContent[] = isMcpResponse
|
||||
? content
|
||||
: fillTemplateWith(content, modelConfig);
|
||||
|
||||
const userMessage: ChatMessage = createMessage({
|
||||
if (!isMcpResponse && attachImages && attachImages.length > 0) {
|
||||
mContent = [
|
||||
...(content ? [{ type: "text" as const, text: content }] : []),
|
||||
...attachImages.map((url) => ({
|
||||
type: "image_url" as const,
|
||||
image_url: { url },
|
||||
})),
|
||||
];
|
||||
}
|
||||
|
||||
let userMessage: ChatMessage = createMessage({
|
||||
role: "user",
|
||||
content: userContent,
|
||||
content: mContent,
|
||||
isMcpResponse,
|
||||
});
|
||||
|
||||
const botMessage: ChatMessage = createMessage({
|
||||
@@ -299,15 +440,15 @@ export const useChatStore = createPersistStore(
|
||||
});
|
||||
|
||||
// get recent messages
|
||||
const recentMessages = get().getMessagesWithMemory();
|
||||
const recentMessages = await get().getMessagesWithMemory();
|
||||
const sendMessages = recentMessages.concat(userMessage);
|
||||
const messageIndex = get().currentSession().messages.length + 1;
|
||||
const messageIndex = session.messages.length + 1;
|
||||
|
||||
// save user's and bot's message
|
||||
get().updateCurrentSession((session) => {
|
||||
get().updateTargetSession(session, (session) => {
|
||||
const savedUserMessage = {
|
||||
...userMessage,
|
||||
content,
|
||||
content: mContent,
|
||||
};
|
||||
session.messages = session.messages.concat([
|
||||
savedUserMessage,
|
||||
@@ -315,13 +456,7 @@ export const useChatStore = createPersistStore(
|
||||
]);
|
||||
});
|
||||
|
||||
var api: ClientApi;
|
||||
if (modelConfig.model.startsWith("gemini")) {
|
||||
api = new ClientApi(ModelProvider.GeminiPro);
|
||||
} else {
|
||||
api = new ClientApi(ModelProvider.GPT);
|
||||
}
|
||||
|
||||
const api: ClientApi = getClientApi(modelConfig.providerName);
|
||||
// make request
|
||||
api.llm.chat({
|
||||
messages: sendMessages,
|
||||
@@ -331,20 +466,37 @@ export const useChatStore = createPersistStore(
|
||||
if (message) {
|
||||
botMessage.content = message;
|
||||
}
|
||||
get().updateCurrentSession((session) => {
|
||||
get().updateTargetSession(session, (session) => {
|
||||
session.messages = session.messages.concat();
|
||||
});
|
||||
},
|
||||
onFinish(message) {
|
||||
async onFinish(message) {
|
||||
botMessage.streaming = false;
|
||||
if (message) {
|
||||
botMessage.content = message;
|
||||
get().onNewMessage(botMessage);
|
||||
botMessage.date = new Date().toLocaleString();
|
||||
get().onNewMessage(botMessage, session);
|
||||
}
|
||||
ChatControllerPool.remove(session.id, botMessage.id);
|
||||
},
|
||||
onBeforeTool(tool: ChatMessageTool) {
|
||||
(botMessage.tools = botMessage?.tools || []).push(tool);
|
||||
get().updateTargetSession(session, (session) => {
|
||||
session.messages = session.messages.concat();
|
||||
});
|
||||
},
|
||||
onAfterTool(tool: ChatMessageTool) {
|
||||
botMessage?.tools?.forEach((t, i, tools) => {
|
||||
if (tool.id == t.id) {
|
||||
tools[i] = { ...tool };
|
||||
}
|
||||
});
|
||||
get().updateTargetSession(session, (session) => {
|
||||
session.messages = session.messages.concat();
|
||||
});
|
||||
},
|
||||
onError(error) {
|
||||
const isAborted = error.message.includes("aborted");
|
||||
const isAborted = error.message?.includes?.("aborted");
|
||||
botMessage.content +=
|
||||
"\n\n" +
|
||||
prettyObject({
|
||||
@@ -354,7 +506,7 @@ export const useChatStore = createPersistStore(
|
||||
botMessage.streaming = false;
|
||||
userMessage.isError = !isAborted;
|
||||
botMessage.isError = !isAborted;
|
||||
get().updateCurrentSession((session) => {
|
||||
get().updateTargetSession(session, (session) => {
|
||||
session.messages = session.messages.concat();
|
||||
});
|
||||
ChatControllerPool.remove(
|
||||
@@ -378,17 +530,16 @@ export const useChatStore = createPersistStore(
|
||||
getMemoryPrompt() {
|
||||
const session = get().currentSession();
|
||||
|
||||
return {
|
||||
role: "system",
|
||||
content:
|
||||
session.memoryPrompt.length > 0
|
||||
? Locale.Store.Prompt.History(session.memoryPrompt)
|
||||
: "",
|
||||
date: "",
|
||||
} as ChatMessage;
|
||||
if (session.memoryPrompt.length) {
|
||||
return {
|
||||
role: "system",
|
||||
content: Locale.Store.Prompt.History(session.memoryPrompt),
|
||||
date: "",
|
||||
} as ChatMessage;
|
||||
}
|
||||
},
|
||||
|
||||
getMessagesWithMemory() {
|
||||
async getMessagesWithMemory() {
|
||||
const session = get().currentSession();
|
||||
const modelConfig = session.mask.modelConfig;
|
||||
const clearContextIndex = session.clearContextIndex ?? 0;
|
||||
@@ -401,36 +552,49 @@ export const useChatStore = createPersistStore(
|
||||
// system prompts, to get close to OpenAI Web ChatGPT
|
||||
const shouldInjectSystemPrompts =
|
||||
modelConfig.enableInjectSystemPrompts &&
|
||||
session.mask.modelConfig.model.startsWith("gpt-");
|
||||
(session.mask.modelConfig.model.startsWith("gpt-") ||
|
||||
session.mask.modelConfig.model.startsWith("chatgpt-"));
|
||||
|
||||
const mcpEnabled = await isMcpEnabled();
|
||||
const mcpSystemPrompt = mcpEnabled ? await getMcpSystemPrompt() : "";
|
||||
|
||||
var systemPrompts: ChatMessage[] = [];
|
||||
systemPrompts = shouldInjectSystemPrompts
|
||||
? [
|
||||
createMessage({
|
||||
role: "system",
|
||||
content: fillTemplateWith("", {
|
||||
|
||||
if (shouldInjectSystemPrompts) {
|
||||
systemPrompts = [
|
||||
createMessage({
|
||||
role: "system",
|
||||
content:
|
||||
fillTemplateWith("", {
|
||||
...modelConfig,
|
||||
template: DEFAULT_SYSTEM_TEMPLATE,
|
||||
}),
|
||||
}),
|
||||
]
|
||||
: [];
|
||||
if (shouldInjectSystemPrompts) {
|
||||
}) + mcpSystemPrompt,
|
||||
}),
|
||||
];
|
||||
} else if (mcpEnabled) {
|
||||
systemPrompts = [
|
||||
createMessage({
|
||||
role: "system",
|
||||
content: mcpSystemPrompt,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
if (shouldInjectSystemPrompts || mcpEnabled) {
|
||||
console.log(
|
||||
"[Global System Prompt] ",
|
||||
systemPrompts.at(0)?.content ?? "empty",
|
||||
);
|
||||
}
|
||||
|
||||
const memoryPrompt = get().getMemoryPrompt();
|
||||
// long term memory
|
||||
const shouldSendLongTermMemory =
|
||||
modelConfig.sendMemory &&
|
||||
session.memoryPrompt &&
|
||||
session.memoryPrompt.length > 0 &&
|
||||
session.lastSummarizeIndex > clearContextIndex;
|
||||
const longTermMemoryPrompts = shouldSendLongTermMemory
|
||||
? [get().getMemoryPrompt()]
|
||||
: [];
|
||||
const longTermMemoryPrompts =
|
||||
shouldSendLongTermMemory && memoryPrompt ? [memoryPrompt] : [];
|
||||
const longTermMemoryStartIndex = session.lastSummarizeIndex;
|
||||
|
||||
// short term memory
|
||||
@@ -461,10 +625,9 @@ export const useChatStore = createPersistStore(
|
||||
) {
|
||||
const msg = messages[i];
|
||||
if (!msg || msg.isError) continue;
|
||||
tokenCount += estimateTokenLength(msg.content);
|
||||
tokenCount += estimateTokenLength(getMessageTextContent(msg));
|
||||
reversedRecentMessages.push(msg);
|
||||
}
|
||||
|
||||
// concat all messages
|
||||
const recentMessages = [
|
||||
...systemPrompts,
|
||||
@@ -488,52 +651,76 @@ export const useChatStore = createPersistStore(
|
||||
set(() => ({ sessions }));
|
||||
},
|
||||
|
||||
resetSession() {
|
||||
get().updateCurrentSession((session) => {
|
||||
resetSession(session: ChatSession) {
|
||||
get().updateTargetSession(session, (session) => {
|
||||
session.messages = [];
|
||||
session.memoryPrompt = "";
|
||||
});
|
||||
},
|
||||
|
||||
summarizeSession() {
|
||||
summarizeSession(
|
||||
refreshTitle: boolean = false,
|
||||
targetSession: ChatSession,
|
||||
) {
|
||||
const config = useAppConfig.getState();
|
||||
const session = get().currentSession();
|
||||
const session = targetSession;
|
||||
const modelConfig = session.mask.modelConfig;
|
||||
|
||||
var api: ClientApi;
|
||||
if (modelConfig.model.startsWith("gemini")) {
|
||||
api = new ClientApi(ModelProvider.GeminiPro);
|
||||
} else {
|
||||
api = new ClientApi(ModelProvider.GPT);
|
||||
// skip summarize when using dalle3?
|
||||
if (isDalle3(modelConfig.model)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// if not config compressModel, then using getSummarizeModel
|
||||
const [model, providerName] = modelConfig.compressModel
|
||||
? [modelConfig.compressModel, modelConfig.compressProviderName]
|
||||
: getSummarizeModel(
|
||||
session.mask.modelConfig.model,
|
||||
session.mask.modelConfig.providerName,
|
||||
);
|
||||
const api: ClientApi = getClientApi(providerName as ServiceProvider);
|
||||
|
||||
// remove error messages if any
|
||||
const messages = session.messages;
|
||||
|
||||
// should summarize topic after chating more than 50 words
|
||||
const SUMMARIZE_MIN_LEN = 50;
|
||||
if (
|
||||
config.enableAutoGenerateTitle &&
|
||||
session.topic === DEFAULT_TOPIC &&
|
||||
countMessages(messages) >= SUMMARIZE_MIN_LEN
|
||||
(config.enableAutoGenerateTitle &&
|
||||
session.topic === DEFAULT_TOPIC &&
|
||||
countMessages(messages) >= SUMMARIZE_MIN_LEN) ||
|
||||
refreshTitle
|
||||
) {
|
||||
const topicMessages = messages.concat(
|
||||
createMessage({
|
||||
role: "user",
|
||||
content: Locale.Store.Prompt.Topic,
|
||||
}),
|
||||
const startIndex = Math.max(
|
||||
0,
|
||||
messages.length - modelConfig.historyMessageCount,
|
||||
);
|
||||
const topicMessages = messages
|
||||
.slice(
|
||||
startIndex < messages.length ? startIndex : messages.length - 1,
|
||||
messages.length,
|
||||
)
|
||||
.concat(
|
||||
createMessage({
|
||||
role: "user",
|
||||
content: Locale.Store.Prompt.Topic,
|
||||
}),
|
||||
);
|
||||
api.llm.chat({
|
||||
messages: topicMessages,
|
||||
config: {
|
||||
model: getSummarizeModel(session.mask.modelConfig.model),
|
||||
model,
|
||||
stream: false,
|
||||
providerName,
|
||||
},
|
||||
onFinish(message) {
|
||||
get().updateCurrentSession(
|
||||
(session) =>
|
||||
(session.topic =
|
||||
message.length > 0 ? trimTopic(message) : DEFAULT_TOPIC),
|
||||
);
|
||||
onFinish(message, responseRes) {
|
||||
if (responseRes?.status === 200) {
|
||||
get().updateTargetSession(
|
||||
session,
|
||||
(session) =>
|
||||
(session.topic =
|
||||
message.length > 0 ? trimTopic(message) : DEFAULT_TOPIC),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -547,15 +734,17 @@ export const useChatStore = createPersistStore(
|
||||
|
||||
const historyMsgLength = countMessages(toBeSummarizedMsgs);
|
||||
|
||||
if (historyMsgLength > modelConfig?.max_tokens ?? 4000) {
|
||||
if (historyMsgLength > (modelConfig?.max_tokens || 4000)) {
|
||||
const n = toBeSummarizedMsgs.length;
|
||||
toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
|
||||
Math.max(0, n - modelConfig.historyMessageCount),
|
||||
);
|
||||
}
|
||||
|
||||
// add memory prompt
|
||||
toBeSummarizedMsgs.unshift(get().getMemoryPrompt());
|
||||
const memoryPrompt = get().getMemoryPrompt();
|
||||
if (memoryPrompt) {
|
||||
// add memory prompt
|
||||
toBeSummarizedMsgs.unshift(memoryPrompt);
|
||||
}
|
||||
|
||||
const lastSummarizeIndex = session.messages.length;
|
||||
|
||||
@@ -570,6 +759,10 @@ export const useChatStore = createPersistStore(
|
||||
historyMsgLength > modelConfig.compressMessageLengthThreshold &&
|
||||
modelConfig.sendMemory
|
||||
) {
|
||||
/** Destruct max_tokens while summarizing
|
||||
* this param is just shit
|
||||
**/
|
||||
const { max_tokens, ...modelcfg } = modelConfig;
|
||||
api.llm.chat({
|
||||
messages: toBeSummarizedMsgs.concat(
|
||||
createMessage({
|
||||
@@ -579,19 +772,22 @@ export const useChatStore = createPersistStore(
|
||||
}),
|
||||
),
|
||||
config: {
|
||||
...modelConfig,
|
||||
...modelcfg,
|
||||
stream: true,
|
||||
model: getSummarizeModel(session.mask.modelConfig.model),
|
||||
model,
|
||||
providerName,
|
||||
},
|
||||
onUpdate(message) {
|
||||
session.memoryPrompt = message;
|
||||
},
|
||||
onFinish(message) {
|
||||
console.log("[Memory] ", message);
|
||||
get().updateCurrentSession((session) => {
|
||||
session.lastSummarizeIndex = lastSummarizeIndex;
|
||||
session.memoryPrompt = message; // Update the memory prompt for stored it in local storage
|
||||
});
|
||||
onFinish(message, responseRes) {
|
||||
if (responseRes?.status === 200) {
|
||||
console.log("[Memory] ", message);
|
||||
get().updateTargetSession(session, (session) => {
|
||||
session.lastSummarizeIndex = lastSummarizeIndex;
|
||||
session.memoryPrompt = message; // Update the memory prompt for stored it in local storage
|
||||
});
|
||||
}
|
||||
},
|
||||
onError(err) {
|
||||
console.error("[Summarize] ", err);
|
||||
@@ -600,31 +796,71 @@ export const useChatStore = createPersistStore(
|
||||
}
|
||||
},
|
||||
|
||||
updateStat(message: ChatMessage) {
|
||||
get().updateCurrentSession((session) => {
|
||||
updateStat(message: ChatMessage, session: ChatSession) {
|
||||
get().updateTargetSession(session, (session) => {
|
||||
session.stat.charCount += message.content.length;
|
||||
// TODO: should update chat count and word count
|
||||
});
|
||||
},
|
||||
|
||||
updateCurrentSession(updater: (session: ChatSession) => void) {
|
||||
updateTargetSession(
|
||||
targetSession: ChatSession,
|
||||
updater: (session: ChatSession) => void,
|
||||
) {
|
||||
const sessions = get().sessions;
|
||||
const index = get().currentSessionIndex;
|
||||
const index = sessions.findIndex((s) => s.id === targetSession.id);
|
||||
if (index < 0) return;
|
||||
updater(sessions[index]);
|
||||
set(() => ({ sessions }));
|
||||
},
|
||||
|
||||
clearAllData() {
|
||||
async clearAllData() {
|
||||
await indexedDBStorage.clear();
|
||||
localStorage.clear();
|
||||
location.reload();
|
||||
},
|
||||
setLastInput(lastInput: string) {
|
||||
set({
|
||||
lastInput,
|
||||
});
|
||||
},
|
||||
|
||||
/** check if the message contains MCP JSON and execute the MCP action */
|
||||
checkMcpJson(message: ChatMessage) {
|
||||
const mcpEnabled = isMcpEnabled();
|
||||
if (!mcpEnabled) return;
|
||||
const content = getMessageTextContent(message);
|
||||
if (isMcpJson(content)) {
|
||||
try {
|
||||
const mcpRequest = extractMcpJson(content);
|
||||
if (mcpRequest) {
|
||||
console.debug("[MCP Request]", mcpRequest);
|
||||
|
||||
executeMcpAction(mcpRequest.clientId, mcpRequest.mcp)
|
||||
.then((result) => {
|
||||
console.log("[MCP Response]", result);
|
||||
const mcpResponse =
|
||||
typeof result === "object"
|
||||
? JSON.stringify(result)
|
||||
: String(result);
|
||||
get().onUserInput(
|
||||
`\`\`\`json:mcp-response:${mcpRequest.clientId}\n${mcpResponse}\n\`\`\``,
|
||||
[],
|
||||
true,
|
||||
);
|
||||
})
|
||||
.catch((error) => showToast("MCP execution failed", error));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[Check MCP JSON]", error);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return methods;
|
||||
},
|
||||
{
|
||||
name: StoreKey.Chat,
|
||||
version: 3.1,
|
||||
version: 3.3,
|
||||
migrate(persistedState, version) {
|
||||
const state = persistedState as any;
|
||||
const newState = JSON.parse(
|
||||
@@ -671,6 +907,24 @@ export const useChatStore = createPersistStore(
|
||||
});
|
||||
}
|
||||
|
||||
// add default summarize model for every session
|
||||
if (version < 3.2) {
|
||||
newState.sessions.forEach((s) => {
|
||||
const config = useAppConfig.getState();
|
||||
s.mask.modelConfig.compressModel = config.modelConfig.compressModel;
|
||||
s.mask.modelConfig.compressProviderName =
|
||||
config.modelConfig.compressProviderName;
|
||||
});
|
||||
}
|
||||
// revert default summarize model for every session
|
||||
if (version < 3.3) {
|
||||
newState.sessions.forEach((s) => {
|
||||
const config = useAppConfig.getState();
|
||||
s.mask.modelConfig.compressModel = "";
|
||||
s.mask.modelConfig.compressProviderName = "";
|
||||
});
|
||||
}
|
||||
|
||||
return newState as any;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
import { LLMModel } from "../client/api";
|
||||
import { isMacOS } from "../utils";
|
||||
import { DalleQuality, DalleStyle, ModelSize } from "../typing";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import {
|
||||
DEFAULT_INPUT_TEMPLATE,
|
||||
DEFAULT_MODELS,
|
||||
DEFAULT_SIDEBAR_WIDTH,
|
||||
DEFAULT_TTS_ENGINE,
|
||||
DEFAULT_TTS_ENGINES,
|
||||
DEFAULT_TTS_MODEL,
|
||||
DEFAULT_TTS_MODELS,
|
||||
DEFAULT_TTS_VOICE,
|
||||
DEFAULT_TTS_VOICES,
|
||||
StoreKey,
|
||||
ServiceProvider,
|
||||
} from "../constant";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import type { Voice } from "rt-client";
|
||||
|
||||
export type ModelType = (typeof DEFAULT_MODELS)[number]["name"];
|
||||
export type TTSModelType = (typeof DEFAULT_TTS_MODELS)[number];
|
||||
export type TTSVoiceType = (typeof DEFAULT_TTS_VOICES)[number];
|
||||
export type TTSEngineType = (typeof DEFAULT_TTS_ENGINES)[number];
|
||||
|
||||
export enum SubmitKey {
|
||||
Enter = "Enter",
|
||||
@@ -25,19 +36,26 @@ export enum Theme {
|
||||
Light = "light",
|
||||
}
|
||||
|
||||
const config = getClientConfig();
|
||||
|
||||
export const DEFAULT_CONFIG = {
|
||||
lastUpdate: Date.now(), // timestamp, to merge state
|
||||
|
||||
submitKey: SubmitKey.Enter,
|
||||
avatar: "1f603",
|
||||
fontSize: 14,
|
||||
fontFamily: "",
|
||||
theme: Theme.Auto as Theme,
|
||||
tightBorder: !!getClientConfig()?.isApp,
|
||||
tightBorder: !!config?.isApp,
|
||||
sendPreviewBubble: true,
|
||||
autoScrollMessage: true,
|
||||
enableAutoGenerateTitle: true,
|
||||
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
|
||||
|
||||
enableArtifacts: true, // show artifacts config
|
||||
|
||||
enableCodeFold: true, // code fold config
|
||||
|
||||
disablePromptHint: false,
|
||||
|
||||
dontShowMaskSplashScreen: false, // dont show splash screen when create chat
|
||||
@@ -47,7 +65,8 @@ export const DEFAULT_CONFIG = {
|
||||
models: DEFAULT_MODELS as any as LLMModel[],
|
||||
|
||||
modelConfig: {
|
||||
model: "gpt-3.5-turbo" as ModelType,
|
||||
model: "gpt-4o-mini" as ModelType,
|
||||
providerName: "OpenAI" as ServiceProvider,
|
||||
temperature: 0.5,
|
||||
top_p: 1,
|
||||
max_tokens: 4000,
|
||||
@@ -56,14 +75,43 @@ export const DEFAULT_CONFIG = {
|
||||
sendMemory: true,
|
||||
historyMessageCount: 4,
|
||||
compressMessageLengthThreshold: 1000,
|
||||
compressModel: "",
|
||||
compressProviderName: "",
|
||||
enableInjectSystemPrompts: true,
|
||||
template: DEFAULT_INPUT_TEMPLATE,
|
||||
template: config?.template ?? DEFAULT_INPUT_TEMPLATE,
|
||||
size: "1024x1024" as ModelSize,
|
||||
quality: "standard" as DalleQuality,
|
||||
style: "vivid" as DalleStyle,
|
||||
},
|
||||
|
||||
ttsConfig: {
|
||||
enable: false,
|
||||
autoplay: false,
|
||||
engine: DEFAULT_TTS_ENGINE,
|
||||
model: DEFAULT_TTS_MODEL,
|
||||
voice: DEFAULT_TTS_VOICE,
|
||||
speed: 1.0,
|
||||
},
|
||||
|
||||
realtimeConfig: {
|
||||
enable: false,
|
||||
provider: "OpenAI" as ServiceProvider,
|
||||
model: "gpt-4o-realtime-preview-2024-10-01",
|
||||
apiKey: "",
|
||||
azure: {
|
||||
endpoint: "",
|
||||
deployment: "",
|
||||
},
|
||||
temperature: 0.9,
|
||||
voice: "alloy" as Voice,
|
||||
},
|
||||
};
|
||||
|
||||
export type ChatConfig = typeof DEFAULT_CONFIG;
|
||||
|
||||
export type ModelConfig = ChatConfig["modelConfig"];
|
||||
export type TTSConfig = ChatConfig["ttsConfig"];
|
||||
export type RealtimeConfig = ChatConfig["realtimeConfig"];
|
||||
|
||||
export function limitNumber(
|
||||
x: number,
|
||||
@@ -78,6 +126,21 @@ export function limitNumber(
|
||||
return Math.min(max, Math.max(min, x));
|
||||
}
|
||||
|
||||
export const TTSConfigValidator = {
|
||||
engine(x: string) {
|
||||
return x as TTSEngineType;
|
||||
},
|
||||
model(x: string) {
|
||||
return x as TTSModelType;
|
||||
},
|
||||
voice(x: string) {
|
||||
return x as TTSVoiceType;
|
||||
},
|
||||
speed(x: number) {
|
||||
return limitNumber(x, 0.25, 4.0, 1.0);
|
||||
},
|
||||
};
|
||||
|
||||
export const ModalConfigValidator = {
|
||||
model(x: string) {
|
||||
return x as ModelType;
|
||||
@@ -92,7 +155,7 @@ export const ModalConfigValidator = {
|
||||
return limitNumber(x, -2, 2, 0);
|
||||
},
|
||||
temperature(x: number) {
|
||||
return limitNumber(x, 0, 1, 1);
|
||||
return limitNumber(x, 0, 2, 1);
|
||||
},
|
||||
top_p(x: number) {
|
||||
return limitNumber(x, 0, 1, 1);
|
||||
@@ -116,12 +179,12 @@ export const useAppConfig = createPersistStore(
|
||||
|
||||
for (const model of oldModels) {
|
||||
model.available = false;
|
||||
modelMap[model.name] = model;
|
||||
modelMap[`${model.name}@${model?.provider?.id}`] = model;
|
||||
}
|
||||
|
||||
for (const model of newModels) {
|
||||
model.available = true;
|
||||
modelMap[model.name] = model;
|
||||
modelMap[`${model.name}@${model?.provider?.id}`] = model;
|
||||
}
|
||||
|
||||
set(() => ({
|
||||
@@ -133,7 +196,22 @@ export const useAppConfig = createPersistStore(
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Config,
|
||||
version: 3.8,
|
||||
version: 4.1,
|
||||
|
||||
merge(persistedState, currentState) {
|
||||
const state = persistedState as ChatConfig | undefined;
|
||||
if (!state) return { ...currentState };
|
||||
const models = currentState.models.slice();
|
||||
state.models.forEach((pModel) => {
|
||||
const idx = models.findIndex(
|
||||
(v) => v.name === pModel.name && v.provider === pModel.provider,
|
||||
);
|
||||
if (idx !== -1) models[idx] = pModel;
|
||||
else models.push(pModel);
|
||||
});
|
||||
return { ...currentState, ...state, models: models };
|
||||
},
|
||||
|
||||
migrate(persistedState, version) {
|
||||
const state = persistedState as ChatConfig;
|
||||
|
||||
@@ -164,6 +242,20 @@ export const useAppConfig = createPersistStore(
|
||||
state.lastUpdate = Date.now();
|
||||
}
|
||||
|
||||
if (version < 3.9) {
|
||||
state.modelConfig.template =
|
||||
state.modelConfig.template !== DEFAULT_INPUT_TEMPLATE
|
||||
? state.modelConfig.template
|
||||
: config?.template ?? DEFAULT_INPUT_TEMPLATE;
|
||||
}
|
||||
|
||||
if (version < 4.1) {
|
||||
state.modelConfig.compressModel =
|
||||
DEFAULT_CONFIG.modelConfig.compressModel;
|
||||
state.modelConfig.compressProviderName =
|
||||
DEFAULT_CONFIG.modelConfig.compressProviderName;
|
||||
}
|
||||
|
||||
return state as any;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -2,3 +2,4 @@ export * from "./chat";
|
||||
export * from "./update";
|
||||
export * from "./access";
|
||||
export * from "./config";
|
||||
export * from "./plugin";
|
||||
|
||||
@@ -17,13 +17,19 @@ export type Mask = {
|
||||
modelConfig: ModelConfig;
|
||||
lang: Lang;
|
||||
builtin: boolean;
|
||||
plugin?: string[];
|
||||
enableArtifacts?: boolean;
|
||||
enableCodeFold?: boolean;
|
||||
};
|
||||
|
||||
export const DEFAULT_MASK_STATE = {
|
||||
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 createEmptyMask = () =>
|
||||
@@ -37,6 +43,7 @@ export const createEmptyMask = () =>
|
||||
lang: getLang(),
|
||||
builtin: false,
|
||||
createdAt: Date.now(),
|
||||
plugin: [],
|
||||
}) as Mask;
|
||||
|
||||
export const useMaskStore = createPersistStore(
|
||||
@@ -99,6 +106,11 @@ export const useMaskStore = createPersistStore(
|
||||
search(text: string) {
|
||||
return Object.values(get().masks);
|
||||
},
|
||||
setLanguage(language: Lang | undefined) {
|
||||
set({
|
||||
language,
|
||||
});
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Mask,
|
||||
|
||||
271
app/store/plugin.ts
Normal file
271
app/store/plugin.ts
Normal file
@@ -0,0 +1,271 @@
|
||||
import OpenAPIClientAxios from "openapi-client-axios";
|
||||
import { StoreKey } from "../constant";
|
||||
import { nanoid } from "nanoid";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import yaml from "js-yaml";
|
||||
import { adapter, getOperationId } from "../utils";
|
||||
import { useAccessStore } from "./access";
|
||||
|
||||
const isApp = getClientConfig()?.isApp !== false;
|
||||
|
||||
export type Plugin = {
|
||||
id: string;
|
||||
createdAt: number;
|
||||
title: string;
|
||||
version: string;
|
||||
content: string;
|
||||
builtin: boolean;
|
||||
authType?: string;
|
||||
authLocation?: string;
|
||||
authHeader?: string;
|
||||
authToken?: string;
|
||||
};
|
||||
|
||||
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 = !isApp ? "/api/proxy" : serverURL;
|
||||
const headers: Record<string, string | undefined> = {
|
||||
"X-Base-URL": !isApp ? serverURL : undefined,
|
||||
};
|
||||
if (authLocation == "header") {
|
||||
headers[headerName] = tokenValue;
|
||||
}
|
||||
// try using openaiApiKey for Dalle3 Plugin.
|
||||
if (!tokenValue && plugin.id === "dalle3") {
|
||||
const openaiApiKey = useAccessStore.getState().openaiApiKey;
|
||||
if (openaiApiKey) {
|
||||
headers[headerName] = `Bearer ${openaiApiKey}`;
|
||||
}
|
||||
}
|
||||
const api = new OpenAPIClientAxios({
|
||||
definition: yaml.load(plugin.content) as any,
|
||||
axiosConfigDefaults: {
|
||||
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: getOperationId(o),
|
||||
description: o.description || o.summary,
|
||||
parameters: parameters,
|
||||
},
|
||||
} as FunctionToolItem;
|
||||
}),
|
||||
funcs: operations.reduce((s, o) => {
|
||||
// @ts-ignore
|
||||
s[getOperationId(o)] = 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 if o.operationId is null, then using o.path and o.method
|
||||
return api.client.paths[o.path][o.method](
|
||||
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 = plugin?.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,
|
||||
onRehydrateStorage(state) {
|
||||
// Skip store rehydration on server side
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("./plugins.json")
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
Promise.all(
|
||||
res.map((item: any) =>
|
||||
// skip get schema
|
||||
state.get(item.id)
|
||||
? item
|
||||
: fetch(item.schema)
|
||||
.then((res) => res.text())
|
||||
.then((content) => ({
|
||||
...item,
|
||||
content,
|
||||
}))
|
||||
.catch((e) => item),
|
||||
),
|
||||
).then((builtinPlugins: any) => {
|
||||
builtinPlugins
|
||||
.filter((item: any) => item?.content)
|
||||
.forEach((item: any) => {
|
||||
const plugin = state.create(item);
|
||||
state.updatePlugin(plugin.id, (plugin) => {
|
||||
const tool = FunctionToolService.add(plugin, true);
|
||||
plugin.title = tool.api.definition.info.title;
|
||||
plugin.version = tool.api.definition.info.version;
|
||||
plugin.builtin = true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -1,7 +1,7 @@
|
||||
import Fuse from "fuse.js";
|
||||
import { getLang } from "../locales";
|
||||
import { StoreKey } from "../constant";
|
||||
import { nanoid } from "nanoid";
|
||||
import { StoreKey } from "../constant";
|
||||
import { getLang } from "../locales";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
|
||||
export interface Prompt {
|
||||
@@ -147,6 +147,11 @@ export const usePromptStore = createPersistStore(
|
||||
},
|
||||
|
||||
onRehydrateStorage(state) {
|
||||
// Skip store rehydration on server side
|
||||
if (typeof window === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
const PROMPT_URL = "./prompts.json";
|
||||
|
||||
type PromptList = Array<[string, string]>;
|
||||
@@ -154,7 +159,7 @@ export const usePromptStore = createPersistStore(
|
||||
fetch(PROMPT_URL)
|
||||
.then((res) => res.json())
|
||||
.then((res) => {
|
||||
let fetchPrompts = [res.en, res.cn];
|
||||
let fetchPrompts = [res.en, res.tw, res.cn];
|
||||
if (getLang() === "cn") {
|
||||
fetchPrompts = fetchPrompts.reverse();
|
||||
}
|
||||
@@ -175,7 +180,8 @@ export const usePromptStore = createPersistStore(
|
||||
const allPromptsForSearch = builtinPrompts
|
||||
.reduce((pre, cur) => pre.concat(cur), [])
|
||||
.filter((v) => !!v.title && !!v.content);
|
||||
SearchService.count.builtin = res.en.length + res.cn.length;
|
||||
SearchService.count.builtin =
|
||||
res.en.length + res.cn.length + res.tw.length;
|
||||
SearchService.init(allPromptsForSearch, userPrompts);
|
||||
});
|
||||
},
|
||||
|
||||
163
app/store/sd.ts
Normal file
163
app/store/sd.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import {
|
||||
Stability,
|
||||
StoreKey,
|
||||
ACCESS_CODE_PREFIX,
|
||||
ApiPath,
|
||||
} from "@/app/constant";
|
||||
import { getBearerToken } from "@/app/client/api";
|
||||
import { createPersistStore } from "@/app/utils/store";
|
||||
import { nanoid } from "nanoid";
|
||||
import { uploadImage, base64Image2Blob } from "@/app/utils/chat";
|
||||
import { models, getModelParamBasicData } from "@/app/components/sd/sd-panel";
|
||||
import { useAccessStore } from "./access";
|
||||
|
||||
const defaultModel = {
|
||||
name: models[0].name,
|
||||
value: models[0].value,
|
||||
};
|
||||
|
||||
const defaultParams = getModelParamBasicData(models[0].params({}), {});
|
||||
|
||||
const DEFAULT_SD_STATE = {
|
||||
currentId: 0,
|
||||
draw: [],
|
||||
currentModel: defaultModel,
|
||||
currentParams: defaultParams,
|
||||
};
|
||||
|
||||
export const useSdStore = createPersistStore<
|
||||
{
|
||||
currentId: number;
|
||||
draw: any[];
|
||||
currentModel: typeof defaultModel;
|
||||
currentParams: any;
|
||||
},
|
||||
{
|
||||
getNextId: () => number;
|
||||
sendTask: (data: any, okCall?: Function) => void;
|
||||
updateDraw: (draw: any) => void;
|
||||
setCurrentModel: (model: any) => void;
|
||||
setCurrentParams: (data: any) => void;
|
||||
}
|
||||
>(
|
||||
DEFAULT_SD_STATE,
|
||||
(set, _get) => {
|
||||
function get() {
|
||||
return {
|
||||
..._get(),
|
||||
...methods,
|
||||
};
|
||||
}
|
||||
|
||||
const methods = {
|
||||
getNextId() {
|
||||
const id = ++_get().currentId;
|
||||
set({ currentId: id });
|
||||
return id;
|
||||
},
|
||||
sendTask(data: any, okCall?: Function) {
|
||||
data = { ...data, id: nanoid(), status: "running" };
|
||||
set({ draw: [data, ..._get().draw] });
|
||||
this.getNextId();
|
||||
this.stabilityRequestCall(data);
|
||||
okCall?.();
|
||||
},
|
||||
stabilityRequestCall(data: any) {
|
||||
const accessStore = useAccessStore.getState();
|
||||
let prefix: string = ApiPath.Stability as string;
|
||||
let bearerToken = "";
|
||||
if (accessStore.useCustomConfig) {
|
||||
prefix = accessStore.stabilityUrl || (ApiPath.Stability as string);
|
||||
bearerToken = getBearerToken(accessStore.stabilityApiKey);
|
||||
}
|
||||
if (!bearerToken && accessStore.enabledAccessControl()) {
|
||||
bearerToken = getBearerToken(
|
||||
ACCESS_CODE_PREFIX + accessStore.accessCode,
|
||||
);
|
||||
}
|
||||
const headers = {
|
||||
Accept: "application/json",
|
||||
Authorization: bearerToken,
|
||||
};
|
||||
const path = `${prefix}/${Stability.GeneratePath}/${data.model}`;
|
||||
const formData = new FormData();
|
||||
for (let paramsKey in data.params) {
|
||||
formData.append(paramsKey, data.params[paramsKey]);
|
||||
}
|
||||
fetch(path, {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: formData,
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((resData) => {
|
||||
if (resData.errors && resData.errors.length > 0) {
|
||||
this.updateDraw({
|
||||
...data,
|
||||
status: "error",
|
||||
error: resData.errors[0],
|
||||
});
|
||||
this.getNextId();
|
||||
return;
|
||||
}
|
||||
const self = this;
|
||||
if (resData.finish_reason === "SUCCESS") {
|
||||
uploadImage(base64Image2Blob(resData.image, "image/png"))
|
||||
.then((img_data) => {
|
||||
console.debug("uploadImage success", img_data, self);
|
||||
self.updateDraw({
|
||||
...data,
|
||||
status: "success",
|
||||
img_data,
|
||||
});
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("uploadImage error", e);
|
||||
self.updateDraw({
|
||||
...data,
|
||||
status: "error",
|
||||
error: JSON.stringify(e),
|
||||
});
|
||||
});
|
||||
} else {
|
||||
self.updateDraw({
|
||||
...data,
|
||||
status: "error",
|
||||
error: JSON.stringify(resData),
|
||||
});
|
||||
}
|
||||
this.getNextId();
|
||||
})
|
||||
.catch((error) => {
|
||||
this.updateDraw({ ...data, status: "error", error: error.message });
|
||||
console.error("Error:", error);
|
||||
this.getNextId();
|
||||
});
|
||||
},
|
||||
updateDraw(_draw: any) {
|
||||
const draw = _get().draw || [];
|
||||
draw.some((item, index) => {
|
||||
if (item.id === _draw.id) {
|
||||
draw[index] = _draw;
|
||||
set(() => ({ draw }));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
},
|
||||
setCurrentModel(model: any) {
|
||||
set({ currentModel: model });
|
||||
},
|
||||
setCurrentParams(data: any) {
|
||||
set({
|
||||
currentParams: data,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
return methods;
|
||||
},
|
||||
{
|
||||
name: StoreKey.SdList,
|
||||
version: 1.0,
|
||||
},
|
||||
);
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { Updater } from "../typing";
|
||||
import { ApiPath, STORAGE_KEY, StoreKey } from "../constant";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import {
|
||||
@@ -13,7 +12,6 @@ import { downloadAs, readFromFile } from "../utils";
|
||||
import { showToast } from "../components/ui-lib";
|
||||
import Locale from "../locales";
|
||||
import { createSyncClient, ProviderType } from "../utils/cloud";
|
||||
import { corsPath } from "../utils/cors";
|
||||
|
||||
export interface WebDavConfig {
|
||||
server: string;
|
||||
@@ -27,7 +25,7 @@ export type SyncStore = GetStoreState<typeof useSyncStore>;
|
||||
const DEFAULT_SYNC_STATE = {
|
||||
provider: ProviderType.WebDAV,
|
||||
useProxy: true,
|
||||
proxyUrl: corsPath(ApiPath.Cors),
|
||||
proxyUrl: ApiPath.Cors as string,
|
||||
|
||||
webdav: {
|
||||
endpoint: "",
|
||||
@@ -97,13 +95,23 @@ export const useSyncStore = createPersistStore(
|
||||
const client = this.getClient();
|
||||
|
||||
try {
|
||||
const remoteState = JSON.parse(
|
||||
await client.get(config.username),
|
||||
) as AppState;
|
||||
mergeAppState(localState, remoteState);
|
||||
setLocalAppState(localState);
|
||||
const remoteState = await client.get(config.username);
|
||||
if (!remoteState || remoteState === "") {
|
||||
await client.set(config.username, JSON.stringify(localState));
|
||||
console.log(
|
||||
"[Sync] Remote state is empty, using local state instead.",
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
const parsedRemoteState = JSON.parse(
|
||||
await client.get(config.username),
|
||||
) as AppState;
|
||||
mergeAppState(localState, parsedRemoteState);
|
||||
setLocalAppState(localState);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Sync] failed to get remote state", e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
await client.set(config.username, JSON.stringify(localState));
|
||||
@@ -118,7 +126,7 @@ export const useSyncStore = createPersistStore(
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Sync,
|
||||
version: 1.1,
|
||||
version: 1.2,
|
||||
|
||||
migrate(persistedState, version) {
|
||||
const newState = persistedState as typeof DEFAULT_SYNC_STATE;
|
||||
@@ -127,6 +135,15 @@ export const useSyncStore = createPersistStore(
|
||||
newState.upstash.username = STORAGE_KEY;
|
||||
}
|
||||
|
||||
if (version < 1.2) {
|
||||
if (
|
||||
(persistedState as typeof DEFAULT_SYNC_STATE).proxyUrl ===
|
||||
"/api/cors/"
|
||||
) {
|
||||
newState.proxyUrl = "";
|
||||
}
|
||||
}
|
||||
|
||||
return newState as any;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -6,10 +6,9 @@ import {
|
||||
} from "../constant";
|
||||
import { getClientConfig } from "../config/client";
|
||||
import { createPersistStore } from "../utils/store";
|
||||
import { clientUpdate } from "../utils";
|
||||
import ChatGptIcon from "../icons/chatgpt.png";
|
||||
import Locale from "../locales";
|
||||
import { use } from "react";
|
||||
import { useAppConfig } from ".";
|
||||
import { ClientApi } from "../client/api";
|
||||
|
||||
const ONE_MINUTE = 60 * 1000;
|
||||
@@ -121,6 +120,7 @@ export const useUpdateStore = createPersistStore(
|
||||
icon: `${ChatGptIcon.src}`,
|
||||
sound: "Default",
|
||||
});
|
||||
clientUpdate();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user