mirror of
https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web.git
synced 2026-03-03 10:44:26 +08:00
Compare commits
103 Commits
update-max
...
3e02a71e0d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e02a71e0d | ||
|
|
4f49626303 | ||
|
|
45db20c1c3 | ||
|
|
82994843f5 | ||
|
|
1110a087a0 | ||
|
|
f0b3e10a6c | ||
|
|
f89872b833 | ||
|
|
90ced92876 | ||
|
|
2c74559010 | ||
|
|
e3ca7e8b44 | ||
|
|
4745706c42 | ||
|
|
c7c2c0211a | ||
|
|
65bb962fc0 | ||
|
|
e791cd441d | ||
|
|
8455fefc8a | ||
|
|
06f897f32f | ||
|
|
deb1e76c41 | ||
|
|
463fa743e9 | ||
|
|
cda4494cec | ||
|
|
87d85c10c3 | ||
|
|
22f83c9e11 | ||
|
|
7f454cbcec | ||
|
|
426269d795 | ||
|
|
370f143157 | ||
|
|
103106bb93 | ||
|
|
2419083adf | ||
|
|
c25903bfb4 | ||
|
|
e34c266438 | ||
|
|
8c39a687b5 | ||
|
|
592f62005b | ||
|
|
12e7caa209 | ||
|
|
b016771555 | ||
|
|
a84383f919 | ||
|
|
7f68fb1ff2 | ||
|
|
8d2003fe68 | ||
|
|
9961b513cc | ||
|
|
819238acaf | ||
|
|
ad49916b1c | ||
|
|
d18bd8a48a | ||
|
|
4a1319f2c0 | ||
|
|
8fd843d228 | ||
|
|
6792d6e475 | ||
|
|
bd43af3a8d | ||
|
|
be98aa2078 | ||
|
|
a0d4a04192 | ||
|
|
bd9de4dc4d | ||
|
|
2eebfcf6fe | ||
|
|
f80da8a263 | ||
|
|
98ab561607 | ||
|
|
9a025ae196 | ||
|
|
c4ae73d8a1 | ||
|
|
7d55a6d0e4 | ||
|
|
31900cbff3 | ||
|
|
c6657d3d0c | ||
|
|
cf7c6f2b9a | ||
|
|
41242cae9b | ||
|
|
89edebd93c | ||
|
|
60bd3c56c1 | ||
|
|
659a389fd4 | ||
|
|
144fdc9b7c | ||
|
|
35f52886c4 | ||
|
|
9551f5dfc6 | ||
|
|
370ce3eeca | ||
|
|
5ae4921ee0 | ||
|
|
6f3d7530b9 | ||
|
|
6dc868154d | ||
|
|
ccacfec918 | ||
|
|
c204031ea7 | ||
|
|
2bf72d0324 | ||
|
|
e8c7ac0c45 | ||
|
|
0638db146e | ||
|
|
2d68f179d7 | ||
|
|
f1d69cb312 | ||
|
|
31baa10363 | ||
|
|
2fdb35bcc8 | ||
|
|
5c51fd2ed8 | ||
|
|
d0b7ddc1d6 | ||
|
|
0745b6498d | ||
|
|
fc97c4b06f | ||
|
|
fdb89af355 | ||
|
|
e515f0f957 | ||
|
|
31f282970b | ||
|
|
b2336f5ed9 | ||
|
|
0a6ddda992 | ||
|
|
5e1064a5c8 | ||
|
|
2ee2d50ae6 | ||
|
|
eae593d660 | ||
|
|
621b1480c2 | ||
|
|
4b22aaf979 | ||
|
|
93bfb55822 | ||
|
|
648e60028d | ||
|
|
4f876f3e65 | ||
|
|
faac0d9817 | ||
|
|
22c79595fb | ||
|
|
5065091b74 | ||
|
|
22f61295bc | ||
|
|
c440637ad0 | ||
|
|
284d33bcdf | ||
|
|
d9573973ca | ||
|
|
cd354cf045 | ||
|
|
1cce87acaa | ||
|
|
78c4084501 | ||
|
|
1d0a40b9e8 |
4
.github/workflows/deploy_preview.yml
vendored
4
.github/workflows/deploy_preview.yml
vendored
@@ -3,9 +3,7 @@ name: VercelPreviewDeployment
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- review_requested
|
||||
|
||||
env:
|
||||
VERCEL_TEAM: ${{ secrets.VERCEL_TEAM }}
|
||||
|
||||
18
.github/workflows/docker.yml
vendored
18
.github/workflows/docker.yml
vendored
@@ -19,26 +19,26 @@ jobs:
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
-
|
||||
|
||||
-
|
||||
name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: yidadaa/chatgpt-next-web
|
||||
images: ${{ secrets.DOCKER_USERNAME }}/chatgpt-next-web
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=ref,event=tag
|
||||
|
||||
-
|
||||
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
-
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
-
|
||||
|
||||
-
|
||||
name: Build and push Docker image
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
@@ -49,4 +49,4 @@ jobs:
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
|
||||
|
||||
39
.github/workflows/test.yml
vendored
Normal file
39
.github/workflows/test.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
name: Run Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "!*"
|
||||
pull_request:
|
||||
types:
|
||||
- review_requested
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 18
|
||||
cache: "yarn"
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node_modules-
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn install
|
||||
|
||||
- name: Run Jest tests
|
||||
run: yarn test:ci
|
||||
@@ -31,7 +31,7 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
|
||||
[MacOS-image]: https://img.shields.io/badge/-MacOS-black?logo=apple
|
||||
[Linux-image]: https://img.shields.io/badge/-Linux-333?logo=ubuntu
|
||||
|
||||
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
|
||||
[<img src="https://vercel.com/button" alt="Deploy on Zeabur" height="30">](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FChatGPTNextWeb%2FChatGPT-Next-Web&env=OPENAI_API_KEY&env=CODE&project-name=nextchat&repository-name=NextChat) [<img src="https://zeabur.com/button.svg" alt="Deploy on Zeabur" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Open in Gitpod" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) [<img src="https://img.shields.io/badge/BT_Deploy-Install-20a53a" alt="Open in Gitpod" height="30">](https://www.bt.cn/new/download.html)
|
||||
|
||||
[<img src="https://github.com/user-attachments/assets/903482d4-3e87-4134-9af1-f2588fa90659" height="60" width="288" >](https://monica.im/?utm=nxcrp)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { handle as alibabaHandler } from "../../alibaba";
|
||||
import { handle as moonshotHandler } from "../../moonshot";
|
||||
import { handle as stabilityHandler } from "../../stability";
|
||||
import { handle as iflytekHandler } from "../../iflytek";
|
||||
import { handle as xaiHandler } from "../../xai";
|
||||
import { handle as proxyHandler } from "../../proxy";
|
||||
|
||||
async function handle(
|
||||
@@ -38,6 +39,8 @@ async function handle(
|
||||
return stabilityHandler(req, { params });
|
||||
case ApiPath.Iflytek:
|
||||
return iflytekHandler(req, { params });
|
||||
case ApiPath.XAI:
|
||||
return xaiHandler(req, { params });
|
||||
case ApiPath.OpenAI:
|
||||
return openaiHandler(req, { params });
|
||||
default:
|
||||
|
||||
@@ -92,6 +92,9 @@ export function auth(req: NextRequest, modelProvider: ModelProvider) {
|
||||
systemApiKey =
|
||||
serverConfig.iflytekApiKey + ":" + serverConfig.iflytekApiSecret;
|
||||
break;
|
||||
case ModelProvider.XAI:
|
||||
systemApiKey = serverConfig.xaiApiKey;
|
||||
break;
|
||||
case ModelProvider.GPT:
|
||||
default:
|
||||
if (req.nextUrl.pathname.includes("azure/deployments")) {
|
||||
|
||||
128
app/api/xai.ts
Normal file
128
app/api/xai.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import { getServerSideConfig } from "@/app/config/server";
|
||||
import {
|
||||
XAI_BASE_URL,
|
||||
ApiPath,
|
||||
ModelProvider,
|
||||
ServiceProvider,
|
||||
} from "@/app/constant";
|
||||
import { prettyObject } from "@/app/utils/format";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/api/auth";
|
||||
import { isModelAvailableInServer } from "@/app/utils/model";
|
||||
|
||||
const serverConfig = getServerSideConfig();
|
||||
|
||||
export async function handle(
|
||||
req: NextRequest,
|
||||
{ params }: { params: { path: string[] } },
|
||||
) {
|
||||
console.log("[XAI Route] params ", params);
|
||||
|
||||
if (req.method === "OPTIONS") {
|
||||
return NextResponse.json({ body: "OK" }, { status: 200 });
|
||||
}
|
||||
|
||||
const authResult = auth(req, ModelProvider.XAI);
|
||||
if (authResult.error) {
|
||||
return NextResponse.json(authResult, {
|
||||
status: 401,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await request(req);
|
||||
return response;
|
||||
} catch (e) {
|
||||
console.error("[XAI] ", e);
|
||||
return NextResponse.json(prettyObject(e));
|
||||
}
|
||||
}
|
||||
|
||||
async function request(req: NextRequest) {
|
||||
const controller = new AbortController();
|
||||
|
||||
// alibaba use base url or just remove the path
|
||||
let path = `${req.nextUrl.pathname}`.replaceAll(ApiPath.XAI, "");
|
||||
|
||||
let baseUrl = serverConfig.xaiUrl || XAI_BASE_URL;
|
||||
|
||||
if (!baseUrl.startsWith("http")) {
|
||||
baseUrl = `https://${baseUrl}`;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, -1);
|
||||
}
|
||||
|
||||
console.log("[Proxy] ", path);
|
||||
console.log("[Base Url]", baseUrl);
|
||||
|
||||
const timeoutId = setTimeout(
|
||||
() => {
|
||||
controller.abort();
|
||||
},
|
||||
10 * 60 * 1000,
|
||||
);
|
||||
|
||||
const fetchUrl = `${baseUrl}${path}`;
|
||||
const fetchOptions: RequestInit = {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: req.headers.get("Authorization") ?? "",
|
||||
},
|
||||
method: req.method,
|
||||
body: req.body,
|
||||
redirect: "manual",
|
||||
// @ts-ignore
|
||||
duplex: "half",
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
// #1815 try to refuse some request to some models
|
||||
if (serverConfig.customModels && req.body) {
|
||||
try {
|
||||
const clonedBody = await req.text();
|
||||
fetchOptions.body = clonedBody;
|
||||
|
||||
const jsonBody = JSON.parse(clonedBody) as { model?: string };
|
||||
|
||||
// not undefined and is false
|
||||
if (
|
||||
isModelAvailableInServer(
|
||||
serverConfig.customModels,
|
||||
jsonBody?.model as string,
|
||||
ServiceProvider.XAI as string,
|
||||
)
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: true,
|
||||
message: `you are not allowed to use ${jsonBody?.model} model`,
|
||||
},
|
||||
{
|
||||
status: 403,
|
||||
},
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`[XAI] filter`, e);
|
||||
}
|
||||
}
|
||||
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");
|
||||
|
||||
return new Response(res.body, {
|
||||
status: res.status,
|
||||
statusText: res.statusText,
|
||||
headers: newHeaders,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { QwenApi } from "./platforms/alibaba";
|
||||
import { HunyuanApi } from "./platforms/tencent";
|
||||
import { MoonshotApi } from "./platforms/moonshot";
|
||||
import { SparkApi } from "./platforms/iflytek";
|
||||
import { XAIApi } from "./platforms/xai";
|
||||
|
||||
export const ROLES = ["system", "user", "assistant"] as const;
|
||||
export type MessageRole = (typeof ROLES)[number];
|
||||
@@ -152,6 +153,9 @@ export class ClientApi {
|
||||
case ModelProvider.Iflytek:
|
||||
this.llm = new SparkApi();
|
||||
break;
|
||||
case ModelProvider.XAI:
|
||||
this.llm = new XAIApi();
|
||||
break;
|
||||
default:
|
||||
this.llm = new ChatGPTApi();
|
||||
}
|
||||
@@ -239,6 +243,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
const isAlibaba = modelConfig.providerName === ServiceProvider.Alibaba;
|
||||
const isMoonshot = modelConfig.providerName === ServiceProvider.Moonshot;
|
||||
const isIflytek = modelConfig.providerName === ServiceProvider.Iflytek;
|
||||
const isXAI = modelConfig.providerName === ServiceProvider.XAI;
|
||||
const isEnabledAccessControl = accessStore.enabledAccessControl();
|
||||
const apiKey = isGoogle
|
||||
? accessStore.googleApiKey
|
||||
@@ -252,6 +257,8 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
? accessStore.alibabaApiKey
|
||||
: isMoonshot
|
||||
? accessStore.moonshotApiKey
|
||||
: isXAI
|
||||
? accessStore.xaiApiKey
|
||||
: isIflytek
|
||||
? accessStore.iflytekApiKey && accessStore.iflytekApiSecret
|
||||
? accessStore.iflytekApiKey + ":" + accessStore.iflytekApiSecret
|
||||
@@ -266,6 +273,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
|
||||
isAlibaba,
|
||||
isMoonshot,
|
||||
isIflytek,
|
||||
isXAI,
|
||||
apiKey,
|
||||
isEnabledAccessControl,
|
||||
};
|
||||
@@ -328,6 +336,8 @@ export function getClientApi(provider: ServiceProvider): ClientApi {
|
||||
return new ClientApi(ModelProvider.Moonshot);
|
||||
case ServiceProvider.Iflytek:
|
||||
return new ClientApi(ModelProvider.Iflytek);
|
||||
case ServiceProvider.XAI:
|
||||
return new ClientApi(ModelProvider.XAI);
|
||||
default:
|
||||
return new ClientApi(ModelProvider.GPT);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { getMessageTextContent, isVisionModel } from "@/app/utils";
|
||||
import { preProcessImageContent, stream } from "@/app/utils/chat";
|
||||
import { cloudflareAIGatewayUrl } from "@/app/utils/cloudflare";
|
||||
import { RequestPayload } from "./openai";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export type MultiBlockContent = {
|
||||
type: "image" | "text";
|
||||
|
||||
@@ -192,7 +192,10 @@ export class GeminiProApi implements LLMApi {
|
||||
requestPayload,
|
||||
getHeaders(),
|
||||
// @ts-ignore
|
||||
[{ functionDeclarations: tools.map((tool) => tool.function) }],
|
||||
tools.length > 0
|
||||
? // @ts-ignore
|
||||
[{ functionDeclarations: tools.map((tool) => tool.function) }]
|
||||
: [],
|
||||
funcs,
|
||||
controller,
|
||||
// parseSSE
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import { RequestPayload } from "./openai";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export class MoonshotApi implements LLMApi {
|
||||
private disableListModels = true;
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
isVisionModel,
|
||||
isDalle3 as _isDalle3,
|
||||
} from "@/app/utils";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export interface OpenAIListModelResponse {
|
||||
object: string;
|
||||
@@ -352,7 +353,7 @@ export class ChatGPTApi implements LLMApi {
|
||||
// make a fetch request
|
||||
const requestTimeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 2 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
|
||||
isDalle3 || isO1 ? REQUEST_TIMEOUT_MS * 4 : REQUEST_TIMEOUT_MS, // dalle3 using b64_json is slow.
|
||||
);
|
||||
|
||||
const res = await fetch(chatPath, chatPayload);
|
||||
|
||||
193
app/client/platforms/xai.ts
Normal file
193
app/client/platforms/xai.ts
Normal file
@@ -0,0 +1,193 @@
|
||||
"use client";
|
||||
// azure and openai, using same models. so using same LLMApi.
|
||||
import { ApiPath, XAI_BASE_URL, XAI, REQUEST_TIMEOUT_MS } from "@/app/constant";
|
||||
import {
|
||||
useAccessStore,
|
||||
useAppConfig,
|
||||
useChatStore,
|
||||
ChatMessageTool,
|
||||
usePluginStore,
|
||||
} from "@/app/store";
|
||||
import { stream } from "@/app/utils/chat";
|
||||
import {
|
||||
ChatOptions,
|
||||
getHeaders,
|
||||
LLMApi,
|
||||
LLMModel,
|
||||
SpeechOptions,
|
||||
} from "../api";
|
||||
import { getClientConfig } from "@/app/config/client";
|
||||
import { getMessageTextContent } from "@/app/utils";
|
||||
import { RequestPayload } from "./openai";
|
||||
import { fetch } from "@/app/utils/stream";
|
||||
|
||||
export class XAIApi implements LLMApi {
|
||||
private disableListModels = true;
|
||||
|
||||
path(path: string): string {
|
||||
const accessStore = useAccessStore.getState();
|
||||
|
||||
let baseUrl = "";
|
||||
|
||||
if (accessStore.useCustomConfig) {
|
||||
baseUrl = accessStore.xaiUrl;
|
||||
}
|
||||
|
||||
if (baseUrl.length === 0) {
|
||||
const isApp = !!getClientConfig()?.isApp;
|
||||
const apiPath = ApiPath.XAI;
|
||||
baseUrl = isApp ? XAI_BASE_URL : apiPath;
|
||||
}
|
||||
|
||||
if (baseUrl.endsWith("/")) {
|
||||
baseUrl = baseUrl.slice(0, baseUrl.length - 1);
|
||||
}
|
||||
if (!baseUrl.startsWith("http") && !baseUrl.startsWith(ApiPath.XAI)) {
|
||||
baseUrl = "https://" + baseUrl;
|
||||
}
|
||||
|
||||
console.log("[Proxy Endpoint] ", baseUrl, path);
|
||||
|
||||
return [baseUrl, path].join("/");
|
||||
}
|
||||
|
||||
extractMessage(res: any) {
|
||||
return res.choices?.at(0)?.message?.content ?? "";
|
||||
}
|
||||
|
||||
speech(options: SpeechOptions): Promise<ArrayBuffer> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async chat(options: ChatOptions) {
|
||||
const messages: ChatOptions["messages"] = [];
|
||||
for (const v of options.messages) {
|
||||
const content = getMessageTextContent(v);
|
||||
messages.push({ role: v.role, content });
|
||||
}
|
||||
|
||||
const modelConfig = {
|
||||
...useAppConfig.getState().modelConfig,
|
||||
...useChatStore.getState().currentSession().mask.modelConfig,
|
||||
...{
|
||||
model: options.config.model,
|
||||
providerName: options.config.providerName,
|
||||
},
|
||||
};
|
||||
|
||||
const requestPayload: RequestPayload = {
|
||||
messages,
|
||||
stream: options.config.stream,
|
||||
model: modelConfig.model,
|
||||
temperature: modelConfig.temperature,
|
||||
presence_penalty: modelConfig.presence_penalty,
|
||||
frequency_penalty: modelConfig.frequency_penalty,
|
||||
top_p: modelConfig.top_p,
|
||||
};
|
||||
|
||||
console.log("[Request] xai payload: ", requestPayload);
|
||||
|
||||
const shouldStream = !!options.config.stream;
|
||||
const controller = new AbortController();
|
||||
options.onController?.(controller);
|
||||
|
||||
try {
|
||||
const chatPath = this.path(XAI.ChatPath);
|
||||
const chatPayload = {
|
||||
method: "POST",
|
||||
body: JSON.stringify(requestPayload),
|
||||
signal: controller.signal,
|
||||
headers: getHeaders(),
|
||||
};
|
||||
|
||||
// make a fetch request
|
||||
const requestTimeoutId = setTimeout(
|
||||
() => controller.abort(),
|
||||
REQUEST_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
if (shouldStream) {
|
||||
const [tools, funcs] = usePluginStore
|
||||
.getState()
|
||||
.getAsTools(
|
||||
useChatStore.getState().currentSession().mask?.plugin || [],
|
||||
);
|
||||
return stream(
|
||||
chatPath,
|
||||
requestPayload,
|
||||
getHeaders(),
|
||||
tools as any,
|
||||
funcs,
|
||||
controller,
|
||||
// parseSSE
|
||||
(text: string, runTools: ChatMessageTool[]) => {
|
||||
// console.log("parseSSE", text, runTools);
|
||||
const json = JSON.parse(text);
|
||||
const choices = json.choices as Array<{
|
||||
delta: {
|
||||
content: string;
|
||||
tool_calls: ChatMessageTool[];
|
||||
};
|
||||
}>;
|
||||
const tool_calls = choices[0]?.delta?.tool_calls;
|
||||
if (tool_calls?.length > 0) {
|
||||
const index = tool_calls[0]?.index;
|
||||
const id = tool_calls[0]?.id;
|
||||
const args = tool_calls[0]?.function?.arguments;
|
||||
if (id) {
|
||||
runTools.push({
|
||||
id,
|
||||
type: tool_calls[0]?.type,
|
||||
function: {
|
||||
name: tool_calls[0]?.function?.name as string,
|
||||
arguments: args,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// @ts-ignore
|
||||
runTools[index]["function"]["arguments"] += args;
|
||||
}
|
||||
}
|
||||
return choices[0]?.delta?.content;
|
||||
},
|
||||
// processToolMessage, include tool_calls message and tool call results
|
||||
(
|
||||
requestPayload: RequestPayload,
|
||||
toolCallMessage: any,
|
||||
toolCallResult: any[],
|
||||
) => {
|
||||
// @ts-ignore
|
||||
requestPayload?.messages?.splice(
|
||||
// @ts-ignore
|
||||
requestPayload?.messages?.length,
|
||||
0,
|
||||
toolCallMessage,
|
||||
...toolCallResult,
|
||||
);
|
||||
},
|
||||
options,
|
||||
);
|
||||
} else {
|
||||
const res = await fetch(chatPath, chatPayload);
|
||||
clearTimeout(requestTimeoutId);
|
||||
|
||||
const resJson = await res.json();
|
||||
const message = this.extractMessage(resJson);
|
||||
options.onFinish(message);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("[Request] failed to make a chat request", e);
|
||||
options.onError?.(e as Error);
|
||||
}
|
||||
}
|
||||
async usage() {
|
||||
return {
|
||||
used: 0,
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
|
||||
async models(): Promise<LLMModel[]> {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -35,6 +35,7 @@ export function useCommand(commands: Commands = {}) {
|
||||
interface ChatCommands {
|
||||
new?: Command;
|
||||
newm?: Command;
|
||||
copy?: Command;
|
||||
next?: Command;
|
||||
prev?: Command;
|
||||
clear?: Command;
|
||||
|
||||
@@ -70,6 +70,7 @@ import {
|
||||
getMessageImages,
|
||||
isVisionModel,
|
||||
isDalle3,
|
||||
removeOutdatedEntries,
|
||||
showPlugins,
|
||||
safeLocalStorage,
|
||||
} from "../utils";
|
||||
@@ -115,11 +116,14 @@ import { getClientConfig } from "../config/client";
|
||||
import { useAllModels } from "../utils/hooks";
|
||||
import { MultimodalContent } from "../client/api";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
import { ClientApi } from "../client/api";
|
||||
import { createTTSPlayer } from "../utils/audio";
|
||||
import { MsEdgeTTS, OUTPUT_FORMAT } from "../utils/ms_edge_tts";
|
||||
|
||||
import { isEmpty } from "lodash-es";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
|
||||
const ttsPlayer = createTTSPlayer();
|
||||
|
||||
const Markdown = dynamic(async () => (await import("./markdown")).Markdown, {
|
||||
@@ -984,6 +988,7 @@ function _Chat() {
|
||||
const chatCommands = useChatCommand({
|
||||
new: () => chatStore.newSession(),
|
||||
newm: () => navigate(Path.NewChat),
|
||||
copy: () => chatStore.copySession(),
|
||||
prev: () => chatStore.nextSession(-1),
|
||||
next: () => chatStore.nextSession(1),
|
||||
clear: () =>
|
||||
@@ -1015,7 +1020,7 @@ function _Chat() {
|
||||
};
|
||||
|
||||
const doSubmit = (userInput: string) => {
|
||||
if (userInput.trim() === "") return;
|
||||
if (userInput.trim() === "" && isEmpty(attachImages)) return;
|
||||
const matchCommand = chatCommands.match(userInput);
|
||||
if (matchCommand.matched) {
|
||||
setUserInput("");
|
||||
@@ -1115,10 +1120,20 @@ function _Chat() {
|
||||
};
|
||||
|
||||
const deleteMessage = (msgId?: string) => {
|
||||
chatStore.updateCurrentSession(
|
||||
(session) =>
|
||||
(session.messages = session.messages.filter((m) => m.id !== msgId)),
|
||||
);
|
||||
chatStore.updateCurrentSession((session) => {
|
||||
session.deletedMessageIds &&
|
||||
removeOutdatedEntries(session.deletedMessageIds);
|
||||
session.messages = session.messages.filter((m) => {
|
||||
if (m.id !== msgId) {
|
||||
return true;
|
||||
}
|
||||
if (!session.deletedMessageIds) {
|
||||
session.deletedMessageIds = {} as Record<string, number>;
|
||||
}
|
||||
session.deletedMessageIds[m.id] = Date.now();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const onDelete = (msgId: string) => {
|
||||
|
||||
@@ -140,6 +140,9 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
&-narrow {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
|
||||
@@ -169,6 +169,12 @@ export function PreCode(props: { children: any }) {
|
||||
}
|
||||
|
||||
function CustomCode(props: { children: any; className?: string }) {
|
||||
const chatStore = useChatStore();
|
||||
const session = chatStore.currentSession();
|
||||
const config = useAppConfig();
|
||||
const enableCodeFold =
|
||||
session.mask?.enableCodeFold !== false && config.enableCodeFold;
|
||||
|
||||
const ref = useRef<HTMLPreElement>(null);
|
||||
const [collapsed, setCollapsed] = useState(true);
|
||||
const [showToggle, setShowToggle] = useState(false);
|
||||
@@ -184,25 +190,30 @@ function CustomCode(props: { children: any; className?: string }) {
|
||||
const toggleCollapsed = () => {
|
||||
setCollapsed((collapsed) => !collapsed);
|
||||
};
|
||||
const renderShowMoreButton = () => {
|
||||
if (showToggle && enableCodeFold && collapsed) {
|
||||
return (
|
||||
<div className={`show-hide-button ${collapsed ? "collapsed" : "expanded"}`}>
|
||||
<button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<code
|
||||
className={props?.className}
|
||||
ref={ref}
|
||||
style={{
|
||||
maxHeight: collapsed ? "400px" : "none",
|
||||
maxHeight: enableCodeFold && collapsed ? "400px" : "none",
|
||||
overflowY: "hidden",
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</code>
|
||||
{showToggle && collapsed && (
|
||||
<div
|
||||
className={`show-hide-button ${collapsed ? "collapsed" : "expanded"}`}
|
||||
>
|
||||
<button onClick={toggleCollapsed}>{Locale.NewChat.More}</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{renderShowMoreButton()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -183,6 +183,23 @@ export function MaskConfig(props: {
|
||||
></input>
|
||||
</ListItem>
|
||||
)}
|
||||
{globalConfig.enableCodeFold && (
|
||||
<ListItem
|
||||
title={Locale.Mask.Config.CodeFold.Title}
|
||||
subTitle={Locale.Mask.Config.CodeFold.SubTitle}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Mask.Config.CodeFold.Title}
|
||||
type="checkbox"
|
||||
checked={props.mask.enableCodeFold !== false}
|
||||
onChange={(e) => {
|
||||
props.updateMask((mask) => {
|
||||
mask.enableCodeFold = e.currentTarget.checked;
|
||||
});
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
)}
|
||||
|
||||
{!props.shouldSyncFromGlobal ? (
|
||||
<ListItem
|
||||
|
||||
@@ -49,7 +49,7 @@ import Locale, {
|
||||
changeLang,
|
||||
getLang,
|
||||
} from "../locales";
|
||||
import { copyToClipboard } from "../utils";
|
||||
import { copyToClipboard, clientUpdate, semverCompare } from "../utils";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Anthropic,
|
||||
@@ -59,6 +59,7 @@ import {
|
||||
ByteDance,
|
||||
Alibaba,
|
||||
Moonshot,
|
||||
XAI,
|
||||
Google,
|
||||
GoogleSafetySettingsThreshold,
|
||||
OPENAI_BASE_URL,
|
||||
@@ -360,6 +361,21 @@ function SyncConfigModal(props: { onClose?: () => void }) {
|
||||
</select>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={Locale.Settings.Sync.Config.EnableAutoSync.Title}
|
||||
subTitle={Locale.Settings.Sync.Config.EnableAutoSync.SubTitle}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={syncStore.enableAutoSync}
|
||||
onChange={(e) => {
|
||||
syncStore.update(
|
||||
(config) => (config.enableAutoSync = e.currentTarget.checked),
|
||||
);
|
||||
}}
|
||||
></input>
|
||||
</ListItem>
|
||||
|
||||
<ListItem
|
||||
title={Locale.Settings.Sync.Config.Proxy.Title}
|
||||
subTitle={Locale.Settings.Sync.Config.Proxy.SubTitle}
|
||||
@@ -585,7 +601,7 @@ export function Settings() {
|
||||
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
||||
const currentVersion = updateStore.formatVersion(updateStore.version);
|
||||
const remoteId = updateStore.formatVersion(updateStore.remoteVersion);
|
||||
const hasNewVersion = currentVersion !== remoteId;
|
||||
const hasNewVersion = semverCompare(currentVersion, remoteId) === -1;
|
||||
const updateUrl = getClientConfig()?.isApp ? RELEASE_URL : UPDATE_URL;
|
||||
|
||||
function checkUpdate(force = false) {
|
||||
@@ -1194,6 +1210,45 @@ export function Settings() {
|
||||
</>
|
||||
);
|
||||
|
||||
const XAIConfigComponent = accessStore.provider === ServiceProvider.XAI && (
|
||||
<>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.XAI.Endpoint.Title}
|
||||
subTitle={
|
||||
Locale.Settings.Access.XAI.Endpoint.SubTitle + XAI.ExampleEndpoint
|
||||
}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Settings.Access.XAI.Endpoint.Title}
|
||||
type="text"
|
||||
value={accessStore.xaiUrl}
|
||||
placeholder={XAI.ExampleEndpoint}
|
||||
onChange={(e) =>
|
||||
accessStore.update(
|
||||
(access) => (access.xaiUrl = e.currentTarget.value),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Settings.Access.XAI.ApiKey.Title}
|
||||
subTitle={Locale.Settings.Access.XAI.ApiKey.SubTitle}
|
||||
>
|
||||
<PasswordInput
|
||||
aria-label={Locale.Settings.Access.XAI.ApiKey.Title}
|
||||
value={accessStore.xaiApiKey}
|
||||
type="text"
|
||||
placeholder={Locale.Settings.Access.XAI.ApiKey.Placeholder}
|
||||
onChange={(e) => {
|
||||
accessStore.update(
|
||||
(access) => (access.xaiApiKey = e.currentTarget.value),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
</>
|
||||
);
|
||||
|
||||
const stabilityConfigComponent = accessStore.provider ===
|
||||
ServiceProvider.Stability && (
|
||||
<>
|
||||
@@ -1357,9 +1412,17 @@ export function Settings() {
|
||||
{checkingUpdate ? (
|
||||
<LoadingIcon />
|
||||
) : hasNewVersion ? (
|
||||
<Link href={updateUrl} target="_blank" className="link">
|
||||
{Locale.Settings.Update.GoToUpdate}
|
||||
</Link>
|
||||
clientConfig?.isApp ? (
|
||||
<IconButton
|
||||
icon={<ResetIcon></ResetIcon>}
|
||||
text={Locale.Settings.Update.GoToUpdate}
|
||||
onClick={() => clientUpdate()}
|
||||
/>
|
||||
) : (
|
||||
<Link href={updateUrl} target="_blank" className="link">
|
||||
{Locale.Settings.Update.GoToUpdate}
|
||||
</Link>
|
||||
)
|
||||
) : (
|
||||
<IconButton
|
||||
icon={<ResetIcon></ResetIcon>}
|
||||
@@ -1509,6 +1572,22 @@ export function Settings() {
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
<ListItem
|
||||
title={Locale.Mask.Config.CodeFold.Title}
|
||||
subTitle={Locale.Mask.Config.CodeFold.SubTitle}
|
||||
>
|
||||
<input
|
||||
aria-label={Locale.Mask.Config.CodeFold.Title}
|
||||
type="checkbox"
|
||||
checked={config.enableCodeFold}
|
||||
data-testid="enable-code-fold-checkbox"
|
||||
onChange={(e) =>
|
||||
updateConfig(
|
||||
(config) => (config.enableCodeFold = e.currentTarget.checked),
|
||||
)
|
||||
}
|
||||
></input>
|
||||
</ListItem>
|
||||
</List>
|
||||
|
||||
<SyncItems />
|
||||
@@ -1628,6 +1707,7 @@ export function Settings() {
|
||||
{moonshotConfigComponent}
|
||||
{stabilityConfigComponent}
|
||||
{lflytekConfigComponent}
|
||||
{XAIConfigComponent}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -165,11 +165,17 @@ export function SideBarHeader(props: {
|
||||
subTitle?: string | React.ReactNode;
|
||||
logo?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
shouldNarrow?: boolean;
|
||||
}) {
|
||||
const { title, subTitle, logo, children } = props;
|
||||
const { title, subTitle, logo, children, shouldNarrow } = props;
|
||||
return (
|
||||
<Fragment>
|
||||
<div className={styles["sidebar-header"]} data-tauri-drag-region>
|
||||
<div
|
||||
className={`${styles["sidebar-header"]} ${
|
||||
shouldNarrow ? styles["sidebar-header-narrow"] : ""
|
||||
}`}
|
||||
data-tauri-drag-region
|
||||
>
|
||||
<div className={styles["sidebar-title-container"]}>
|
||||
<div className={styles["sidebar-title"]} data-tauri-drag-region>
|
||||
{title}
|
||||
@@ -227,6 +233,7 @@ export function SideBar(props: { className?: string }) {
|
||||
title="NextChat"
|
||||
subTitle="Build your own AI assistant."
|
||||
logo={<ChatGptIcon />}
|
||||
shouldNarrow={shouldNarrow}
|
||||
>
|
||||
<div className={styles["sidebar-header-bar"]}>
|
||||
<IconButton
|
||||
|
||||
@@ -71,6 +71,10 @@ declare global {
|
||||
IFLYTEK_API_KEY?: string;
|
||||
IFLYTEK_API_SECRET?: string;
|
||||
|
||||
// xai only
|
||||
XAI_URL?: string;
|
||||
XAI_API_KEY?: string;
|
||||
|
||||
// custom template for preprocessing user input
|
||||
DEFAULT_INPUT_TEMPLATE?: string;
|
||||
}
|
||||
@@ -146,6 +150,7 @@ export const getServerSideConfig = () => {
|
||||
const isAlibaba = !!process.env.ALIBABA_API_KEY;
|
||||
const isMoonshot = !!process.env.MOONSHOT_API_KEY;
|
||||
const isIflytek = !!process.env.IFLYTEK_API_KEY;
|
||||
const isXAI = !!process.env.XAI_API_KEY;
|
||||
// const apiKeyEnvVar = process.env.OPENAI_API_KEY ?? "";
|
||||
// const apiKeys = apiKeyEnvVar.split(",").map((v) => v.trim());
|
||||
// const randomIndex = Math.floor(Math.random() * apiKeys.length);
|
||||
@@ -208,6 +213,10 @@ export const getServerSideConfig = () => {
|
||||
iflytekApiKey: process.env.IFLYTEK_API_KEY,
|
||||
iflytekApiSecret: process.env.IFLYTEK_API_SECRET,
|
||||
|
||||
isXAI,
|
||||
xaiUrl: process.env.XAI_URL,
|
||||
xaiApiKey: getApiKey(process.env.XAI_API_KEY),
|
||||
|
||||
cloudflareAccountId: process.env.CLOUDFLARE_ACCOUNT_ID,
|
||||
cloudflareKVNamespaceId: process.env.CLOUDFLARE_KV_NAMESPACE_ID,
|
||||
cloudflareKVApiKey: getApiKey(process.env.CLOUDFLARE_KV_API_KEY),
|
||||
|
||||
@@ -28,6 +28,8 @@ export const TENCENT_BASE_URL = "https://hunyuan.tencentcloudapi.com";
|
||||
export const MOONSHOT_BASE_URL = "https://api.moonshot.cn";
|
||||
export const IFLYTEK_BASE_URL = "https://spark-api-open.xf-yun.com";
|
||||
|
||||
export const XAI_BASE_URL = "https://api.x.ai";
|
||||
|
||||
export const CACHE_URL_PREFIX = "/api/cache";
|
||||
export const UPLOAD_URL = `${CACHE_URL_PREFIX}/upload`;
|
||||
|
||||
@@ -59,6 +61,7 @@ export enum ApiPath {
|
||||
Iflytek = "/api/iflytek",
|
||||
Stability = "/api/stability",
|
||||
Artifacts = "/api/artifacts",
|
||||
XAI = "/api/xai",
|
||||
}
|
||||
|
||||
export enum SlotID {
|
||||
@@ -111,6 +114,7 @@ export enum ServiceProvider {
|
||||
Moonshot = "Moonshot",
|
||||
Stability = "Stability",
|
||||
Iflytek = "Iflytek",
|
||||
XAI = "XAI",
|
||||
}
|
||||
|
||||
// Google API safety settings, see https://ai.google.dev/gemini-api/docs/safety-settings
|
||||
@@ -133,6 +137,7 @@ export enum ModelProvider {
|
||||
Hunyuan = "Hunyuan",
|
||||
Moonshot = "Moonshot",
|
||||
Iflytek = "Iflytek",
|
||||
XAI = "XAI",
|
||||
}
|
||||
|
||||
export const Stability = {
|
||||
@@ -215,6 +220,11 @@ export const Iflytek = {
|
||||
ChatPath: "v1/chat/completions",
|
||||
};
|
||||
|
||||
export const XAI = {
|
||||
ExampleEndpoint: XAI_BASE_URL,
|
||||
ChatPath: "v1/chat/completions",
|
||||
};
|
||||
|
||||
export const DEFAULT_INPUT_TEMPLATE = `{{input}}`; // input / time / model / lang
|
||||
// export const DEFAULT_SYSTEM_TEMPLATE = `
|
||||
// You are ChatGPT, a large language model trained by {{ServiceProvider}}.
|
||||
@@ -364,6 +374,8 @@ const iflytekModels = [
|
||||
"4.0Ultra",
|
||||
];
|
||||
|
||||
const xAIModes = ["grok-beta"];
|
||||
|
||||
let seq = 1000; // 内置的模型序号生成器从1000开始
|
||||
export const DEFAULT_MODELS = [
|
||||
...openaiModels.map((name) => ({
|
||||
@@ -476,6 +488,17 @@ export const DEFAULT_MODELS = [
|
||||
sorted: 10,
|
||||
},
|
||||
})),
|
||||
...xAIModes.map((name) => ({
|
||||
name,
|
||||
available: true,
|
||||
sorted: seq++,
|
||||
provider: {
|
||||
id: "xai",
|
||||
providerName: "XAI",
|
||||
providerType: "xai",
|
||||
sorted: 11,
|
||||
},
|
||||
})),
|
||||
] as const;
|
||||
|
||||
export const CHAT_PAGE_SIZE = 15;
|
||||
|
||||
7
app/global.d.ts
vendored
7
app/global.d.ts
vendored
@@ -26,6 +26,13 @@ declare interface Window {
|
||||
isPermissionGranted(): Promise<boolean>;
|
||||
sendNotification(options: string | Options): void;
|
||||
};
|
||||
updater: {
|
||||
checkUpdate(): Promise<UpdateResult>;
|
||||
installUpdate(): Promise<void>;
|
||||
onUpdaterEvent(
|
||||
handler: (status: UpdateStatusResult) => void,
|
||||
): Promise<UnlistenFn>;
|
||||
};
|
||||
http: {
|
||||
fetch<T>(
|
||||
url: string,
|
||||
|
||||
@@ -62,6 +62,7 @@ const cn = {
|
||||
Commands: {
|
||||
new: "新建聊天",
|
||||
newm: "从面具新建聊天",
|
||||
copy: "复制当前聊天",
|
||||
next: "下一个聊天",
|
||||
prev: "上一个聊天",
|
||||
clear: "清除上下文",
|
||||
@@ -205,6 +206,8 @@ const cn = {
|
||||
IsChecking: "正在检查更新...",
|
||||
FoundUpdate: (x: string) => `发现新版本:${x}`,
|
||||
GoToUpdate: "前往更新",
|
||||
Success: "更新成功!",
|
||||
Failed: "更新失败",
|
||||
},
|
||||
SendKey: "发送键",
|
||||
Theme: "主题",
|
||||
@@ -232,6 +235,10 @@ const cn = {
|
||||
Title: "同步类型",
|
||||
SubTitle: "选择喜爱的同步服务器",
|
||||
},
|
||||
EnableAutoSync: {
|
||||
Title: "自动同步设置",
|
||||
SubTitle: "在回复完成或删除消息后自动同步数据",
|
||||
},
|
||||
Proxy: {
|
||||
Title: "启用代理",
|
||||
SubTitle: "在浏览器中同步时,必须启用代理以避免跨域限制",
|
||||
@@ -460,6 +467,17 @@ const cn = {
|
||||
SubTitle: "样例:",
|
||||
},
|
||||
},
|
||||
XAI: {
|
||||
ApiKey: {
|
||||
Title: "接口密钥",
|
||||
SubTitle: "使用自定义XAI API Key",
|
||||
Placeholder: "XAI API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "接口地址",
|
||||
SubTitle: "样例:",
|
||||
},
|
||||
},
|
||||
Stability: {
|
||||
ApiKey: {
|
||||
Title: "接口密钥",
|
||||
@@ -495,8 +513,8 @@ const cn = {
|
||||
|
||||
Model: "模型 (model)",
|
||||
CompressModel: {
|
||||
Title: "压缩模型",
|
||||
SubTitle: "用于压缩历史记录的模型",
|
||||
Title: "对话摘要模型",
|
||||
SubTitle: "用于压缩历史记录、生成对话标题的模型",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "随机性 (temperature)",
|
||||
@@ -665,6 +683,10 @@ const cn = {
|
||||
Title: "启用Artifacts",
|
||||
SubTitle: "启用之后可以直接渲染HTML页面",
|
||||
},
|
||||
CodeFold: {
|
||||
Title: "启用代码折叠",
|
||||
SubTitle: "启用之后可以自动折叠/展开过长的代码块",
|
||||
},
|
||||
Share: {
|
||||
Title: "分享此面具",
|
||||
SubTitle: "生成此面具的直达链接",
|
||||
|
||||
@@ -63,6 +63,7 @@ const en: LocaleType = {
|
||||
Commands: {
|
||||
new: "Start a new chat",
|
||||
newm: "Start a new chat with mask",
|
||||
copy: "Copy the current Chat",
|
||||
next: "Next Chat",
|
||||
prev: "Previous Chat",
|
||||
clear: "Clear Context",
|
||||
@@ -207,6 +208,8 @@ const en: LocaleType = {
|
||||
IsChecking: "Checking update...",
|
||||
FoundUpdate: (x: string) => `Found new version: ${x}`,
|
||||
GoToUpdate: "Update",
|
||||
Success: "Update Successful.",
|
||||
Failed: "Update Failed.",
|
||||
},
|
||||
SendKey: "Send Key",
|
||||
Theme: "Theme",
|
||||
@@ -234,6 +237,11 @@ const en: LocaleType = {
|
||||
Title: "Sync Type",
|
||||
SubTitle: "Choose your favorite sync service",
|
||||
},
|
||||
EnableAutoSync: {
|
||||
Title: "Auto Sync Settings",
|
||||
SubTitle:
|
||||
"Automatically synchronize data after replying or deleting messages",
|
||||
},
|
||||
Proxy: {
|
||||
Title: "Enable CORS Proxy",
|
||||
SubTitle: "Enable a proxy to avoid cross-origin restrictions",
|
||||
@@ -444,6 +452,17 @@ const en: LocaleType = {
|
||||
SubTitle: "Example: ",
|
||||
},
|
||||
},
|
||||
XAI: {
|
||||
ApiKey: {
|
||||
Title: "XAI API Key",
|
||||
SubTitle: "Use a custom XAI API Key",
|
||||
Placeholder: "XAI API Key",
|
||||
},
|
||||
Endpoint: {
|
||||
Title: "Endpoint Address",
|
||||
SubTitle: "Example: ",
|
||||
},
|
||||
},
|
||||
Stability: {
|
||||
ApiKey: {
|
||||
Title: "Stability API Key",
|
||||
@@ -500,8 +519,8 @@ const en: LocaleType = {
|
||||
|
||||
Model: "Model",
|
||||
CompressModel: {
|
||||
Title: "Compression Model",
|
||||
SubTitle: "Model used to compress history",
|
||||
Title: "Summary Model",
|
||||
SubTitle: "Model used to compress history and generate title",
|
||||
},
|
||||
Temperature: {
|
||||
Title: "Temperature",
|
||||
@@ -675,6 +694,11 @@ const en: LocaleType = {
|
||||
Title: "Enable Artifacts",
|
||||
SubTitle: "Can render HTML page when enable artifacts.",
|
||||
},
|
||||
CodeFold: {
|
||||
Title: "Enable CodeFold",
|
||||
SubTitle:
|
||||
"Automatically collapse/expand overly long code blocks when CodeFold is enabled",
|
||||
},
|
||||
Share: {
|
||||
Title: "Share This Mask",
|
||||
SubTitle: "Generate a link to this mask",
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
MOONSHOT_BASE_URL,
|
||||
STABILITY_BASE_URL,
|
||||
IFLYTEK_BASE_URL,
|
||||
XAI_BASE_URL,
|
||||
} from "../constant";
|
||||
import { getHeaders } from "../client/api";
|
||||
import { getClientConfig } from "../config/client";
|
||||
@@ -44,6 +45,8 @@ const DEFAULT_STABILITY_URL = isApp ? STABILITY_BASE_URL : ApiPath.Stability;
|
||||
|
||||
const DEFAULT_IFLYTEK_URL = isApp ? IFLYTEK_BASE_URL : ApiPath.Iflytek;
|
||||
|
||||
const DEFAULT_XAI_URL = isApp ? XAI_BASE_URL : ApiPath.XAI;
|
||||
|
||||
const DEFAULT_ACCESS_STATE = {
|
||||
accessCode: "",
|
||||
useCustomConfig: false,
|
||||
@@ -101,6 +104,10 @@ const DEFAULT_ACCESS_STATE = {
|
||||
iflytekApiKey: "",
|
||||
iflytekApiSecret: "",
|
||||
|
||||
// xai
|
||||
xaiUrl: DEFAULT_XAI_URL,
|
||||
xaiApiKey: "",
|
||||
|
||||
// server config
|
||||
needCode: true,
|
||||
hideUserApiKey: false,
|
||||
@@ -169,6 +176,10 @@ export const useAccessStore = createPersistStore(
|
||||
return ensure(get(), ["iflytekApiKey"]);
|
||||
},
|
||||
|
||||
isValidXAI() {
|
||||
return ensure(get(), ["xaiApiKey"]);
|
||||
},
|
||||
|
||||
isAuthorized() {
|
||||
this.fetch();
|
||||
|
||||
@@ -184,6 +195,7 @@ export const useAccessStore = createPersistStore(
|
||||
this.isValidTencent() ||
|
||||
this.isValidMoonshot() ||
|
||||
this.isValidIflytek() ||
|
||||
this.isValidXAI() ||
|
||||
!this.enabledAccessControl() ||
|
||||
(this.enabledAccessControl() && ensure(get(), ["accessCode"]))
|
||||
);
|
||||
@@ -211,7 +223,7 @@ export const useAccessStore = createPersistStore(
|
||||
})
|
||||
.then((res: DangerConfig) => {
|
||||
console.log("[Config] got config from server", res);
|
||||
set(() => ({ ...res }));
|
||||
set(() => ({ lastUpdateTime: Date.now(), ...res }));
|
||||
})
|
||||
.catch(() => {
|
||||
console.error("[Config] failed to fetch config");
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { getMessageTextContent, trimTopic } from "../utils";
|
||||
import {
|
||||
getMessageTextContent,
|
||||
trimTopic,
|
||||
removeOutdatedEntries,
|
||||
} from "../utils";
|
||||
|
||||
import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
|
||||
import { nanoid } from "nanoid";
|
||||
@@ -29,6 +33,7 @@ import { ModelConfig, ModelType, useAppConfig } from "./config";
|
||||
import { useAccessStore } from "./access";
|
||||
import { collectModelsWithDefaultModel } from "../utils/model";
|
||||
import { createEmptyMask, Mask } from "./mask";
|
||||
import { useSyncStore } from "./sync";
|
||||
|
||||
const localStorage = safeLocalStorage();
|
||||
|
||||
@@ -80,6 +85,7 @@ export interface ChatSession {
|
||||
lastUpdate: number;
|
||||
lastSummarizeIndex: number;
|
||||
clearContextIndex?: number;
|
||||
deletedMessageIds?: Record<string, number>;
|
||||
|
||||
mask: Mask;
|
||||
}
|
||||
@@ -103,6 +109,7 @@ function createEmptySession(): ChatSession {
|
||||
},
|
||||
lastUpdate: Date.now(),
|
||||
lastSummarizeIndex: 0,
|
||||
deletedMessageIds: {},
|
||||
|
||||
mask: createEmptyMask(),
|
||||
};
|
||||
@@ -188,9 +195,19 @@ function fillTemplateWith(input: string, modelConfig: ModelConfig) {
|
||||
return output;
|
||||
}
|
||||
|
||||
let cloudSyncTimer: any = null;
|
||||
function noticeCloudSync(): void {
|
||||
const syncStore = useSyncStore.getState();
|
||||
cloudSyncTimer && clearTimeout(cloudSyncTimer);
|
||||
cloudSyncTimer = setTimeout(() => {
|
||||
syncStore.autoSync();
|
||||
}, 500);
|
||||
}
|
||||
|
||||
const DEFAULT_CHAT_STATE = {
|
||||
sessions: [createEmptySession()],
|
||||
currentSessionIndex: 0,
|
||||
deletedSessionIds: {} as Record<string, number>,
|
||||
lastInput: "",
|
||||
};
|
||||
|
||||
@@ -240,6 +257,28 @@ export const useChatStore = createPersistStore(
|
||||
});
|
||||
},
|
||||
|
||||
copySession() {
|
||||
set((state) => {
|
||||
const { sessions, currentSessionIndex } = state;
|
||||
const emptySession = createEmptySession();
|
||||
|
||||
// copy the session
|
||||
const curSession = JSON.parse(
|
||||
JSON.stringify(sessions[currentSessionIndex]),
|
||||
);
|
||||
curSession.id = emptySession.id;
|
||||
curSession.lastUpdate = emptySession.lastUpdate;
|
||||
|
||||
const newSessions = [...sessions];
|
||||
newSessions.splice(0, 0, curSession);
|
||||
|
||||
return {
|
||||
currentSessionIndex: 0,
|
||||
sessions: newSessions,
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
moveSession(from: number, to: number) {
|
||||
set((state) => {
|
||||
const { sessions, currentSessionIndex: oldIndex } = state;
|
||||
@@ -302,7 +341,18 @@ export const useChatStore = createPersistStore(
|
||||
if (!deletedSession) return;
|
||||
|
||||
const sessions = get().sessions.slice();
|
||||
sessions.splice(index, 1);
|
||||
const deletedSessionIds = { ...get().deletedSessionIds };
|
||||
|
||||
removeOutdatedEntries(deletedSessionIds);
|
||||
|
||||
const hasDelSessions = sessions.splice(index, 1);
|
||||
if (hasDelSessions?.length) {
|
||||
hasDelSessions.forEach((session) => {
|
||||
if (session.messages.length > 0) {
|
||||
deletedSessionIds[session.id] = Date.now();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const currentIndex = get().currentSessionIndex;
|
||||
let nextIndex = Math.min(
|
||||
@@ -319,19 +369,24 @@ export const useChatStore = createPersistStore(
|
||||
const restoreState = {
|
||||
currentSessionIndex: get().currentSessionIndex,
|
||||
sessions: get().sessions.slice(),
|
||||
deletedSessionIds: get().deletedSessionIds,
|
||||
};
|
||||
|
||||
set(() => ({
|
||||
currentSessionIndex: nextIndex,
|
||||
sessions,
|
||||
deletedSessionIds,
|
||||
}));
|
||||
|
||||
noticeCloudSync();
|
||||
|
||||
showToast(
|
||||
Locale.Home.DeleteToast,
|
||||
{
|
||||
text: Locale.Home.Revert,
|
||||
onClick() {
|
||||
set(() => restoreState);
|
||||
noticeCloudSync();
|
||||
},
|
||||
},
|
||||
5000,
|
||||
@@ -352,6 +407,24 @@ export const useChatStore = createPersistStore(
|
||||
return session;
|
||||
},
|
||||
|
||||
sortSessions() {
|
||||
const currentSession = get().currentSession();
|
||||
const sessions = get().sessions.slice();
|
||||
|
||||
sessions.sort(
|
||||
(a, b) =>
|
||||
new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(),
|
||||
);
|
||||
const currentSessionIndex = sessions.findIndex((session) => {
|
||||
return session && currentSession && session.id === currentSession.id;
|
||||
});
|
||||
|
||||
set((state) => ({
|
||||
currentSessionIndex,
|
||||
sessions,
|
||||
}));
|
||||
},
|
||||
|
||||
onNewMessage(message: ChatMessage) {
|
||||
get().updateCurrentSession((session) => {
|
||||
session.messages = session.messages.concat();
|
||||
@@ -359,6 +432,8 @@ export const useChatStore = createPersistStore(
|
||||
});
|
||||
get().updateStat(message);
|
||||
get().summarizeSession();
|
||||
get().sortSessions();
|
||||
noticeCloudSync();
|
||||
},
|
||||
|
||||
async onUserInput(content: string, attachImages?: string[]) {
|
||||
@@ -372,22 +447,16 @@ export const useChatStore = createPersistStore(
|
||||
|
||||
if (attachImages && attachImages.length > 0) {
|
||||
mContent = [
|
||||
{
|
||||
type: "text",
|
||||
text: userContent,
|
||||
},
|
||||
...(userContent
|
||||
? [{ type: "text" as const, text: userContent }]
|
||||
: []),
|
||||
...attachImages.map((url) => ({
|
||||
type: "image_url" as const,
|
||||
image_url: { url },
|
||||
})),
|
||||
];
|
||||
mContent = mContent.concat(
|
||||
attachImages.map((url) => {
|
||||
return {
|
||||
type: "image_url",
|
||||
image_url: {
|
||||
url: url,
|
||||
},
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let userMessage: ChatMessage = createMessage({
|
||||
role: "user",
|
||||
content: mContent,
|
||||
|
||||
@@ -52,6 +52,8 @@ export const DEFAULT_CONFIG = {
|
||||
|
||||
enableArtifacts: true, // show artifacts config
|
||||
|
||||
enableCodeFold: true, // code fold config
|
||||
|
||||
disablePromptHint: false,
|
||||
|
||||
dontShowMaskSplashScreen: false, // dont show splash screen when create chat
|
||||
|
||||
@@ -19,6 +19,7 @@ export type Mask = {
|
||||
builtin: boolean;
|
||||
plugin?: string[];
|
||||
enableArtifacts?: boolean;
|
||||
enableCodeFold?: boolean;
|
||||
};
|
||||
|
||||
export const DEFAULT_MASK_STATE = {
|
||||
|
||||
@@ -24,6 +24,7 @@ export type SyncStore = GetStoreState<typeof useSyncStore>;
|
||||
|
||||
const DEFAULT_SYNC_STATE = {
|
||||
provider: ProviderType.WebDAV,
|
||||
enableAutoSync: true,
|
||||
useProxy: true,
|
||||
proxyUrl: ApiPath.Cors as string,
|
||||
|
||||
@@ -43,6 +44,8 @@ const DEFAULT_SYNC_STATE = {
|
||||
lastProvider: "",
|
||||
};
|
||||
|
||||
let lastSyncTime = 0;
|
||||
|
||||
export const useSyncStore = createPersistStore(
|
||||
DEFAULT_SYNC_STATE,
|
||||
(set, get) => ({
|
||||
@@ -89,6 +92,16 @@ export const useSyncStore = createPersistStore(
|
||||
},
|
||||
|
||||
async sync() {
|
||||
if (lastSyncTime && lastSyncTime >= Date.now() - 800) {
|
||||
return;
|
||||
}
|
||||
lastSyncTime = Date.now();
|
||||
|
||||
const enableAutoSync = get().enableAutoSync;
|
||||
if (!enableAutoSync) {
|
||||
return;
|
||||
}
|
||||
|
||||
const localState = getLocalAppState();
|
||||
const provider = get().provider;
|
||||
const config = get()[provider];
|
||||
@@ -103,9 +116,7 @@ export const useSyncStore = createPersistStore(
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
const parsedRemoteState = JSON.parse(
|
||||
await client.get(config.username),
|
||||
) as AppState;
|
||||
const parsedRemoteState = JSON.parse(remoteState) as AppState;
|
||||
mergeAppState(localState, parsedRemoteState);
|
||||
setLocalAppState(localState);
|
||||
}
|
||||
@@ -123,6 +134,14 @@ export const useSyncStore = createPersistStore(
|
||||
const client = this.getClient();
|
||||
return await client.check();
|
||||
},
|
||||
|
||||
async autoSync() {
|
||||
const { lastSyncTime, provider } = get();
|
||||
const syncStore = useSyncStore.getState();
|
||||
if (lastSyncTime && syncStore.cloudSync()) {
|
||||
syncStore.sync();
|
||||
}
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: StoreKey.Sync,
|
||||
|
||||
@@ -6,6 +6,7 @@ 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 { ClientApi } from "../client/api";
|
||||
@@ -119,6 +120,7 @@ export const useUpdateStore = createPersistStore(
|
||||
icon: `${ChatGptIcon.src}`,
|
||||
sound: "Default",
|
||||
});
|
||||
clientUpdate();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
47
app/utils.ts
47
app/utils.ts
@@ -274,6 +274,19 @@ export function isDalle3(model: string) {
|
||||
return "dall-e-3" === model;
|
||||
}
|
||||
|
||||
export function removeOutdatedEntries(
|
||||
timeMap: Record<string, number>,
|
||||
): Record<string, number> {
|
||||
const oneMonthAgo = Date.now() - 30 * 24 * 60 * 60 * 1000;
|
||||
// Delete data from a month ago
|
||||
Object.keys(timeMap).forEach((id) => {
|
||||
if (timeMap[id] < oneMonthAgo) {
|
||||
delete timeMap[id];
|
||||
}
|
||||
});
|
||||
return timeMap;
|
||||
}
|
||||
|
||||
export function showPlugins(provider: ServiceProvider, model: string) {
|
||||
if (
|
||||
provider == ServiceProvider.OpenAI ||
|
||||
@@ -386,3 +399,37 @@ export function getOperationId(operation: {
|
||||
`${operation.method.toUpperCase()}${operation.path.replaceAll("/", "_")}`
|
||||
);
|
||||
}
|
||||
|
||||
export function clientUpdate() {
|
||||
// this a wild for updating client app
|
||||
return window.__TAURI__?.updater
|
||||
.checkUpdate()
|
||||
.then((updateResult) => {
|
||||
if (updateResult.shouldUpdate) {
|
||||
window.__TAURI__?.updater
|
||||
.installUpdate()
|
||||
.then((result) => {
|
||||
showToast(Locale.Settings.Update.Success);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("[Install Update Error]", e);
|
||||
showToast(Locale.Settings.Update.Failed);
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("[Check Update Error]", e);
|
||||
showToast(Locale.Settings.Update.Failed);
|
||||
});
|
||||
}
|
||||
|
||||
// https://gist.github.com/iwill/a83038623ba4fef6abb9efca87ae9ccb
|
||||
export function semverCompare(a: string, b: string) {
|
||||
if (a.startsWith(b + "-")) return -1;
|
||||
if (b.startsWith(a + "-")) return 1;
|
||||
return a.localeCompare(b, undefined, {
|
||||
numeric: true,
|
||||
sensitivity: "case",
|
||||
caseFirst: "upper",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -100,7 +100,8 @@ export function fetch(url: string, options?: RequestInit): Promise<any> {
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error("stream error", e);
|
||||
throw e;
|
||||
// throw e;
|
||||
return new Response("", { status: 599 });
|
||||
});
|
||||
}
|
||||
return window.fetch(url, options);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useMaskStore } from "../store/mask";
|
||||
import { usePromptStore } from "../store/prompt";
|
||||
import { StoreKey } from "../constant";
|
||||
import { merge } from "./merge";
|
||||
import { removeOutdatedEntries } from "@/app/utils";
|
||||
|
||||
type NonFunctionKeys<T> = {
|
||||
[K in keyof T]: T[K] extends (...args: any[]) => any ? never : K;
|
||||
@@ -65,7 +66,10 @@ type StateMerger = {
|
||||
const MergeStates: StateMerger = {
|
||||
[StoreKey.Chat]: (localState, remoteState) => {
|
||||
// merge sessions
|
||||
const currentSession = useChatStore.getState().currentSession();
|
||||
|
||||
const localSessions: Record<string, ChatSession> = {};
|
||||
const localDeletedSessionIds = localState.deletedSessionIds || {};
|
||||
localState.sessions.forEach((s) => (localSessions[s.id] = s));
|
||||
|
||||
remoteState.sessions.forEach((remoteSession) => {
|
||||
@@ -75,29 +79,98 @@ const MergeStates: StateMerger = {
|
||||
const localSession = localSessions[remoteSession.id];
|
||||
if (!localSession) {
|
||||
// if remote session is new, just merge it
|
||||
localState.sessions.push(remoteSession);
|
||||
if (
|
||||
(localDeletedSessionIds[remoteSession.id] || -1) <
|
||||
remoteSession.lastUpdate
|
||||
) {
|
||||
localState.sessions.push(remoteSession);
|
||||
}
|
||||
} else {
|
||||
// if both have the same session id, merge the messages
|
||||
const localMessageIds = new Set(localSession.messages.map((v) => v.id));
|
||||
const localDeletedMessageIds = localSession.deletedMessageIds || {};
|
||||
remoteSession.messages.forEach((m) => {
|
||||
if (!localMessageIds.has(m.id)) {
|
||||
localSession.messages.push(m);
|
||||
if (
|
||||
!localDeletedMessageIds[m.id] ||
|
||||
new Date(localDeletedMessageIds[m.id]).toLocaleString() < m.date
|
||||
) {
|
||||
localSession.messages.push(m);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const remoteDeletedMessageIds = remoteSession.deletedMessageIds || {};
|
||||
localSession.messages = localSession.messages.filter((localMessage) => {
|
||||
return (
|
||||
!remoteDeletedMessageIds[localMessage.id] ||
|
||||
new Date(localDeletedMessageIds[localMessage.id]).toLocaleString() <
|
||||
localMessage.date
|
||||
);
|
||||
});
|
||||
|
||||
// sort local messages with date field in asc order
|
||||
localSession.messages.sort(
|
||||
(a, b) => new Date(a.date).getTime() - new Date(b.date).getTime(),
|
||||
);
|
||||
localSession.lastUpdate = Math.max(
|
||||
remoteSession.lastUpdate,
|
||||
localSession.lastUpdate,
|
||||
);
|
||||
|
||||
const deletedMessageIds = {
|
||||
...remoteDeletedMessageIds,
|
||||
...localDeletedMessageIds,
|
||||
};
|
||||
removeOutdatedEntries(deletedMessageIds);
|
||||
localSession.deletedMessageIds = deletedMessageIds;
|
||||
}
|
||||
});
|
||||
|
||||
const remoteDeletedSessionIds = remoteState.deletedSessionIds || {};
|
||||
|
||||
const finalIds: Record<string, any> = {};
|
||||
localState.sessions = localState.sessions.filter((localSession) => {
|
||||
// 去除掉重复的会话
|
||||
if (finalIds[localSession.id]) {
|
||||
return false;
|
||||
}
|
||||
finalIds[localSession.id] = true;
|
||||
|
||||
// 去除掉非首个空会话,避免多个空会话在中间,不方便管理
|
||||
if (
|
||||
localSession.messages.length === 0 &&
|
||||
localSession != localState.sessions[0]
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 去除云端删除并且删除时间小于本地修改时间的会话
|
||||
return (
|
||||
(remoteDeletedSessionIds[localSession.id] || -1) <=
|
||||
localSession.lastUpdate
|
||||
);
|
||||
});
|
||||
|
||||
// sort local sessions with date field in desc order
|
||||
localState.sessions.sort(
|
||||
(a, b) =>
|
||||
new Date(b.lastUpdate).getTime() - new Date(a.lastUpdate).getTime(),
|
||||
);
|
||||
|
||||
const deletedSessionIds = {
|
||||
...remoteDeletedSessionIds,
|
||||
...localDeletedSessionIds,
|
||||
};
|
||||
removeOutdatedEntries(deletedSessionIds);
|
||||
localState.deletedSessionIds = deletedSessionIds;
|
||||
|
||||
localState.currentSessionIndex = localState.sessions.findIndex(
|
||||
(session) => {
|
||||
return session && currentSession && session.id === currentSession.id;
|
||||
},
|
||||
);
|
||||
|
||||
return localState;
|
||||
},
|
||||
[StoreKey.Prompt]: (localState, remoteState) => {
|
||||
@@ -153,9 +226,9 @@ export function mergeWithUpdate<T extends { lastUpdateTime?: number }>(
|
||||
remoteState: T,
|
||||
) {
|
||||
const localUpdateTime = localState.lastUpdateTime ?? 0;
|
||||
const remoteUpdateTime = localState.lastUpdateTime ?? 1;
|
||||
const remoteUpdateTime = remoteState.lastUpdateTime ?? 1;
|
||||
|
||||
if (localUpdateTime < remoteUpdateTime) {
|
||||
if (localUpdateTime >= remoteUpdateTime) {
|
||||
merge(remoteState, localState);
|
||||
return { ...remoteState };
|
||||
} else {
|
||||
|
||||
10
package.json
10
package.json
@@ -6,13 +6,13 @@
|
||||
"mask": "npx tsx app/masks/build.ts",
|
||||
"mask:watch": "npx watch \"yarn mask\" app/masks",
|
||||
"dev": "concurrently -r \"yarn run mask:watch\" \"next dev\"",
|
||||
"build": "yarn test:ci && yarn mask && cross-env BUILD_MODE=standalone next build",
|
||||
"build": "yarn mask && cross-env BUILD_MODE=standalone next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"export": "yarn test:ci && yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build",
|
||||
"export": "yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build",
|
||||
"export:dev": "concurrently -r \"yarn mask:watch\" \"cross-env BUILD_MODE=export BUILD_APP=1 next dev\"",
|
||||
"app:dev": "concurrently -r \"yarn mask:watch\" \"yarn tauri dev\"",
|
||||
"app:build": "yarn test:ci && yarn mask && yarn tauri build",
|
||||
"app:build": "yarn mask && yarn tauri build",
|
||||
"prompts": "node ./scripts/fetch-prompts.mjs",
|
||||
"prepare": "husky install",
|
||||
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev",
|
||||
@@ -58,7 +58,7 @@
|
||||
"@tauri-apps/cli": "1.5.11",
|
||||
"@testing-library/jest-dom": "^6.4.8",
|
||||
"@testing-library/react": "^16.0.0",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.11.30",
|
||||
@@ -88,4 +88,4 @@
|
||||
"lint-staged/yaml": "^2.2.2"
|
||||
},
|
||||
"packageManager": "yarn@1.22.19"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,11 +119,22 @@ pub async fn stream_fetch(
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
println!("Error response: {:?}", err.source().expect("REASON").to_string());
|
||||
let error: String = err.source()
|
||||
.map(|e| e.to_string())
|
||||
.unwrap_or_else(|| "Unknown error occurred".to_string());
|
||||
println!("Error response: {:?}", error);
|
||||
tauri::async_runtime::spawn( async move {
|
||||
if let Err(e) = window.emit(event_name, ChunkPayload{ request_id, chunk: error.into() }) {
|
||||
println!("Failed to emit chunk payload: {:?}", e);
|
||||
}
|
||||
if let Err(e) = window.emit(event_name, EndPayload{ request_id, status: 0 }) {
|
||||
println!("Failed to emit end payload: {:?}", e);
|
||||
}
|
||||
});
|
||||
StreamResponse {
|
||||
request_id,
|
||||
status: 599,
|
||||
status_text: err.source().expect("REASON").to_string(),
|
||||
status_text: "Error".to_string(),
|
||||
headers: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "NextChat",
|
||||
"version": "2.15.4"
|
||||
"version": "2.15.6"
|
||||
},
|
||||
"tauri": {
|
||||
"allowlist": {
|
||||
@@ -99,7 +99,7 @@
|
||||
"endpoints": [
|
||||
"https://github.com/ChatGPTNextWeb/ChatGPT-Next-Web/releases/latest/download/latest.json"
|
||||
],
|
||||
"dialog": false,
|
||||
"dialog": true,
|
||||
"windows": {
|
||||
"installMode": "passive"
|
||||
},
|
||||
|
||||
@@ -2263,10 +2263,10 @@
|
||||
dependencies:
|
||||
"@types/istanbul-lib-report" "*"
|
||||
|
||||
"@types/jest@^29.5.12":
|
||||
version "29.5.12"
|
||||
resolved "https://registry.npmmirror.com/@types/jest/-/jest-29.5.12.tgz#7f7dc6eb4cf246d2474ed78744b05d06ce025544"
|
||||
integrity sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==
|
||||
"@types/jest@^29.5.13":
|
||||
version "29.5.13"
|
||||
resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.5.13.tgz#8bc571659f401e6a719a7bf0dbcb8b78c71a8adc"
|
||||
integrity sha512-wd+MVEZCHt23V0/L642O5APvspWply/rGY5BcW4SUETo2UzPU3Z26qr8jC2qxpimI2jjx9h7+2cj2FwIr01bXg==
|
||||
dependencies:
|
||||
expect "^29.0.0"
|
||||
pretty-format "^29.0.0"
|
||||
|
||||
Reference in New Issue
Block a user