Compare commits

...

45 Commits

Author SHA1 Message Date
lyf
ebe617b733 fix max_completions_tokens 2024-10-16 16:24:02 +08:00
Lloyd Zhou
c139038e01 Merge pull request #5639 from code-october/fix/auth-ui
优化访问码输入框
2024-10-11 19:11:35 +08:00
code-october
4a7fd3a380 优化首页 api 输入框 2024-10-11 10:36:11 +00:00
code-october
c98dc31cdf 优化访问码输入框 2024-10-11 09:03:20 +00:00
Lloyd Zhou
c5074f0aa4 Merge pull request #5581 from ConnectAI-E/feature/gemini-functioncall
google gemini support function call
2024-10-10 21:02:36 +08:00
Lloyd Zhou
ba58018a15 Merge pull request #5211 from ConnectAI-E/feature/jest
feat: jest
2024-10-10 21:02:05 +08:00
Lloyd Zhou
63ab83c3c8 Merge pull request #5621 from ConnectAI-E/hotfix/plugin-result
hotfix plugin result is not string #5614
2024-10-10 12:48:55 +08:00
lloydzhou
268cf3b606 hotfix plugin result is not string #5614 2024-10-10 12:47:25 +08:00
Lloyd Zhou
fbc68fa776 Merge pull request #5602 from PeterDaveHello/ImproveTwLocale
i18n: improve tw Traditional Chinese locale
2024-10-09 19:38:06 +08:00
lloydzhou
4ae34ea3ee merge main 2024-10-09 18:27:23 +08:00
Lloyd Zhou
96273fd75e Merge pull request #5611 from ConnectAI-E/feature/tauri-fetch-update
make sure get request_id before body chunk
2024-10-09 16:18:37 +08:00
lloydzhou
3e63d405c1 update 2024-10-09 16:12:01 +08:00
Lloyd Zhou
19b42aac5d Merge pull request #5608 from ConnectAI-E/fix-readme
fix: [#5574] readme
2024-10-09 14:49:34 +08:00
Lloyd Zhou
b67a23200e Merge pull request #5610 from ChatGPTNextWeb/lloydzhou-patch-1
Update README.md
2024-10-09 14:48:55 +08:00
Lloyd Zhou
1dac02e4d6 Update README.md 2024-10-09 14:48:43 +08:00
Lloyd Zhou
acad5b1d08 Merge pull request #5609 from ElricLiu/main
Update README.md
2024-10-09 14:45:27 +08:00
ElricLiu
4e9bb51d2f Update README.md 2024-10-09 14:43:49 +08:00
DDMeaqua
c0c8cdbbf3 fix: [#5574] 文档错误 2024-10-09 14:36:58 +08:00
Lloyd Zhou
cbdc611b54 Merge pull request #5607 from ConnectAI-E/hotfix/summarize-model
fix compressModel, related #5426, fix #5606 #5603 #5575
2024-10-09 14:08:13 +08:00
lloydzhou
93ca303b6c fix ts error 2024-10-09 13:49:33 +08:00
lloydzhou
a925b424a8 fix compressModel, related #5426, fix #5606 #5603 #5575 2024-10-09 13:42:25 +08:00
Lloyd Zhou
5b4d423b58 Merge pull request #5565 from ConnectAI-E/feature/using-tauri-fetch
Feat: using tauri fetch api in App
2024-10-09 13:03:01 +08:00
lloydzhou
6c1cbe120c update 2024-10-09 11:46:49 +08:00
Peter Dave Hello
77a58bc4b0 i18n: improve tw Traditional Chinese locale 2024-10-09 03:14:38 +08:00
Dogtiti
8ad63a6c25 Merge pull request #5586 from little-huang/patch-1
fix: correct typo in variable name from ALLOWD_PATH to ALLOWED_PATH
2024-10-08 15:26:41 +08:00
Dogtiti
acf9fa36f9 Merge branch 'main' of https://github.com/ConnectAI-E/ChatGPT-Next-Web into feature/jest 2024-10-08 10:30:47 +08:00
Dogtiti
461154bb03 fix: format package 2024-10-08 10:29:42 +08:00
little_huang
cd75461f9e fix: correct typo in variable name from ALLOWD_PATH to ALLOWED_PATH 2024-10-07 10:30:25 +08:00
Dogtiti
2bac174e6f Merge pull request #4393 from ChatGPTNextWeb/dean-delete-escapeDollarNumber
bugfix: Delete the escapeDollarNumber function, which causes errors i…
2024-10-06 12:41:03 +08:00
Lloyd Zhou
65f80f81ad Merge branch 'main' into dean-delete-escapeDollarNumber 2024-10-04 14:31:00 +08:00
lloydzhou
450766a44b google gemini support function call 2024-10-03 20:28:15 +08:00
Lloyd Zhou
05e6e4bffb Merge pull request #5578 from code-october/fix/safe-equal
use safe equal operation
2024-10-03 10:59:32 +08:00
code-october
fbb66a4a5d use safe equal operation 2024-10-03 02:08:10 +00:00
lloydzhou
d51d31a559 update 2024-10-01 14:40:23 +08:00
lloydzhou
919ee51dca hover show errorMsg when plugin run error 2024-10-01 13:58:50 +08:00
lloydzhou
9c577ad9d5 hotfix for plugin runtime 2024-10-01 12:55:57 +08:00
lloydzhou
953114041b add connect timeout 2024-10-01 12:02:29 +08:00
Lloyd Zhou
cea5b91f96 Merge pull request #5567 from ChatGPTNextWeb/fix-readme
update  readme
2024-09-30 13:31:34 +08:00
lyf
d2984db6e7 fix readme 2024-09-30 13:28:14 +08:00
lyf
deb215ccd1 fix readme 2024-09-30 13:23:24 +08:00
Lloyd Zhou
0c697e123d Merge pull request #5564 from code-october/fix/html-code
fix quoteEnd extract regex
2024-09-30 13:06:52 +08:00
code-october
f5ad51a35e fix quoteEnd extract regex 2024-09-29 14:29:42 +00:00
Dogtiti
1287e39cc6 feat: run test before build 2024-08-06 19:24:47 +08:00
Dogtiti
1ef2aa35e9 feat: jest 2024-08-06 18:03:27 +08:00
butterfly
4d6b981a54 bugfix: Delete the escapeDollarNumber function, which causes errors in rendering a latex string 2024-03-26 11:43:55 +08:00
23 changed files with 2246 additions and 272 deletions

View File

@@ -18,11 +18,11 @@ One-Click to get a well-designed cross-platform ChatGPT web UI, with GPT3, GPT4
[![MacOS][MacOS-image]][download-url] [![MacOS][MacOS-image]][download-url]
[![Linux][Linux-image]][download-url] [![Linux][Linux-image]][download-url]
[NextChatAI](https://nextchat.dev/chat) / [Web App](https://app.nextchat.dev) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Enterprise Edition](#enterprise-edition) / [Twitter](https://twitter.com/NextChatDev) [NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [Web App](https://app.nextchat.dev) / [Desktop App](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [Discord](https://discord.gg/YCkeafCafC) / [Enterprise Edition](#enterprise-edition) / [Twitter](https://twitter.com/NextChatDev)
[NextChatAI](https://nextchat.dev/chat) / [网页版](https://app.nextchat.dev) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) [NextChatAI](https://nextchat.dev/chat) / [网页版](https://app.nextchat.dev) / [客户端](https://github.com/Yidadaa/ChatGPT-Next-Web/releases) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [反馈](https://github.com/Yidadaa/ChatGPT-Next-Web/issues)
[saas-url]: https://nextchat.dev/chat [saas-url]: https://nextchat.dev/chat?utm_source=readme
[saas-image]: https://img.shields.io/badge/NextChat-Saas-green?logo=microsoftedge [saas-image]: https://img.shields.io/badge/NextChat-Saas-green?logo=microsoftedge
[web-url]: https://app.nextchat.dev/ [web-url]: https://app.nextchat.dev/
[download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases [download-url]: https://github.com/Yidadaa/ChatGPT-Next-Web/releases
@@ -63,7 +63,7 @@ For enterprise inquiries, please contact: **business@nextchat.dev**
企业版咨询: **business@nextchat.dev** 企业版咨询: **business@nextchat.dev**
<img width="300" src="https://github.com/user-attachments/assets/3daeb7b6-ab63-4542-9141-2e4a12c80601"> <img width="300" src="https://github.com/user-attachments/assets/3d4305ac-6e95-489e-884b-51d51db5f692">
## Features ## Features
@@ -334,9 +334,9 @@ To control custom models, use `+` to add a custom model, use `-` to hide a model
User `-all` to disable all default models, `+all` to enable all default models. User `-all` to disable all default models, `+all` to enable all default models.
For Azure: use `modelName@azure=deploymentName` to customize model name and deployment name. For Azure: use `modelName@Azure=deploymentName` to customize model name and deployment name.
> Example: `+gpt-3.5-turbo@azure=gpt35` will show option `gpt35(Azure)` in model list. > Example: `+gpt-3.5-turbo@Azure=gpt35` will show option `gpt35(Azure)` in model list.
> If you only can use Azure model, `-all,+gpt-3.5-turbo@azure=gpt35` will `gpt35(Azure)` the only option in model list. > If you only can use Azure model, `-all,+gpt-3.5-turbo@Azure=gpt35` will `gpt35(Azure)` the only option in model list.
For ByteDance: use `modelName@bytedance=deploymentName` to customize model name and deployment name. For ByteDance: use `modelName@bytedance=deploymentName` to customize model name and deployment name.
> Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list. > Example: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` will show option `Doubao-lite-4k(ByteDance)` in model list.

View File

@@ -8,7 +8,7 @@
一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。 一键免费部署你的私人 ChatGPT 网页应用,支持 GPT3, GPT4 & Gemini Pro 模型。
[NextChatAI](https://nextchat.dev/chat) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N) [NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [企业版](#%E4%BC%81%E4%B8%9A%E7%89%88) / [演示 Demo](https://chat-gpt-next-web.vercel.app/) / [反馈 Issues](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [加入 Discord](https://discord.gg/zrhvHCr79N)
[<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)
@@ -216,9 +216,9 @@ ByteDance Api Url.
用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。 用来控制模型列表,使用 `+` 增加一个模型,使用 `-` 来隐藏一个模型,使用 `模型名=展示名` 来自定义模型的展示名,用英文逗号隔开。
在Azure的模式下支持使用`modelName@azure=deploymentName`的方式配置模型名称和部署名称(deploy-name) 在Azure的模式下支持使用`modelName@Azure=deploymentName`的方式配置模型名称和部署名称(deploy-name)
> 示例:`+gpt-3.5-turbo@azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。 > 示例:`+gpt-3.5-turbo@Azure=gpt35`这个配置会在模型列表显示一个`gpt35(Azure)`的选项。
> 如果你只能使用Azure模式那么设置 `-all,+gpt-3.5-turbo@azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)` > 如果你只能使用Azure模式那么设置 `-all,+gpt-3.5-turbo@Azure=gpt35` 则可以让对话的默认使用 `gpt35(Azure)`
在ByteDance的模式下支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name) 在ByteDance的模式下支持使用`modelName@bytedance=deploymentName`的方式配置模型名称和部署名称(deploy-name)
> 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项 > 示例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx`这个配置会在模型列表显示一个`Doubao-lite-4k(ByteDance)`的选项

View File

@@ -5,7 +5,7 @@
ワンクリックで無料であなた専用の ChatGPT ウェブアプリをデプロイ。GPT3、GPT4 & Gemini Pro モデルをサポート。 ワンクリックで無料であなた専用の ChatGPT ウェブアプリをデプロイ。GPT3、GPT4 & Gemini Pro モデルをサポート。
[NextChatAI](https://nextchat.dev/chat) / [企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N) [NextChatAI](https://nextchat.dev/chat?utm_source=readme) / [企業版](#企業版) / [デモ](https://chat-gpt-next-web.vercel.app/) / [フィードバック](https://github.com/Yidadaa/ChatGPT-Next-Web/issues) / [Discordに参加](https://discord.gg/zrhvHCr79N)
[<img src="https://vercel.com/button" alt="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="Zeaburでデプロイ" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Gitpodで開く" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web) [<img src="https://vercel.com/button" alt="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="Zeaburでデプロイ" height="30">](https://zeabur.com/templates/ZBUEFA) [<img src="https://gitpod.io/button/open-in-gitpod.svg" alt="Gitpodで開く" height="30">](https://gitpod.io/#https://github.com/Yidadaa/ChatGPT-Next-Web)
@@ -207,8 +207,8 @@ ByteDance API の URL。
モデルリストを管理します。`+` でモデルを追加し、`-` でモデルを非表示にし、`モデル名=表示名` でモデルの表示名をカスタマイズし、カンマで区切ります。 モデルリストを管理します。`+` でモデルを追加し、`-` でモデルを非表示にし、`モデル名=表示名` でモデルの表示名をカスタマイズし、カンマで区切ります。
Azure モードでは、`modelName@azure=deploymentName` 形式でモデル名とデプロイ名deploy-nameを設定できます。 Azure モードでは、`modelName@Azure=deploymentName` 形式でモデル名とデプロイ名deploy-nameを設定できます。
> 例:`+gpt-3.5-turbo@azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。 > 例:`+gpt-3.5-turbo@Azure=gpt35` この設定でモデルリストに `gpt35(Azure)` のオプションが表示されます。
ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデル名とデプロイ名deploy-nameを設定できます。 ByteDance モードでは、`modelName@bytedance=deploymentName` 形式でモデル名とデプロイ名deploy-nameを設定できます。
> 例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` この設定でモデルリストに `Doubao-lite-4k(ByteDance)` のオプションが表示されます。 > 例: `+Doubao-lite-4k@bytedance=ep-xxxxx-xxx` この設定でモデルリストに `Doubao-lite-4k(ByteDance)` のオプションが表示されます。

View File

@@ -6,7 +6,7 @@ import { NextRequest, NextResponse } from "next/server";
import { auth } from "./auth"; import { auth } from "./auth";
import { requestOpenai } from "./common"; import { requestOpenai } from "./common";
const ALLOWD_PATH = new Set(Object.values(OpenaiPath)); const ALLOWED_PATH = new Set(Object.values(OpenaiPath));
function getModels(remoteModelRes: OpenAIListModelResponse) { function getModels(remoteModelRes: OpenAIListModelResponse) {
const config = getServerSideConfig(); const config = getServerSideConfig();
@@ -34,7 +34,7 @@ export async function handle(
const subpath = params.path.join("/"); const subpath = params.path.join("/");
if (!ALLOWD_PATH.has(subpath)) { if (!ALLOWED_PATH.has(subpath)) {
console.log("[OpenAI Route] forbidden path ", subpath); console.log("[OpenAI Route] forbidden path ", subpath);
return NextResponse.json( return NextResponse.json(
{ {

View File

@@ -231,7 +231,7 @@ export function getHeaders(ignoreHeaders: boolean = false) {
function getConfig() { function getConfig() {
const modelConfig = chatStore.currentSession().mask.modelConfig; const modelConfig = chatStore.currentSession().mask.modelConfig;
const isGoogle = modelConfig.providerName == ServiceProvider.Google; const isGoogle = modelConfig.providerName === ServiceProvider.Google;
const isAzure = modelConfig.providerName === ServiceProvider.Azure; const isAzure = modelConfig.providerName === ServiceProvider.Azure;
const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic; const isAnthropic = modelConfig.providerName === ServiceProvider.Anthropic;
const isBaidu = modelConfig.providerName == ServiceProvider.Baidu; const isBaidu = modelConfig.providerName == ServiceProvider.Baidu;

View File

@@ -7,21 +7,25 @@ import {
LLMUsage, LLMUsage,
SpeechOptions, SpeechOptions,
} from "../api"; } from "../api";
import { useAccessStore, useAppConfig, useChatStore } from "@/app/store"; import {
useAccessStore,
useAppConfig,
useChatStore,
usePluginStore,
ChatMessageTool,
} from "@/app/store";
import { stream } from "@/app/utils/chat";
import { getClientConfig } from "@/app/config/client"; import { getClientConfig } from "@/app/config/client";
import { GEMINI_BASE_URL } from "@/app/constant"; import { GEMINI_BASE_URL } from "@/app/constant";
import Locale from "../../locales";
import {
EventStreamContentType,
fetchEventSource,
} from "@fortaine/fetch-event-source";
import { prettyObject } from "@/app/utils/format";
import { import {
getMessageTextContent, getMessageTextContent,
getMessageImages, getMessageImages,
isVisionModel, isVisionModel,
} from "@/app/utils"; } from "@/app/utils";
import { preProcessImageContent } from "@/app/utils/chat"; import { preProcessImageContent } from "@/app/utils/chat";
import { nanoid } from "nanoid";
import { RequestPayload } from "./openai";
import { fetch } from "@/app/utils/stream"; import { fetch } from "@/app/utils/stream";
export class GeminiProApi implements LLMApi { export class GeminiProApi implements LLMApi {
@@ -178,115 +182,81 @@ export class GeminiProApi implements LLMApi {
); );
if (shouldStream) { if (shouldStream) {
let responseText = ""; const [tools, funcs] = usePluginStore
let remainText = ""; .getState()
let finished = false; .getAsTools(
useChatStore.getState().currentSession().mask?.plugin || [],
);
return stream(
chatPath,
requestPayload,
getHeaders(),
// @ts-ignore
[{ functionDeclarations: tools.map((tool) => tool.function) }],
funcs,
controller,
// parseSSE
(text: string, runTools: ChatMessageTool[]) => {
// console.log("parseSSE", text, runTools);
const chunkJson = JSON.parse(text);
const finish = () => { const functionCall = chunkJson?.candidates
if (!finished) { ?.at(0)
finished = true; ?.content.parts.at(0)?.functionCall;
options.onFinish(responseText + remainText); if (functionCall) {
} const { name, args } = functionCall;
}; runTools.push({
id: nanoid(),
// animate response to make it looks smooth type: "function",
function animateResponseText() { function: {
if (finished || controller.signal.aborted) { name,
responseText += remainText; arguments: JSON.stringify(args), // utils.chat call function, using JSON.parse
finish(); },
return; });
} }
return chunkJson?.candidates?.at(0)?.content.parts.at(0)?.text;
if (remainText.length > 0) { },
const fetchCount = Math.max(1, Math.round(remainText.length / 60)); // processToolMessage, include tool_calls message and tool call results
const fetchText = remainText.slice(0, fetchCount); (
responseText += fetchText; requestPayload: RequestPayload,
remainText = remainText.slice(fetchCount); toolCallMessage: any,
options.onUpdate?.(responseText, fetchText); toolCallResult: any[],
} ) => {
// @ts-ignore
requestAnimationFrame(animateResponseText); requestPayload?.contents?.splice(
} // @ts-ignore
requestPayload?.contents?.length,
// start animaion 0,
animateResponseText(); {
role: "model",
controller.signal.onabort = finish; parts: toolCallMessage.tool_calls.map(
(tool: ChatMessageTool) => ({
fetchEventSource(chatPath, { functionCall: {
fetch: fetch as any, name: tool?.function?.name,
...chatPayload, args: JSON.parse(tool?.function?.arguments as string),
async onopen(res) { },
clearTimeout(requestTimeoutId); }),
const contentType = res.headers.get("content-type"); ),
console.log( },
"[Gemini] request response content type: ", // @ts-ignore
contentType, ...toolCallResult.map((result) => ({
role: "function",
parts: [
{
functionResponse: {
name: result.name,
response: {
name: result.name,
content: result.content, // TODO just text content...
},
},
},
],
})),
); );
if (contentType?.startsWith("text/plain")) {
responseText = await res.clone().text();
return finish();
}
if (
!res.ok ||
!res.headers
.get("content-type")
?.startsWith(EventStreamContentType) ||
res.status !== 200
) {
const responseTexts = [responseText];
let extraInfo = await res.clone().text();
try {
const resJson = await res.clone().json();
extraInfo = prettyObject(resJson);
} catch {}
if (res.status === 401) {
responseTexts.push(Locale.Error.Unauthorized);
}
if (extraInfo) {
responseTexts.push(extraInfo);
}
responseText = responseTexts.join("\n\n");
return finish();
}
}, },
onmessage(msg) { options,
if (msg.data === "[DONE]" || finished) { );
return finish();
}
const text = msg.data;
try {
const json = JSON.parse(text);
const delta = apiClient.extractMessage(json);
if (delta) {
remainText += delta;
}
const blockReason = json?.promptFeedback?.blockReason;
if (blockReason) {
// being blocked
console.log(`[Google] [Safety Ratings] result:`, blockReason);
}
} catch (e) {
console.error("[Request] parse error", text, msg);
}
},
onclose() {
finish();
},
onerror(e) {
options.onError?.(e);
throw e;
},
openWhenHidden: true,
});
} else { } else {
const res = await fetch(chatPath, chatPayload); const res = await fetch(chatPath, chatPayload);
clearTimeout(requestTimeoutId); clearTimeout(requestTimeoutId);

View File

@@ -63,7 +63,7 @@ export interface RequestPayload {
presence_penalty: number; presence_penalty: number;
frequency_penalty: number; frequency_penalty: number;
top_p: number; top_p: number;
max_tokens?: number; max_completions_tokens?: number;
} }
export interface DalleRequestPayload { export interface DalleRequestPayload {
@@ -228,13 +228,16 @@ export class ChatGPTApi implements LLMApi {
presence_penalty: !isO1 ? modelConfig.presence_penalty : 0, presence_penalty: !isO1 ? modelConfig.presence_penalty : 0,
frequency_penalty: !isO1 ? modelConfig.frequency_penalty : 0, frequency_penalty: !isO1 ? modelConfig.frequency_penalty : 0,
top_p: !isO1 ? modelConfig.top_p : 1, top_p: !isO1 ? modelConfig.top_p : 1,
// max_tokens: Math.max(modelConfig.max_tokens, 1024), // max_completions_tokens: Math.max(modelConfig.max_completions_tokens, 1024),
// Please do not ask me why not send max_tokens, no reason, this param is just shit, I dont want to explain anymore. // Please do not ask me why not send max_completions_tokens, no reason, this param is just shit, I dont want to explain anymore.
}; };
// add max_tokens to vision model // add max_completions_tokens to vision model
if (visionModel) { if (visionModel) {
requestPayload["max_tokens"] = Math.max(modelConfig.max_tokens, 4000); requestPayload["max_completions_tokens"] = Math.max(
modelConfig.max_completions_tokens,
4000,
);
} }
} }

View File

@@ -11,6 +11,7 @@ import Logo from "../icons/logo.svg";
import { useMobileScreen } from "@/app/utils"; import { useMobileScreen } from "@/app/utils";
import BotIcon from "../icons/bot.svg"; import BotIcon from "../icons/bot.svg";
import { getClientConfig } from "../config/client"; import { getClientConfig } from "../config/client";
import { PasswordInput } from "./ui-lib";
import LeftIcon from "@/app/icons/left.svg"; import LeftIcon from "@/app/icons/left.svg";
import { safeLocalStorage } from "@/app/utils"; import { safeLocalStorage } from "@/app/utils";
import { import {
@@ -60,36 +61,43 @@ export function AuthPage() {
<div className={styles["auth-title"]}>{Locale.Auth.Title}</div> <div className={styles["auth-title"]}>{Locale.Auth.Title}</div>
<div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div> <div className={styles["auth-tips"]}>{Locale.Auth.Tips}</div>
<input <PasswordInput
className={styles["auth-input"]} style={{ marginTop: "3vh", marginBottom: "3vh" }}
type="password" aria={Locale.Settings.ShowPassword}
placeholder={Locale.Auth.Input} aria-label={Locale.Auth.Input}
value={accessStore.accessCode} value={accessStore.accessCode}
type="text"
placeholder={Locale.Auth.Input}
onChange={(e) => { onChange={(e) => {
accessStore.update( accessStore.update(
(access) => (access.accessCode = e.currentTarget.value), (access) => (access.accessCode = e.currentTarget.value),
); );
}} }}
/> />
{!accessStore.hideUserApiKey ? ( {!accessStore.hideUserApiKey ? (
<> <>
<div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div> <div className={styles["auth-tips"]}>{Locale.Auth.SubTips}</div>
<input <PasswordInput
className={styles["auth-input"]} style={{ marginTop: "3vh", marginBottom: "3vh" }}
type="password" aria={Locale.Settings.ShowPassword}
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder} aria-label={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
value={accessStore.openaiApiKey} value={accessStore.openaiApiKey}
type="text"
placeholder={Locale.Settings.Access.OpenAI.ApiKey.Placeholder}
onChange={(e) => { onChange={(e) => {
accessStore.update( accessStore.update(
(access) => (access.openaiApiKey = e.currentTarget.value), (access) => (access.openaiApiKey = e.currentTarget.value),
); );
}} }}
/> />
<input <PasswordInput
className={styles["auth-input-second"]} style={{ marginTop: "3vh", marginBottom: "3vh" }}
type="password" aria={Locale.Settings.ShowPassword}
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder} aria-label={Locale.Settings.Access.Google.ApiKey.Placeholder}
value={accessStore.googleApiKey} value={accessStore.googleApiKey}
type="text"
placeholder={Locale.Settings.Access.Google.ApiKey.Placeholder}
onChange={(e) => { onChange={(e) => {
accessStore.update( accessStore.update(
(access) => (access.googleApiKey = e.currentTarget.value), (access) => (access.googleApiKey = e.currentTarget.value),

View File

@@ -1815,6 +1815,7 @@ function _Chat() {
{message?.tools?.map((tool) => ( {message?.tools?.map((tool) => (
<div <div
key={tool.id} key={tool.id}
title={tool?.errorMsg}
className={styles["chat-message-tool"]} className={styles["chat-message-tool"]}
> >
{tool.isError === false ? ( {tool.isError === false ? (

View File

@@ -207,23 +207,6 @@ function CustomCode(props: { children: any; className?: string }) {
); );
} }
function escapeDollarNumber(text: string) {
let escapedText = "";
for (let i = 0; i < text.length; i += 1) {
let char = text[i];
const nextChar = text[i + 1] || " ";
if (char === "$" && nextChar >= "0" && nextChar <= "9") {
char = "\\$";
}
escapedText += char;
}
return escapedText;
}
function escapeBrackets(text: string) { function escapeBrackets(text: string) {
const pattern = const pattern =
/(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g; /(```[\s\S]*?```|`.*?`)|\\\[([\s\S]*?[^\\])\\\]|\\\((.*?)\\\)/g;
@@ -252,7 +235,7 @@ function tryWrapHtmlCode(text: string) {
}, },
) )
.replace( .replace(
/(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*?)([`]*?)([\n\r]*?)/g, /(<\/body>)([\r\n\s]*?)(<\/html>)([\n\r]*)([`]*)([\n\r]*?)/g,
(match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => { (match, bodyEnd, space, htmlEnd, newLine, quoteEnd) => {
return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match; return !quoteEnd ? bodyEnd + space + htmlEnd + "\n```\n" : match;
}, },
@@ -261,7 +244,7 @@ function tryWrapHtmlCode(text: string) {
function _MarkDownContent(props: { content: string }) { function _MarkDownContent(props: { content: string }) {
const escapedContent = useMemo(() => { const escapedContent = useMemo(() => {
return tryWrapHtmlCode(escapeBrackets(escapeDollarNumber(props.content))); return tryWrapHtmlCode(escapeBrackets(props.content));
}, [props.content]); }, [props.content]);
return ( return (

View File

@@ -8,12 +8,12 @@ const tw = {
Error: { Error: {
Unauthorized: isApp Unauthorized: isApp
? `😆 對話遇到了一些問題,不用慌: ? `😆 對話遇到了一些問題,不用慌:
\\ 1⃣ 想要零配置開箱即用,[點這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL}) \\ 1⃣ 想要無須設定開箱即用,[點這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
\\ 2⃣ 如果你想消耗自己的 OpenAI 資源,點[這裡](/#/settings)修改設定 ⚙️` \\ 2⃣ 如果你想消耗自己的 OpenAI 資源,點[這裡](/#/settings)修改設定 ⚙️`
: `😆 對話遇到了一些問題,不用慌: : `😆 對話遇到了一些問題,不用慌:
\ 1⃣ 想要零配置開箱即用,[點這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL}) \ 1⃣ 想要無須設定開箱即用,[點這裡立刻開啟對話 🚀](${SAAS_CHAT_UTM_URL})
\ 2⃣ 如果你正在使用私有部署版本,點[這裡](/#/auth)輸入訪問秘鑰 🔑 \ 2⃣ 如果你正在使用私有部署版本,點[這裡](/#/auth)輸入存取金鑰 🔑
\ 3⃣ 如果你想消耗自己的 OpenAI 資源,點[這裡](/#/settings)修改設定 ⚙️ \ 3⃣ 如果你想消耗自己的 OpenAI 資源,點[這裡](/#/settings)修改設定 ⚙️
`, `,
}, },
@@ -25,9 +25,9 @@ const tw = {
Confirm: "確認", Confirm: "確認",
Later: "稍候再說", Later: "稍候再說",
Return: "返回", Return: "返回",
SaasTips: "配置太麻煩,想要立即使用", SaasTips: "設定太麻煩,想要立即使用",
TopTips: TopTips:
"🥳 NextChat AI 首發優惠,立刻解鎖 OpenAI o1, GPT-4o, Claude-3.5 等最新模型", "🥳 NextChat AI 首發優惠,立刻解鎖 OpenAI o1, GPT-4o, Claude-3.5 等最新的大型語言模型",
}, },
ChatItem: { ChatItem: {
ChatItemCount: (count: number) => `${count} 則對話`, ChatItemCount: (count: number) => `${count} 則對話`,
@@ -53,8 +53,8 @@ const tw = {
PinToastAction: "檢視", PinToastAction: "檢視",
Delete: "刪除", Delete: "刪除",
Edit: "編輯", Edit: "編輯",
RefreshTitle: "刷新標題", RefreshTitle: "重新整理標題",
RefreshToast: "已發送刷新標題請求", RefreshToast: "已傳送重新整理標題請求",
}, },
Commands: { Commands: {
new: "新建聊天", new: "新建聊天",
@@ -95,10 +95,10 @@ const tw = {
IsContext: "預設提示詞", IsContext: "預設提示詞",
ShortcutKey: { ShortcutKey: {
Title: "鍵盤快捷方式", Title: "鍵盤快捷方式",
newChat: "開新聊天", newChat: "開新聊天",
focusInput: "聚焦輸入框", focusInput: "聚焦輸入框",
copyLastMessage: "複製最後一個回覆", copyLastMessage: "複製最後一個回覆",
copyLastCode: "複製最後一個代碼塊", copyLastCode: "複製最後一個程式碼區塊",
showShortcutKey: "顯示快捷方式", showShortcutKey: "顯示快捷方式",
}, },
}, },
@@ -174,9 +174,9 @@ const tw = {
SubTitle: "聊天內容的字型大小", SubTitle: "聊天內容的字型大小",
}, },
FontFamily: { FontFamily: {
Title: "聊天字", Title: "聊天字",
SubTitle: "聊天內容的字,若空則用全局默認字體", SubTitle: "聊天內容的字,若空則用全域預設字型",
Placeholder: "字名稱", Placeholder: "字名稱",
}, },
InjectSystemPrompts: { InjectSystemPrompts: {
Title: "匯入系統提示", Title: "匯入系統提示",
@@ -301,8 +301,8 @@ const tw = {
Title: "使用 NextChat AI", Title: "使用 NextChat AI",
Label: "(性價比最高的方案)", Label: "(性價比最高的方案)",
SubTitle: SubTitle:
"由 NextChat 官方維護,零配置開箱即用,支 OpenAI o1、GPT-4o、Claude-3.5 等最新模型", "由 NextChat 官方維護,無須設定開箱即用,支 OpenAI o1、GPT-4o、Claude-3.5 等最新的大型語言模型",
ChatNow: "立刻對話", ChatNow: "立刻開始對話",
}, },
AccessCode: { AccessCode: {
@@ -485,18 +485,18 @@ const tw = {
}, },
}, },
SearchChat: { SearchChat: {
Name: "搜", Name: "搜",
Page: { Page: {
Title: "搜聊天記錄", Title: "搜聊天記錄",
Search: "輸入搜關鍵詞", Search: "輸入搜關鍵詞",
NoResult: "沒有找到結果", NoResult: "沒有找到結果",
NoData: "沒有數據", NoData: "沒有資料",
Loading: "載中", Loading: "載中",
SubTitle: (count: number) => `找到 ${count} 條結果`, SubTitle: (count: number) => `找到 ${count} 條結果`,
}, },
Item: { Item: {
View: "查看", View: "檢視",
}, },
}, },
NewChat: { NewChat: {

View File

@@ -16,6 +16,9 @@ import {
DEFAULT_SYSTEM_TEMPLATE, DEFAULT_SYSTEM_TEMPLATE,
KnowledgeCutOffDate, KnowledgeCutOffDate,
StoreKey, StoreKey,
SUMMARIZE_MODEL,
GEMINI_SUMMARIZE_MODEL,
ServiceProvider,
} from "../constant"; } from "../constant";
import Locale, { getLang } from "../locales"; import Locale, { getLang } from "../locales";
import { isDalle3, safeLocalStorage } from "../utils"; import { isDalle3, safeLocalStorage } from "../utils";
@@ -23,6 +26,8 @@ import { prettyObject } from "../utils/format";
import { createPersistStore } from "../utils/store"; import { createPersistStore } from "../utils/store";
import { estimateTokenLength } from "../utils/token"; import { estimateTokenLength } from "../utils/token";
import { ModelConfig, ModelType, useAppConfig } from "./config"; import { ModelConfig, ModelType, useAppConfig } from "./config";
import { useAccessStore } from "./access";
import { collectModelsWithDefaultModel } from "../utils/model";
import { createEmptyMask, Mask } from "./mask"; import { createEmptyMask, Mask } from "./mask";
const localStorage = safeLocalStorage(); const localStorage = safeLocalStorage();
@@ -37,6 +42,7 @@ export type ChatMessageTool = {
}; };
content?: string; content?: string;
isError?: boolean; isError?: boolean;
errorMsg?: string;
}; };
export type ChatMessage = RequestMessage & { export type ChatMessage = RequestMessage & {
@@ -102,6 +108,35 @@ function createEmptySession(): ChatSession {
}; };
} }
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];
}
return [currentModel, providerName];
}
function countMessages(msgs: ChatMessage[]) { function countMessages(msgs: ChatMessage[]) {
return msgs.reduce( return msgs.reduce(
(pre, cur) => pre + estimateTokenLength(getMessageTextContent(cur)), (pre, cur) => pre + estimateTokenLength(getMessageTextContent(cur)),
@@ -578,8 +613,14 @@ export const useChatStore = createPersistStore(
return; return;
} }
const providerName = modelConfig.compressProviderName; // if not config compressModel, then using getSummarizeModel
const api: ClientApi = getClientApi(providerName); 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 // remove error messages if any
const messages = session.messages; const messages = session.messages;
@@ -610,7 +651,7 @@ export const useChatStore = createPersistStore(
api.llm.chat({ api.llm.chat({
messages: topicMessages, messages: topicMessages,
config: { config: {
model: modelConfig.compressModel, model,
stream: false, stream: false,
providerName, providerName,
}, },
@@ -674,7 +715,8 @@ export const useChatStore = createPersistStore(
config: { config: {
...modelcfg, ...modelcfg,
stream: true, stream: true,
model: modelConfig.compressModel, model,
providerName,
}, },
onUpdate(message) { onUpdate(message) {
session.memoryPrompt = message; session.memoryPrompt = message;
@@ -727,7 +769,7 @@ export const useChatStore = createPersistStore(
}, },
{ {
name: StoreKey.Chat, name: StoreKey.Chat,
version: 3.2, version: 3.3,
migrate(persistedState, version) { migrate(persistedState, version) {
const state = persistedState as any; const state = persistedState as any;
const newState = JSON.parse( const newState = JSON.parse(
@@ -783,6 +825,14 @@ export const useChatStore = createPersistStore(
config.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; return newState as any;
}, },

View File

@@ -65,14 +65,14 @@ export const DEFAULT_CONFIG = {
providerName: "OpenAI" as ServiceProvider, providerName: "OpenAI" as ServiceProvider,
temperature: 0.5, temperature: 0.5,
top_p: 1, top_p: 1,
max_tokens: 4000, max_completions_tokens: 4000,
presence_penalty: 0, presence_penalty: 0,
frequency_penalty: 0, frequency_penalty: 0,
sendMemory: true, sendMemory: true,
historyMessageCount: 4, historyMessageCount: 4,
compressMessageLengthThreshold: 1000, compressMessageLengthThreshold: 1000,
compressModel: "gpt-4o-mini" as ModelType, compressModel: "",
compressProviderName: "OpenAI" as ServiceProvider, compressProviderName: "",
enableInjectSystemPrompts: true, enableInjectSystemPrompts: true,
template: config?.template ?? DEFAULT_INPUT_TEMPLATE, template: config?.template ?? DEFAULT_INPUT_TEMPLATE,
size: "1024x1024" as DalleSize, size: "1024x1024" as DalleSize,
@@ -127,7 +127,7 @@ export const ModalConfigValidator = {
model(x: string) { model(x: string) {
return x as ModelType; return x as ModelType;
}, },
max_tokens(x: number) { max_completions_tokens(x: number) {
return limitNumber(x, 0, 512000, 1024); return limitNumber(x, 0, 512000, 1024);
}, },
presence_penalty(x: number) { presence_penalty(x: number) {
@@ -178,7 +178,7 @@ export const useAppConfig = createPersistStore(
}), }),
{ {
name: StoreKey.Config, name: StoreKey.Config,
version: 4, version: 4.1,
merge(persistedState, currentState) { merge(persistedState, currentState) {
const state = persistedState as ChatConfig | undefined; const state = persistedState as ChatConfig | undefined;
@@ -231,7 +231,7 @@ export const useAppConfig = createPersistStore(
: config?.template ?? DEFAULT_INPUT_TEMPLATE; : config?.template ?? DEFAULT_INPUT_TEMPLATE;
} }
if (version < 4) { if (version < 4.1) {
state.modelConfig.compressModel = state.modelConfig.compressModel =
DEFAULT_CONFIG.modelConfig.compressModel; DEFAULT_CONFIG.modelConfig.compressModel;
state.modelConfig.compressProviderName = state.modelConfig.compressProviderName =

View File

@@ -151,7 +151,7 @@ export const usePromptStore = createPersistStore(
if (typeof window === "undefined") { if (typeof window === "undefined") {
return; return;
} }
const PROMPT_URL = "./prompts.json"; const PROMPT_URL = "./prompts.json";
type PromptList = Array<[string, string]>; type PromptList = Array<[string, string]>;

View File

@@ -285,6 +285,9 @@ export function showPlugins(provider: ServiceProvider, model: string) {
if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) { if (provider == ServiceProvider.Anthropic && !model.includes("claude-2")) {
return true; return true;
} }
if (provider == ServiceProvider.Google && !model.includes("vision")) {
return true;
}
return false; return false;
} }
@@ -293,36 +296,23 @@ export function fetch(
options?: Record<string, unknown>, options?: Record<string, unknown>,
): Promise<any> { ): Promise<any> {
if (window.__TAURI__) { if (window.__TAURI__) {
return tauriStreamFetch(url, { return tauriStreamFetch(url, options);
...options,
body: (options?.body || options?.data) as any,
});
// const payload = options?.body || options?.data;
// return tauriFetch(url, {
// ...options,
// body:
// payload &&
// ({
// type: "Text",
// payload,
// } as any),
// timeout: ((options?.timeout as number) || REQUEST_TIMEOUT_MS) / 1000,
// responseType:
// options?.responseType == "text" ? ResponseType.Text : ResponseType.JSON,
// } as any);
} }
return window.fetch(url, options); return window.fetch(url, options);
} }
export function adapter(config: Record<string, unknown>) { export function adapter(config: Record<string, unknown>) {
const { baseURL, url, params, ...rest } = config; const { baseURL, url, params, data: body, ...rest } = config;
const path = baseURL ? `${baseURL}${url}` : url; const path = baseURL ? `${baseURL}${url}` : url;
const fetchUrl = params const fetchUrl = params
? `${path}?${new URLSearchParams(params as any).toString()}` ? `${path}?${new URLSearchParams(params as any).toString()}`
: path; : path;
return fetch(fetchUrl as string, { ...rest, responseType: "text" }) return fetch(fetchUrl as string, { ...rest, body }).then((res) => {
.then((res) => res.text()) const { status, headers, statusText } = res;
.then((data) => ({ data })); return res
.text()
.then((data: string) => ({ status, statusText, headers, data }));
});
} }
export function safeLocalStorage(): { export function safeLocalStorage(): {

View File

@@ -222,7 +222,12 @@ export function stream(
), ),
) )
.then((res) => { .then((res) => {
const content = JSON.stringify(res.data); let content = res.data || res?.statusText;
// hotfix #5614
content =
typeof content === "string"
? content
: JSON.stringify(content);
if (res.status >= 300) { if (res.status >= 300) {
return Promise.reject(content); return Promise.reject(content);
} }
@@ -237,10 +242,15 @@ export function stream(
return content; return content;
}) })
.catch((e) => { .catch((e) => {
options?.onAfterTool?.({ ...tool, isError: true }); options?.onAfterTool?.({
...tool,
isError: true,
errorMsg: e.toString(),
});
return e.toString(); return e.toString();
}) })
.then((content) => ({ .then((content) => ({
name: tool.function.name,
role: "tool", role: "tool",
content, content,
tool_call_id: tool.id, tool_call_id: tool.id,

View File

@@ -28,7 +28,8 @@ export function fetch(url: string, options?: RequestInit): Promise<any> {
body = [], body = [],
} = options || {}; } = options || {};
let unlisten: Function | undefined; let unlisten: Function | undefined;
let request_id = 0; let setRequestId: Function | undefined;
const requestIdPromise = new Promise((resolve) => (setRequestId = resolve));
const ts = new TransformStream(); const ts = new TransformStream();
const writer = ts.writable.getWriter(); const writer = ts.writable.getWriter();
@@ -47,20 +48,22 @@ export function fetch(url: string, options?: RequestInit): Promise<any> {
} }
// @ts-ignore 2. listen response multi times, and write to Response.body // @ts-ignore 2. listen response multi times, and write to Response.body
window.__TAURI__.event window.__TAURI__.event
.listen("stream-response", (e: ResponseEvent) => { .listen("stream-response", (e: ResponseEvent) =>
const { request_id: rid, chunk, status } = e?.payload || {}; requestIdPromise.then((request_id) => {
if (request_id != rid) { const { request_id: rid, chunk, status } = e?.payload || {};
return; if (request_id != rid) {
} return;
if (chunk) { }
writer.ready.then(() => { if (chunk) {
writer.write(new Uint8Array(chunk)); writer.ready.then(() => {
}); writer.write(new Uint8Array(chunk));
} else if (status === 0) { });
// end of body } else if (status === 0) {
close(); // end of body
} close();
}) }
}),
)
.then((u: Function) => (unlisten = u)); .then((u: Function) => (unlisten = u));
const headers: Record<string, string> = { const headers: Record<string, string> = {
@@ -83,15 +86,15 @@ export function fetch(url: string, options?: RequestInit): Promise<any> {
: [], : [],
}) })
.then((res: StreamResponse) => { .then((res: StreamResponse) => {
request_id = res.request_id; const { request_id, status, status_text: statusText, headers } = res;
const { status, status_text: statusText, headers } = res; setRequestId?.(request_id);
const response = new Response(ts.readable, { const response = new Response(ts.readable, {
status, status,
statusText, statusText,
headers, headers,
}); });
if (status >= 300) { if (status >= 300) {
setTimeout(close, 50); setTimeout(close, 100);
} }
return response; return response;
}) })

21
jest.config.ts Normal file
View File

@@ -0,0 +1,21 @@
import type { Config } from "jest";
import nextJest from "next/jest.js";
const createJestConfig = nextJest({
// Provide the path to your Next.js app to load next.config.js and .env files in your test environment
dir: "./",
});
// Add any custom config to be passed to Jest
const config: Config = {
coverageProvider: "v8",
testEnvironment: "jsdom",
testMatch: ["**/*.test.js", "**/*.test.ts", "**/*.test.jsx", "**/*.test.tsx"],
setupFilesAfterEnv: ["<rootDir>/jest.setup.ts"],
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/$1",
},
};
// createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
export default createJestConfig(config);

2
jest.setup.ts Normal file
View File

@@ -0,0 +1,2 @@
// Learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom";

View File

@@ -6,16 +6,18 @@
"mask": "npx tsx app/masks/build.ts", "mask": "npx tsx app/masks/build.ts",
"mask:watch": "npx watch \"yarn mask\" app/masks", "mask:watch": "npx watch \"yarn mask\" app/masks",
"dev": "concurrently -r \"yarn run mask:watch\" \"next dev\"", "dev": "concurrently -r \"yarn run mask:watch\" \"next dev\"",
"build": "yarn mask && cross-env BUILD_MODE=standalone next build", "build": "yarn test:ci && yarn mask && cross-env BUILD_MODE=standalone next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"export": "yarn mask && cross-env BUILD_MODE=export BUILD_APP=1 next build", "export": "yarn test:ci && 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\"", "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:dev": "concurrently -r \"yarn mask:watch\" \"yarn tauri dev\"",
"app:build": "yarn mask && yarn tauri build", "app:build": "yarn test:ci && yarn mask && yarn tauri build",
"prompts": "node ./scripts/fetch-prompts.mjs", "prompts": "node ./scripts/fetch-prompts.mjs",
"prepare": "husky install", "prepare": "husky install",
"proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev" "proxy-dev": "sh ./scripts/init-proxy.sh && proxychains -f ./scripts/proxychains.conf yarn dev",
"test": "jest --watch",
"test:ci": "jest --ci"
}, },
"dependencies": { "dependencies": {
"@fortaine/fetch-event-source": "^3.0.6", "@fortaine/fetch-event-source": "^3.0.6",
@@ -54,6 +56,9 @@
"devDependencies": { "devDependencies": {
"@tauri-apps/api": "^1.6.0", "@tauri-apps/api": "^1.6.0",
"@tauri-apps/cli": "1.5.11", "@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/js-yaml": "4.0.9", "@types/js-yaml": "4.0.9",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/node": "^20.11.30", "@types/node": "^20.11.30",
@@ -69,8 +74,11 @@
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unused-imports": "^3.2.0", "eslint-plugin-unused-imports": "^3.2.0",
"husky": "^8.0.0", "husky": "^8.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"lint-staged": "^13.2.2", "lint-staged": "^13.2.2",
"prettier": "^3.0.2", "prettier": "^3.0.2",
"ts-node": "^10.9.2",
"tsx": "^4.16.0", "tsx": "^4.16.0",
"typescript": "5.2.2", "typescript": "5.2.2",
"watch": "^1.0.2", "watch": "^1.0.2",

View File

@@ -1,6 +1,7 @@
// //
// //
use std::time::Duration;
use std::error::Error; use std::error::Error;
use std::sync::atomic::{AtomicU32, Ordering}; use std::sync::atomic::{AtomicU32, Ordering};
use std::collections::HashMap; use std::collections::HashMap;
@@ -56,6 +57,7 @@ pub async fn stream_fetch(
let client = Client::builder() let client = Client::builder()
.default_headers(_headers) .default_headers(_headers)
.redirect(reqwest::redirect::Policy::limited(3)) .redirect(reqwest::redirect::Policy::limited(3))
.connect_timeout(Duration::new(3, 0))
.build() .build()
.map_err(|err| format!("failed to generate client: {}", err))?; .map_err(|err| format!("failed to generate client: {}", err))?;

9
test/sum-module.test.ts Normal file
View File

@@ -0,0 +1,9 @@
function sum(a: number, b: number) {
return a + b;
}
describe("sum module", () => {
test("adds 1 + 2 to equal 3", () => {
expect(sum(1, 2)).toBe(3);
});
});

1970
yarn.lock

File diff suppressed because it is too large Load Diff