32 Commits

Author SHA1 Message Date
7793d94514 fix: deepseek rate limit keep-alive
Some checks failed
Deploy static content to Pages / deploy (push) Has been cancelled
2025-06-10 18:20:03 +08:00
24973eabfe fix: structuredClone template
Some checks are pending
Deploy static content to Pages / deploy (push) Waiting to run
2025-06-10 17:13:53 +08:00
6078d8a2c3 remove gitea action 2025-06-10 15:38:43 +08:00
d830b92fbf feat: Add tooltips to template attributes
Some checks failed
Build static content / build (push) Has been cancelled
2025-05-30 18:50:21 +08:00
7694ed6792 Merge branch 'cursor' 2025-05-30 18:36:57 +08:00
6b8426868a Fix: Template attribute dialog input and type issues 2025-05-30 18:35:40 +08:00
e18dd9b680 Merge pull request #25 from heimoshuiyu/cursor
feat: Add edit and delete functionality for templates and enhance JSON editor
2025-05-28 09:58:50 +08:00
13295bd24d Refactor: Replace window.confirm with custom dialog component 2025-05-28 09:57:46 +08:00
aa83f10657 feat: Add edit and delete functionality for templates and enhance JSON editor 2025-05-28 09:37:29 +08:00
95b319db7d Merge pull request #24 from heimoshuiyu/cursor
fix: chat template / api template select menu not working for the first click
2025-05-28 09:30:41 +08:00
8f24489959 refac: api template menu 2025-05-28 02:10:18 +08:00
39c3860c78 fix: chat template menu by cursor 2025-05-28 02:01:04 +08:00
812ce3cc1f fix: conversation list show in startup 2025-04-23 10:21:51 +08:00
667b334dfc feat: add URL parameter configuration import dialog and related functionality 2025-03-25 10:49:10 +08:00
9b32948cfa remove lg:w-[65%] 2025-03-24 16:09:39 +08:00
9fbd9b98c2 Merge branch 'dev' 2025-03-24 15:50:29 +08:00
14df7bebac Revert "fix overflow"
This reverts commit 2a39ff885a.
2025-03-24 15:49:29 +08:00
e4919bb91f fix: panic if usage is null in stream mode 2025-02-20 16:42:52 +08:00
2a39ff885a fix overflow 2025-02-20 11:59:36 +08:00
c03dbef798 add response count to ChatStoreMessage and update message creation 2025-02-08 14:25:12 +08:00
8cd43bec72 add Chinese translations for "follow" and "stop generating"; log translation misses only for non-English 2025-02-08 11:03:20 +08:00
ed5f561148 refactor Chatbox component layout and restore stop generating button 2025-02-08 11:03:00 +08:00
ecwu
8d4a9b840a Add syntax highlighting support to MessageBubble component with rehype-highlight and highlight.js 2025-02-07 21:52:09 +00:00
ecwu
8db892caf7 Update MessageBubble component to adjust Markdown styling and improve responsiveness 2025-02-07 21:37:53 +00:00
ecwu
d18040dca1 Add @tailwindcss/typography plugin and update MessageBubble component for improved Markdown rendering 2025-02-07 21:32:14 +00:00
ecwu
a5f7447f4f Refactor MessageBubble component to enhance Markdown rendering for list items and paragraphs 2025-02-07 17:42:49 +00:00
ecwu
332a645e34 Enhance MessageBubble component with improved styling and collapsible content 2025-02-07 17:30:52 +00:00
c37a99f06d fix haha 2025-02-08 00:54:32 +08:00
Zhenghao Wu
3e89e88c1d Merge pull request #23 from heimoshuiyu/master
sync 0207
2025-02-07 16:52:54 +00:00
5b4a0507ae stop generating store message 2025-02-08 00:09:49 +08:00
75bf4a419d 访问冲突警告 2025-02-07 23:14:12 +08:00
7dea556a56 fix history 2025-02-07 21:40:48 +08:00
23 changed files with 14699 additions and 518 deletions

View File

@@ -1,27 +0,0 @@
name: Build static content
on:
# Runs on pushes targeting the default branch
push:
branches: ["master"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 20.x
cache: 'npm'
- run: npm install
- run: npm run build
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: dist-files
path: './dist/'

90
package-lock.json generated
View File

@@ -37,6 +37,7 @@
"@radix-ui/react-toggle": "^1.1.1", "@radix-ui/react-toggle": "^1.1.1",
"@radix-ui/react-toggle-group": "^1.1.1", "@radix-ui/react-toggle-group": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.6", "@radix-ui/react-tooltip": "^1.1.6",
"@tailwindcss/typography": "^0.5.16",
"@types/react": "^18.0.0", "@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0", "@types/react-dom": "^18.0.0",
"@types/ungap__structured-clone": "^1.2.0", "@types/ungap__structured-clone": "^1.2.0",
@@ -47,6 +48,7 @@
"cmdk": "1.0.4", "cmdk": "1.0.4",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"embla-carousel-react": "^8.5.1", "embla-carousel-react": "^8.5.1",
"highlight.js": "^11.11.1",
"idb": "^8.0.1", "idb": "^8.0.1",
"input-otp": "^1.4.1", "input-otp": "^1.4.1",
"lucide-react": "^0.469.0", "lucide-react": "^0.469.0",
@@ -58,6 +60,7 @@
"react-markdown": "^9.0.3", "react-markdown": "^9.0.3",
"react-resizable-panels": "^2.1.7", "react-resizable-panels": "^2.1.7",
"recharts": "^2.15.0", "recharts": "^2.15.0",
"rehype-highlight": "^7.0.2",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"sakura.css": "^1.5.0", "sakura.css": "^1.5.0",
@@ -3884,6 +3887,34 @@
"string.prototype.matchall": "^4.0.6" "string.prototype.matchall": "^4.0.6"
} }
}, },
"node_modules/@tailwindcss/typography": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
"integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==",
"license": "MIT",
"dependencies": {
"lodash.castarray": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.merge": "^4.6.2",
"postcss-selector-parser": "6.0.10"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@types/babel__core": { "node_modules/@types/babel__core": {
"version": "7.20.5", "version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -6103,6 +6134,15 @@
"url": "https://opencollective.com/unified" "url": "https://opencollective.com/unified"
} }
}, },
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/html-url-attributes": { "node_modules/html-url-attributes": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
@@ -6875,6 +6915,12 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.castarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
"license": "MIT"
},
"node_modules/lodash.debounce": { "node_modules/lodash.debounce": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -6882,6 +6928,18 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"license": "MIT"
},
"node_modules/lodash.sortby": { "node_modules/lodash.sortby": {
"version": "4.7.0", "version": "4.7.0",
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
@@ -6911,6 +6969,21 @@
"loose-envify": "cli.js" "loose-envify": "cli.js"
} }
}, },
"node_modules/lowlight": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz",
"integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"devlop": "^1.0.0",
"highlight.js": "~11.11.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/wooorm"
}
},
"node_modules/lru-cache": { "node_modules/lru-cache": {
"version": "10.4.3", "version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
@@ -8534,6 +8607,23 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/rehype-highlight": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz",
"integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==",
"license": "MIT",
"dependencies": {
"@types/hast": "^3.0.0",
"hast-util-to-text": "^4.0.0",
"lowlight": "^3.0.0",
"unist-util-visit": "^5.0.0",
"vfile": "^6.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/unified"
}
},
"node_modules/rehype-katex": { "node_modules/rehype-katex": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz", "resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz",

View File

@@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@heroicons/react": "^2.2.0", "@heroicons/react": "^2.2.0",
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@monaco-editor/react": "^4.7.0",
"@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.4", "@radix-ui/react-alert-dialog": "^1.1.4",
"@radix-ui/react-aspect-ratio": "^1.1.1", "@radix-ui/react-aspect-ratio": "^1.1.1",
@@ -38,6 +39,7 @@
"@radix-ui/react-toggle": "^1.1.1", "@radix-ui/react-toggle": "^1.1.1",
"@radix-ui/react-toggle-group": "^1.1.1", "@radix-ui/react-toggle-group": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.6", "@radix-ui/react-tooltip": "^1.1.6",
"@tailwindcss/typography": "^0.5.16",
"@types/react": "^18.0.0", "@types/react": "^18.0.0",
"@types/react-dom": "^18.0.0", "@types/react-dom": "^18.0.0",
"@types/ungap__structured-clone": "^1.2.0", "@types/ungap__structured-clone": "^1.2.0",
@@ -48,6 +50,7 @@
"cmdk": "1.0.4", "cmdk": "1.0.4",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"embla-carousel-react": "^8.5.1", "embla-carousel-react": "^8.5.1",
"highlight.js": "^11.11.1",
"idb": "^8.0.1", "idb": "^8.0.1",
"input-otp": "^1.4.1", "input-otp": "^1.4.1",
"lucide-react": "^0.469.0", "lucide-react": "^0.469.0",
@@ -59,6 +62,7 @@
"react-markdown": "^9.0.3", "react-markdown": "^9.0.3",
"react-resizable-panels": "^2.1.7", "react-resizable-panels": "^2.1.7",
"recharts": "^2.15.0", "recharts": "^2.15.0",
"rehype-highlight": "^7.0.2",
"rehype-katex": "^7.0.1", "rehype-katex": "^7.0.1",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"sakura.css": "^1.5.0", "sakura.css": "^1.5.0",

7654
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -338,6 +338,9 @@ class Chat {
console.log("line", line); console.log("line", line);
try { try {
const jsonStr = line.slice("data:".length).trim(); const jsonStr = line.slice("data:".length).trim();
if (jsonStr === "keep-alive") { // for deepseek https://api-docs.deepseek.com/quick_start/rate_limit
continue;
}
const json = JSON.parse(jsonStr) as StreamingResponseChunk; const json = JSON.parse(jsonStr) as StreamingResponseChunk;
yield json; yield json;
} catch (e) { } catch (e) {

View File

@@ -0,0 +1,129 @@
import { Tr } from "@/translate";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "./ui/alert-dialog";
import { useContext } from "react";
import { AppChatStoreContext, AppContext } from "@/pages/App";
import { STORAGE_NAME } from "@/const";
const Item = ({ children }: { children: React.ReactNode }) => (
<div className="mt-2">{children}</div>
);
const ImportDialog = ({
open,
setOpen,
}: {
open: boolean;
setOpen: (open: boolean) => void;
}) => {
const { handleNewChatStoreWithOldOne } = useContext(AppContext);
const { chatStore } = useContext(AppChatStoreContext);
const params = new URLSearchParams(window.location.search);
const api = params.get("api");
const key = params.get("key");
const sys = params.get("sys");
const mode = params.get("mode");
const model = params.get("model");
const max = params.get("max");
const temp = params.get("temp");
const dev = params.get("dev");
const whisper_api = params.get("whisper-api");
const whisper_key = params.get("whisper-key");
const tts_api = params.get("tts-api");
const tts_key = params.get("tts-key");
return (
<AlertDialog open={open}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
<Tr>Import Configuration</Tr>
</AlertDialogTitle>
<AlertDialogDescription className="message-content">
<Tr>There are some configurations in the URL, import them?</Tr>
{key && <Item>Key: {key}</Item>}
{api && <Item>API: {api}</Item>}
{sys && <Item>Sys: {sys}</Item>}
{mode && <Item>Mode: {mode}</Item>}
{model && <Item>Model: {model}</Item>}
{max && <Item>Max: {max}</Item>}
{temp && <Item>Temp: {temp}</Item>}
{dev && <Item>Dev: {dev}</Item>}
{whisper_api && <Item>Whisper API: {whisper_api}</Item>}
{whisper_key && <Item>Whisper Key: {whisper_key}</Item>}
{tts_api && <div className="mt-2">TTS API: {tts_api}</div>}
{tts_key && <div className="mt-2">TTS Key: {tts_key}</div>}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setOpen(false)}>
<Tr>Cancel</Tr>
</AlertDialogCancel>
<AlertDialogAction
onClick={async () => {
params.delete("key");
params.delete("api");
params.delete("sys");
params.delete("mode");
params.delete("model");
params.delete("max");
params.delete("temp");
params.delete("dev");
params.delete("whisper-api");
params.delete("whisper-key");
params.delete("tts-api");
params.delete("tts-key");
const newChatStore = structuredClone(chatStore);
if (key) newChatStore.apiKey = key;
if (api) newChatStore.apiEndpoint = api;
if (sys) newChatStore.systemMessageContent = sys;
if (mode) newChatStore.streamMode = mode === "stream";
if (model) newChatStore.model = model;
if (max) {
try {
newChatStore.maxTokens = parseInt(max);
} catch (e) {
console.error(e);
}
}
if (temp) {
try {
newChatStore.temperature = parseFloat(temp);
} catch (e) {
console.error(e);
}
}
if (dev) newChatStore.develop_mode = dev === "true";
if (whisper_api) newChatStore.whisper_api = whisper_api;
if (whisper_key) newChatStore.whisper_key = whisper_key;
if (tts_api) newChatStore.tts_api = tts_api;
if (tts_key) newChatStore.tts_key = tts_key;
await handleNewChatStoreWithOldOne(newChatStore);
const newUrl =
window.location.pathname +
(params.toString() ? `?${params}` : "");
window.history.replaceState(null, "", newUrl); // 替换URL不刷新页面
setOpen(false);
}}
>
<Tr>Import</Tr>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};
export default ImportDialog;

View File

@@ -1,16 +1,15 @@
import React from "react"; import React, { useContext, useState, useRef } from "react";
import { ChatStore, TemplateAPI, TemplateChatStore } from "@/types/chatstore";
import { Tr } from "@/translate";
import { import {
NavigationMenuContent, ChatStore,
NavigationMenuItem, TemplateAPI,
NavigationMenuLink, TemplateChatStore,
NavigationMenuTrigger, TemplateTools,
} from "@/components/ui/navigation-menu"; } from "@/types/chatstore";
import { Tr } from "@/translate";
import Editor from "@monaco-editor/react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { useContext } from "react";
import { AppChatStoreContext, AppContext } from "@/pages/App"; import { AppChatStoreContext, AppContext } from "@/pages/App";
import { import {
NavigationMenu, NavigationMenu,
@@ -37,14 +36,17 @@ import {
Dialog, Dialog,
DialogContent, DialogContent,
DialogHeader, DialogHeader,
DialogTrigger, DialogTitle,
DialogFooter,
DialogClose,
} from "./ui/dialog"; } from "./ui/dialog";
import { DialogTitle } from "@radix-ui/react-dialog";
import { Textarea } from "./ui/textarea"; import { Textarea } from "./ui/textarea";
import { Label } from "./ui/label"; import { Label } from "./ui/label";
import { Input } from "./ui/input"; import { Input } from "./ui/input";
import { SetAPIsTemplate } from "./setAPIsTemplate"; import { SetAPIsTemplate } from "./setAPIsTemplate";
import { isVailedJSON } from "@/utils/isVailedJSON"; import { isVailedJSON } from "@/utils/isVailedJSON";
import { toast } from 'sonner';
import { ConfirmationDialog } from "./ui/confirmation-dialog";
interface APITemplateDropdownProps { interface APITemplateDropdownProps {
label: string; label: string;
@@ -52,6 +54,81 @@ interface APITemplateDropdownProps {
apiField: string; apiField: string;
keyField: string; keyField: string;
} }
interface EditTemplateDialogProps {
template: TemplateAPI;
onSave: (updatedTemplate: TemplateAPI) => void;
onClose: () => void;
}
function EditTemplateDialog({ template, onSave, onClose }: EditTemplateDialogProps) {
const [name, setName] = useState(template.name);
const [endpoint, setEndpoint] = useState(template.endpoint);
const [key, setKey] = useState(template.key);
const { toast } = useToast();
const handleSave = () => {
if (!name.trim()) {
toast({
title: "Error",
description: "Template name cannot be empty",
variant: "destructive",
});
return;
}
onSave({
...template,
name: name.trim(),
endpoint: endpoint.trim(),
key: key.trim(),
});
onClose();
};
return (
<Dialog open onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit Template</DialogTitle>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid gap-2">
<Label htmlFor="name">Template Name</Label>
<Input
id="name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="endpoint">API Endpoint</Label>
<Input
id="endpoint"
value={endpoint}
onChange={(e) => setEndpoint(e.target.value)}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="key">API Key</Label>
<Input
id="key"
value={key}
onChange={(e) => setKey(e.target.value)}
/>
</div>
</div>
<DialogFooter>
<DialogClose asChild>
<Button variant="outline">Cancel</Button>
</DialogClose>
<Button onClick={handleSave}>Save Changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function APIsDropdownList({ function APIsDropdownList({
label, label,
shortLabel, shortLabel,
@@ -72,73 +149,147 @@ function APIsDropdownList({
setTemplateAPIsWhisper, setTemplateAPIsWhisper,
setTemplateTools, setTemplateTools,
} = useContext(AppContext); } = useContext(AppContext);
const { toast } = useToast();
const [open, setOpen] = React.useState(false);
const [editingTemplate, setEditingTemplate] = useState<TemplateAPI | null>(null);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [templateToDelete, setTemplateToDelete] = useState<TemplateAPI | null>(null);
let API = templateAPIs; let API = templateAPIs;
let setAPI = setTemplateAPIs;
if (label === "Chat API") { if (label === "Chat API") {
API = templateAPIs; API = templateAPIs;
setAPI = setTemplateAPIs;
} else if (label === "Whisper API") { } else if (label === "Whisper API") {
API = templateAPIsWhisper; API = templateAPIsWhisper;
setAPI = setTemplateAPIsWhisper;
} else if (label === "TTS API") { } else if (label === "TTS API") {
API = templateAPIsTTS; API = templateAPIsTTS;
setAPI = setTemplateAPIsTTS;
} else if (label === "Image Gen API") { } else if (label === "Image Gen API") {
API = templateAPIsImageGen; API = templateAPIsImageGen;
setAPI = setTemplateAPIsImageGen;
} }
const handleEdit = (template: TemplateAPI) => {
setEditingTemplate(template);
};
const handleSave = (updatedTemplate: TemplateAPI) => {
const index = API.findIndex(t => t.name === updatedTemplate.name);
if (index !== -1) {
const newAPI = [...API];
newAPI[index] = updatedTemplate;
setAPI(newAPI);
toast({
title: "Success",
description: "Template updated successfully",
});
}
};
const handleDelete = (template: TemplateAPI) => {
setTemplateToDelete(template);
setDeleteDialogOpen(true);
};
const confirmDelete = () => {
if (templateToDelete) {
const newAPI = API.filter(t => t.name !== templateToDelete.name);
setAPI(newAPI);
toast({
title: "Success",
description: "Template deleted successfully",
});
}
};
return ( return (
<NavigationMenuItem> <div className="flex items-center space-x-4 mx-3">
<NavigationMenuTrigger> <p className="text-sm text-muted-foreground">
<span className="lg:hidden">{shortLabel}</span> <Tr>{label}</Tr>
<span className="hidden lg:inline"> </p>
{label}{" "} <Popover open={open} onOpenChange={setOpen}>
{API.find( <PopoverTrigger asChild>
(t: TemplateAPI) => <Button variant="outline" className="w-[150px] justify-start">
chatStore[apiField as keyof ChatStore] === t.endpoint && {API.find(
chatStore[keyField as keyof ChatStore] === t.key (t: TemplateAPI) =>
)?.name && chatStore[apiField as keyof ChatStore] === t.endpoint &&
`: ${ chatStore[keyField as keyof ChatStore] === t.key
API.find( )?.name || `+ ${shortLabel}`}
(t: TemplateAPI) => </Button>
chatStore[apiField as keyof ChatStore] === t.endpoint && </PopoverTrigger>
chatStore[keyField as keyof ChatStore] === t.key <PopoverContent className="p-0" side="bottom" align="start">
)?.name <Command>
}`} <CommandInput placeholder="Search template..." />
</span> <CommandList>
</NavigationMenuTrigger> <CommandEmpty>
<NavigationMenuContent> <Tr>No results found.</Tr>
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] "> </CommandEmpty>
{API.map((t: TemplateAPI, index: number) => ( <CommandGroup>
<li key={index}> {API.map((t: TemplateAPI, index: number) => (
<NavigationMenuLink asChild> <CommandItem
<a key={index}
onClick={() => { value={t.name}
// @ts-ignore onSelect={() => {
chatStore[apiField as keyof ChatStore] = t.endpoint; setChatStore({
// @ts-ignore ...chatStore,
chatStore[keyField] = t.key; [apiField]: t.endpoint,
setChatStore({ [keyField]: t.key,
...chatStore, });
}); setOpen(false);
}} }}
className={cn( >
"block select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground", <div className="flex items-center justify-between w-full">
chatStore[apiField as keyof ChatStore] === t.endpoint && <span>{t.name}</span>
chatStore[keyField as keyof ChatStore] === t.key <div className="flex gap-2">
? "bg-accent text-accent-foreground" <Button
: "" variant="ghost"
)} size="icon"
> onClick={(e) => {
<div className="text-sm font-medium leading-none"> e.stopPropagation();
{t.name} handleEdit(t);
</div> }}
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground"> >
{new URL(t.endpoint).host} <EditIcon className="h-4 w-4" />
</p> </Button>
</a> <Button
</NavigationMenuLink> variant="ghost"
</li> size="icon"
))} onClick={(e) => {
</ul> e.stopPropagation();
</NavigationMenuContent> handleDelete(t);
</NavigationMenuItem> }}
>
<DeleteIcon className="h-4 w-4" />
</Button>
</div>
</div>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
{editingTemplate && (
<EditTemplateDialog
template={editingTemplate}
onSave={handleSave}
onClose={() => setEditingTemplate(null)}
/>
)}
<ConfirmationDialog
isOpen={deleteDialogOpen}
onClose={() => {
setDeleteDialogOpen(false);
setTemplateToDelete(null);
}}
onConfirm={confirmDelete}
title="Delete Template"
description={`Are you sure you want to delete "${templateToDelete?.name}"?`}
/>
</div>
); );
} }
@@ -197,7 +348,7 @@ function ToolsDropdownList() {
<BrushIcon /> <Tr>Clear tools</Tr> <BrushIcon /> <Tr>Clear tools</Tr>
</CommandItem> </CommandItem>
)} )}
{ctx.templateTools.map((t, index) => ( {ctx.templateTools.map((t: TemplateTools, index: number) => (
<CommandItem <CommandItem
key={index} key={index}
value={t.toolsString} value={t.toolsString}
@@ -218,170 +369,278 @@ function ToolsDropdownList() {
); );
} }
function ChatTemplateDropdownList() { interface EditChatTemplateDialogProps {
const ctx = useContext(AppContext); template: TemplateChatStore;
onSave: (updatedTemplate: TemplateChatStore) => void;
onClose: () => void;
}
const { chatStore, setChatStore } = useContext(AppChatStoreContext); function EditChatTemplateDialog({ template, onSave, onClose }: EditChatTemplateDialogProps) {
const { templates, setTemplates } = useContext(AppContext); const [name, setName] = useState(template.name);
const [jsonContent, setJsonContent] = useState(() => {
const { name: _, ...rest } = template;
return JSON.stringify(rest, null, 2);
});
const [editor, setEditor] = useState<any>(null);
const handleEditorDidMount = (editor: any) => {
setEditor(editor);
};
const handleFormat = () => {
if (editor) {
editor.getAction('editor.action.formatDocument').run();
}
};
const handleSave = () => {
if (!name.trim()) {
toast.error('Template name cannot be empty');
return;
}
try {
const parsedJson = JSON.parse(jsonContent);
const updatedTemplate: TemplateChatStore = {
name: name.trim(),
...parsedJson
};
onSave(updatedTemplate);
toast.success('Template updated successfully');
} catch (error) {
toast.error('Invalid JSON format');
}
};
return ( return (
<NavigationMenuItem> <Dialog open onOpenChange={onClose}>
<NavigationMenuTrigger> <DialogContent className="max-w-4xl">
<span className="lg:hidden">Chat Template</span> <DialogHeader>
<span className="hidden lg:inline">Chat Template</span> <DialogTitle>Edit Template</DialogTitle>
</NavigationMenuTrigger> </DialogHeader>
<NavigationMenuContent> <div className="space-y-4">
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px]"> <div>
{templates.map((t: TemplateChatStore, index: number) => ( <Label htmlFor="name">Template Name</Label>
<ChatTemplateItem key={index} t={t} index={index} /> <Input
))} id="name"
</ul> value={name}
</NavigationMenuContent> onChange={(e) => setName(e.target.value)}
</NavigationMenuItem> placeholder="Enter template name"
/>
</div>
<div>
<Label>Template Content (JSON)</Label>
<div className="relative">
<Button
variant="outline"
size="sm"
className="absolute right-2 top-2 z-10"
onClick={handleFormat}
>
Format JSON
</Button>
<div className="h-[400px] border rounded-md">
<Editor
height="400px"
defaultLanguage="json"
value={jsonContent}
onChange={(value) => setJsonContent(value || '')}
onMount={handleEditorDidMount}
options={{
minimap: { enabled: false },
fontSize: 14,
lineNumbers: 'on',
scrollBeyondLastLine: false,
automaticLayout: true,
tabSize: 2,
wordWrap: 'on'
}}
/>
</div>
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button onClick={handleSave}>Save Changes</Button>
</DialogFooter>
</DialogContent>
</Dialog>
); );
} }
const ChatTemplateItem = ({ function ChatTemplateDropdownList() {
t, const ctx = useContext(AppContext);
index,
}: {
t: TemplateChatStore;
index: number;
}) => {
const [dialogOpen, setDialogOpen] = React.useState(false);
const { chatStore, setChatStore } = useContext(AppChatStoreContext); const { chatStore, setChatStore } = useContext(AppChatStoreContext);
const { templates, setTemplates } = useContext(AppContext); const { templates, setTemplates } = useContext(AppContext);
const [open, setOpen] = React.useState(false);
const [editingTemplate, setEditingTemplate] = useState<TemplateChatStore | null>(null);
const { toast } = useToast();
const [confirmDialogOpen, setConfirmDialogOpen] = useState(false);
const [templateToApply, setTemplateToApply] = useState<TemplateChatStore | null>(null);
const handleEdit = (template: TemplateChatStore) => {
setEditingTemplate(template);
};
const handleSave = (updatedTemplate: TemplateChatStore) => {
const index = templates.findIndex(t => t.name === updatedTemplate.name);
if (index !== -1) {
const newTemplates = [...templates];
newTemplates[index] = updatedTemplate;
setTemplates(newTemplates);
toast({
title: "Success",
description: "Template updated successfully",
});
}
};
const handleDelete = (template: TemplateChatStore) => {
setTemplateToApply(template);
setConfirmDialogOpen(true);
};
const handleTemplateSelect = (template: TemplateChatStore) => {
if (chatStore.history.length > 0 || chatStore.systemMessageContent) {
setTemplateToApply(template);
setConfirmDialogOpen(true);
} else {
applyTemplate(template);
}
};
const applyTemplate = (template: TemplateChatStore) => {
setChatStore({
...newChatStore({
...chatStore,
...{
use_this_history: template.history ?? chatStore.history,
},
...template,
}),
});
setOpen(false);
};
return ( return (
<li <div className="flex items-center space-x-4 mx-3">
onClick={() => { <p className="text-sm text-muted-foreground">
// Update chatStore with the selected template <Tr>Chat Template</Tr>
if (chatStore.history.length > 0 || chatStore.systemMessageContent) { </p>
console.log("you clicked", t.name); <Popover open={open} onOpenChange={setOpen}>
const confirm = window.confirm( <PopoverTrigger asChild>
"This will replace the current chat history. Are you sure?" <Button variant="outline" className="w-[150px] justify-start">
); <Tr>Select Template</Tr>
if (!confirm) return; </Button>
} </PopoverTrigger>
setChatStore({ ...newChatStore({ ...chatStore, ...t }) }); <PopoverContent className="p-0" side="bottom" align="start">
}} <Command>
> <CommandInput placeholder="Search template..." />
<NavigationMenuLink asChild> <CommandList>
<a <CommandEmpty>
className={cn( <Tr>No results found.</Tr>
"flex flex-row justify-between items-center select-none space-y-1 rounded-md p-3 leading-none no-underline outline-none transition-colors hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground" </CommandEmpty>
)} <CommandGroup>
> {templates.map((t: TemplateChatStore, index: number) => (
<div className="text-sm font-medium leading-non">{t.name}</div> <CommandItem
<div onClick={(e) => e.stopPropagation()}> key={index}
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}> value={t.name}
<DialogTrigger asChild> onSelect={() => handleTemplateSelect(structuredClone(t))}
<EditIcon /> >
</DialogTrigger> <div className="flex items-center justify-between w-full">
<DialogContent> <span>{t.name}</span>
<DialogHeader> <div className="flex gap-2">
<DialogTitle>Edit Template</DialogTitle> <Button
</DialogHeader> variant="ghost"
<Label>Template Name</Label> size="icon"
<Input onClick={(e) => {
value={t.name} e.stopPropagation();
onBlur={(e) => { handleEdit(t);
t.name = e.target.value; }}
templates[index] = t; >
setTemplates([...templates]); <EditIcon className="h-4 w-4" />
}} </Button>
/> <Button
<p> variant="ghost"
Raw JSON allows you to modify any content within the template. size="icon"
You can remove unnecessary fields, and non-existent fields onClick={(e) => {
will be inherited from the current session. e.stopPropagation();
</p> handleDelete(t);
<Textarea }}
className="h-64" >
value={JSON.stringify(t, null, 2)} <DeleteIcon className="h-4 w-4" />
onBlur={(e) => { </Button>
try { </div>
const json = JSON.parse( </div>
e.target.value </CommandItem>
) as TemplateChatStore; ))}
json.name = t.name; </CommandGroup>
templates[index] = json; </CommandList>
setTemplates([...templates]); </Command>
} catch (e) { </PopoverContent>
console.error(e); </Popover>
alert("Invalid JSON"); {editingTemplate && (
} <EditChatTemplateDialog
}} template={editingTemplate}
/> onSave={handleSave}
<Button onClose={() => setEditingTemplate(null)}
type="submit" />
variant={"destructive"} )}
onClick={() => { <ConfirmationDialog
let confirm = window.confirm( isOpen={confirmDialogOpen}
"Are you sure you want to delete this template?" onClose={() => {
); setConfirmDialogOpen(false);
if (!confirm) return; setTemplateToApply(null);
templates.splice(index, 1); }}
setTemplates([...templates]); onConfirm={() => templateToApply && applyTemplate(templateToApply)}
setDialogOpen(false); title="Replace Chat History"
}} description="This will replace the current chat history. Are you sure?"
> />
Delete </div>
</Button>
<Button type="submit" onClick={() => setDialogOpen(false)}>
Close
</Button>
</DialogContent>
</Dialog>
</div>
</a>
</NavigationMenuLink>
</li>
); );
}; }
const APIListMenu: React.FC = () => { const APIListMenu: React.FC = () => {
const ctx = useContext(AppContext); const ctx = useContext(AppContext);
return ( return (
<div className="flex flex-col my-2 gap-2 w-full"> <div className="flex flex-col my-2 gap-2 w-full">
{ctx.templateTools.length > 0 && <ToolsDropdownList />} {ctx.templateTools.length > 0 && <ToolsDropdownList />}
<NavigationMenu> {ctx.templates.length > 0 && <ChatTemplateDropdownList />}
<NavigationMenuList> {ctx.templateAPIs.length > 0 && (
{ctx.templates.length > 0 && <ChatTemplateDropdownList />} <APIsDropdownList
{ctx.templateAPIs.length > 0 && ( label="Chat API"
<APIsDropdownList shortLabel="Chat"
label="Chat API" apiField="apiEndpoint"
shortLabel="Chat" keyField="apiKey"
apiField="apiEndpoint" />
keyField="apiKey" )}
/> {ctx.templateAPIsWhisper.length > 0 && (
)} <APIsDropdownList
{ctx.templateAPIsWhisper.length > 0 && ( label="Whisper API"
<APIsDropdownList shortLabel="Whisper"
label="Whisper API" apiField="whisper_api"
shortLabel="Whisper" keyField="whisper_key"
apiField="whisper_api" />
keyField="whisper_key" )}
/> {ctx.templateAPIsTTS.length > 0 && (
)} <APIsDropdownList
{ctx.templateAPIsTTS.length > 0 && ( label="TTS API"
<APIsDropdownList shortLabel="TTS"
label="TTS API" apiField="tts_api"
shortLabel="TTS" keyField="tts_key"
apiField="tts_api" />
keyField="tts_key" )}
/> {ctx.templateAPIsImageGen.length > 0 && (
)} <APIsDropdownList
{ctx.templateAPIsImageGen.length > 0 && ( label="Image Gen API"
<APIsDropdownList shortLabel="ImgGen"
label="Image Gen API" apiField="image_gen_api"
shortLabel="ImgGen" keyField="image_gen_key"
apiField="image_gen_api" />
keyField="image_gen_key" )}
/>
)}
</NavigationMenuList>
</NavigationMenu>
</div> </div>
); );
}; };

View File

@@ -2,6 +2,7 @@ import { LightBulbIcon, XMarkIcon } from "@heroicons/react/24/outline";
import Markdown from "react-markdown"; import Markdown from "react-markdown";
import remarkMath from "remark-math"; import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex"; import rehypeKatex from "rehype-katex";
import rehypeHighlight from "rehype-highlight";
import "katex/dist/katex.min.css"; import "katex/dist/katex.min.css";
import { import {
useContext, useContext,
@@ -51,10 +52,12 @@ function MessageHide({ chat }: HideMessageProps) {
return ( return (
<> <>
<div className="flex items-center gap-2 text-sm text-muted-foreground"> <div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>{getMessageText(chat).split("\n")[0].slice(0, 28)} ...</span> <span>{getMessageText(chat).trim().slice(0, 28)} ...</span>
</div> </div>
<div className="flex mt-2 justify-center"> <div className="flex mt-2 justify-center">
<Badge variant="destructive">Removed from context</Badge> <Badge variant="destructive">
<Tr>Removed from context</Tr>
</Badge>
</div> </div>
</> </>
); );
@@ -73,7 +76,7 @@ function MessageDetail({ chat, renderMarkdown }: MessageDetailProps) {
{chat.content.map((mdt) => {chat.content.map((mdt) =>
mdt.type === "text" ? ( mdt.type === "text" ? (
chat.hide ? ( chat.hide ? (
mdt.text?.split("\n")[0].slice(0, 16) + " ..." mdt.text?.trim().slice(0, 16) + " ..."
) : renderMarkdown ? ( ) : renderMarkdown ? (
<Markdown>{mdt.text}</Markdown> <Markdown>{mdt.text}</Markdown>
) : ( ) : (
@@ -305,26 +308,28 @@ export default function Message(props: { messageIndex: number }) {
</div> </div>
)} )}
{chat.role === "assistant" ? ( {chat.role === "assistant" ? (
<div className="border-b border-border dark:border-border-dark pb-4"> <div className="pb-4">
{chat.reasoning_content ? ( {chat.reasoning_content ? (
<Collapsible className="mb-3"> <Card className="bg-muted hover:bg-muted/80 mb-5 w-full">
<div className="flex items-center justify-between"> <Collapsible>
<div className="flex items-center gap-2"> <div className="flex items-center justify-between px-3 py-1">
<h4 className="text-sm font-semibold text-gray-500"> <div className="flex items-center">
{chat.response_model_name} <h4 className="font-semibold text-sm">
</h4> Think Content of {chat.response_model_name}
<CollapsibleTrigger asChild> </h4>
<Button variant="ghost" size="sm"> <CollapsibleTrigger asChild>
<LightBulbIcon className="h-3 w-3 text-gray-500" /> <Button variant="ghost" size="sm">
<span className="sr-only">Toggle</span> <LightBulbIcon className="h-3 w-3 text-gray-500" />
</Button> <span className="sr-only">Toggle</span>
</CollapsibleTrigger> </Button>
</CollapsibleTrigger>
</div>
</div> </div>
</div> <CollapsibleContent className="ml-5 text-gray-500 message-content p">
<CollapsibleContent className="ml-5 text-gray-500 message-content"> {chat.reasoning_content.trim()}
{chat.reasoning_content.trim()} </CollapsibleContent>
</CollapsibleContent> </Collapsible>
</Collapsible> </Card>
) : null} ) : null}
<div> <div>
{chat.hide ? ( {chat.hide ? (
@@ -334,37 +339,35 @@ export default function Message(props: { messageIndex: number }) {
) : chat.tool_calls ? ( ) : chat.tool_calls ? (
<MessageToolCall chat={chat} copyToClipboard={copyToClipboard} /> <MessageToolCall chat={chat} copyToClipboard={copyToClipboard} />
) : renderMarkdown ? ( ) : renderMarkdown ? (
<div className="message-content max-w-full md:max-w-[100%]"> <Markdown
<Markdown remarkPlugins={[remarkMath]}
remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex, rehypeHighlight]}
rehypePlugins={[rehypeKatex]} disallowedElements={[
//break={true} "script",
components={{ "iframe",
code: ({ children }) => ( "object",
<code className="bg-muted px-1 py-0.5 rounded"> "embed",
{children} "hr",
</code> ]}
), // allowElement={(element) => {
pre: ({ children }) => ( // return [
<pre className="bg-muted p-4 rounded-lg overflow-auto"> // "p",
{children} // "em",
</pre> // "strong",
), // "del",
a: ({ href, children }) => ( // "code",
<a // "inlineCode",
href={href} // "blockquote",
target="_blank" // "ul",
rel="noopener noreferrer" // "ol",
className="text-primary hover:underline" // "li",
> // "pre",
{children} // ].includes(element.tagName);
</a> // }}
), className={"prose max-w-none md:max-w-[75%]"}
}} >
> {getMessageText(chat)}
{getMessageText(chat)} </Markdown>
</Markdown>
</div>
) : ( ) : (
<div className="message-content max-w-full md:max-w-[100%]"> <div className="message-content max-w-full md:max-w-[100%]">
{chat.content && {chat.content &&
@@ -433,7 +436,18 @@ export default function Message(props: { messageIndex: number }) {
) : chat.role === "tool" ? ( ) : chat.role === "tool" ? (
<MessageToolResp chat={chat} copyToClipboard={copyToClipboard} /> <MessageToolResp chat={chat} copyToClipboard={copyToClipboard} />
) : renderMarkdown ? ( ) : renderMarkdown ? (
<Markdown>{getMessageText(chat)}</Markdown> <Markdown
components={{
p: ({ children, node }: any) => {
if (node?.parent?.type === "listItem") {
return <>{children}</>;
}
return <p>{children}</p>;
},
}}
>
{getMessageText(chat)}
</Markdown>
) : ( ) : (
<div className="message-content"> <div className="message-content">
{chat.content && {chat.content &&

View File

@@ -10,7 +10,6 @@ import { tr, Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
import { isVailedJSON } from "@/utils/isVailedJSON"; import { isVailedJSON } from "@/utils/isVailedJSON";
import { SetAPIsTemplate } from "@/components/setAPIsTemplate"; import { SetAPIsTemplate } from "@/components/setAPIsTemplate";
import { autoHeight } from "@/utils/textAreaHelp"; import { autoHeight } from "@/utils/textAreaHelp";
import { getDefaultParams } from "@/utils/getDefaultParam";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
@@ -78,6 +77,7 @@ import { Slider } from "@/components/ui/slider";
import { NonOverflowScrollArea, ScrollArea } from "@/components/ui/scroll-area"; import { NonOverflowScrollArea, ScrollArea } from "@/components/ui/scroll-area";
import { AppChatStoreContext, AppContext } from "@/pages/App"; import { AppChatStoreContext, AppContext } from "@/pages/App";
import { toast } from "@/hooks/use-toast"; import { toast } from "@/hooks/use-toast";
import { TemplateAttributeDialog } from "@/components/TemplateAttributeDialog";
const TTS_VOICES: string[] = [ const TTS_VOICES: string[] = [
"alloy", "alloy",
@@ -153,10 +153,7 @@ const SelectModel = (props: { help: string }) => {
value={chatStore.model} value={chatStore.model}
onValueChange={(model: string) => { onValueChange={(model: string) => {
chatStore.model = model; chatStore.model = model;
chatStore.maxTokens = getDefaultParams( chatStore.maxTokens = models[model].maxToken;
"max",
models[model].maxToken
);
setChatStore({ ...chatStore }); setChatStore({ ...chatStore });
}} }}
> >
@@ -743,6 +740,7 @@ export default (props: {}) => {
// @ts-ignore // @ts-ignore
const { langCode, setLangCode } = useContext(langCodeContext); const { langCode, setLangCode } = useContext(langCodeContext);
const [open, setOpen] = useState<boolean>(false); const [open, setOpen] = useState<boolean>(false);
const [showTemplateDialog, setShowTemplateDialog] = useState(false);
useEffect(() => { useEffect(() => {
themeChange(false); themeChange(false);
@@ -808,7 +806,7 @@ export default (props: {}) => {
<LongInput <LongInput
label="System Prompt" label="System Prompt"
field="systemMessageContent" field="systemMessageContent"
help="系统消息用于指示ChatGPT的角色和一些前置条件例如“你是一个有帮助的人工智能助理”或者“你是一个专业英语翻译把我的话全部翻译成英语”详情参考 OPEAN AI API 文档" help="System prompt, used to indicate the role of ChatGPT and some preconditions, such as 'You are a helpful AI assistant' or 'You are a professional English translator, translate my words into English', please refer to the OpenAI API documentation"
{...props} {...props}
/> />
@@ -1055,21 +1053,7 @@ export default (props: {}) => {
<Button <Button
variant="outline" variant="outline"
className="w-full" className="w-full"
onClick={() => { onClick={() => setShowTemplateDialog(true)}
const name = prompt(
tr("Give this template a name:", langCode)
);
if (!name) {
alert(tr("No template name specified", langCode));
return;
}
const tmp: ChatStore = structuredClone(chatStore);
tmp.history = tmp.history.filter((h) => h.example);
// @ts-ignore
tmp.name = name;
templates.push(tmp as TemplateChatStore);
setTemplates([...templates]);
}}
> >
<Tr>As template</Tr> <Tr>As template</Tr>
</Button> </Button>
@@ -1598,6 +1582,25 @@ export default (props: {}) => {
</div> </div>
</NonOverflowScrollArea> </NonOverflowScrollArea>
</SheetContent> </SheetContent>
<TemplateAttributeDialog
open={showTemplateDialog}
chatStore={chatStore}
langCode={langCode}
onClose={() => setShowTemplateDialog(false)}
onSave={(name, selectedAttributes) => {
const tmp: ChatStore = {
...chatStore,
...selectedAttributes,
history: chatStore.history.filter((h) => h.example),
};
// @ts-ignore
tmp.name = name;
templates.push(tmp as TemplateChatStore);
setTemplates([...templates]);
setShowTemplateDialog(false);
}}
/>
</Sheet> </Sheet>
); );
}; };

View File

@@ -0,0 +1,187 @@
import { useState } from "react";
import { ChatStore } from "@/types/chatstore";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Label } from "@/components/ui/label";
import { ControlledInput } from "@/components/ui/controlled-input";
import { tr } from "@/translate";
interface TemplateAttributeDialogProps {
chatStore: ChatStore;
onSave: (name: string, selectedAttributes: Partial<ChatStore>) => void;
onClose: () => void;
open: boolean;
langCode: "en-US" | "zh-CN";
}
export function TemplateAttributeDialog({
chatStore,
onSave,
onClose,
open,
langCode,
}: TemplateAttributeDialogProps) {
// Create a map of all ChatStore attributes and their selection state
const [selectedAttributes, setSelectedAttributes] = useState<
Record<string, boolean>
>(() => {
const initial: Record<string, boolean> = {};
// Initialize all attributes as selected by default
Object.keys(chatStore).forEach((key) => {
initial[key] = true;
});
return initial;
});
const [templateName, setTemplateName] = useState("");
const [nameError, setNameError] = useState("");
const handleSave = () => {
// Validate name
if (!templateName.trim()) {
setNameError(tr("Template name is required", langCode));
return;
}
setNameError("");
// Create a new object with only the selected attributes
const filteredStore = {} as Partial<ChatStore>;
Object.entries(selectedAttributes).forEach(([key, isSelected]) => {
if (isSelected) {
const typedKey = key as keyof ChatStore;
// Use type assertion to ensure type safety
(filteredStore as any)[typedKey] = chatStore[typedKey];
}
});
onSave(templateName, structuredClone(filteredStore));
};
const toggleAll = (checked: boolean) => {
const newSelected = { ...selectedAttributes };
Object.keys(newSelected).forEach((key) => {
newSelected[key] = checked;
});
setSelectedAttributes(newSelected);
};
const formatValue = (value: any): string => {
if (value === null || value === undefined) return "null";
if (typeof value === "object") {
if (Array.isArray(value)) {
return `[${value.length} items]`;
}
return "{...}";
}
if (typeof value === "string") {
if (value.length > 50) {
return value.substring(0, 47) + "...";
}
return value;
}
return String(value);
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Select Template Attributes</DialogTitle>
<DialogDescription>
Choose which attributes to include in your template. Unselected
attributes will be omitted.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="template-name">Template Name</Label>
<ControlledInput
id="template-name"
value={templateName}
onChange={(e) => {
setTemplateName(e.target.value);
setNameError("");
}}
placeholder={tr("Enter template name", langCode)}
/>
{nameError && <p className="text-sm text-red-500">{nameError}</p>}
</div>
<div className="flex items-center space-x-2">
<Checkbox
id="select-all"
checked={Object.values(selectedAttributes).every((v) => v)}
onCheckedChange={(checked) => toggleAll(checked as boolean)}
/>
<Label htmlFor="select-all">Select All</Label>
</div>
<ScrollArea className="h-[400px] rounded-md border p-4">
<div className="grid grid-cols-1 gap-4">
{Object.keys(chatStore).map((key) => (
<div key={key} className="flex items-center space-x-2">
<Checkbox
id={key}
checked={selectedAttributes[key]}
onCheckedChange={(checked) =>
setSelectedAttributes((prev) => ({
...prev,
[key]: checked as boolean,
}))
}
/>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div className="flex flex-col">
<Label htmlFor={key} className="text-sm">
{key}
</Label>
<span className="text-xs text-muted-foreground">
{formatValue(chatStore[key as keyof ChatStore])}
</span>
</div>
</TooltipTrigger>
<TooltipContent>
<p className="max-w-xs break-words">
{JSON.stringify(
chatStore[key as keyof ChatStore],
null,
2
)}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
))}
</div>
</ScrollArea>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button onClick={handleSave}>Save Template</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -1,7 +1,6 @@
import { AppChatStoreContext, AppContext } from "@/pages/App"; import { AppChatStoreContext, AppContext } from "@/pages/App";
import { TemplateChatStore } from "@/types/chatstore"; import { TemplateChatStore } from "@/types/chatstore";
import { ChatStore } from "@/types/chatstore"; import { ChatStore } from "@/types/chatstore";
import { getDefaultParams } from "@/utils/getDefaultParam";
import { useContext } from "react"; import { useContext } from "react";
const Templates = () => { const Templates = () => {
@@ -18,51 +17,6 @@ const Templates = () => {
const newChatStore: ChatStore = structuredClone(t); const newChatStore: ChatStore = structuredClone(t);
// @ts-ignore // @ts-ignore
delete newChatStore.name; delete newChatStore.name;
if (!newChatStore.apiEndpoint) {
newChatStore.apiEndpoint = getDefaultParams(
"api",
chatStore.apiEndpoint
);
}
if (!newChatStore.apiKey) {
newChatStore.apiKey = getDefaultParams("key", chatStore.apiKey);
}
if (!newChatStore.whisper_api) {
newChatStore.whisper_api = getDefaultParams(
"whisper-api",
chatStore.whisper_api
);
}
if (!newChatStore.whisper_key) {
newChatStore.whisper_key = getDefaultParams(
"whisper-key",
chatStore.whisper_key
);
}
if (!newChatStore.tts_api) {
newChatStore.tts_api = getDefaultParams(
"tts-api",
chatStore.tts_api
);
}
if (!newChatStore.tts_key) {
newChatStore.tts_key = getDefaultParams(
"tts-key",
chatStore.tts_key
);
}
if (!newChatStore.image_gen_api) {
newChatStore.image_gen_api = getDefaultParams(
"image-gen-api",
chatStore.image_gen_api
);
}
if (!newChatStore.image_gen_key) {
newChatStore.image_gen_key = getDefaultParams(
"image-gen-key",
chatStore.image_gen_key
);
}
newChatStore.cost = 0; newChatStore.cost = 0;
// manage undefined value because of version update // manage undefined value because of version update

View File

@@ -14,6 +14,7 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { AppChatStoreContext, AppContext } from "../pages/App"; import { AppChatStoreContext, AppContext } from "../pages/App";
import { ConfirmationDialog } from "./ui/confirmation-dialog";
interface EditMessageProps { interface EditMessageProps {
chat: ChatStoreMessage; chat: ChatStoreMessage;
@@ -22,9 +23,19 @@ interface EditMessageProps {
} }
export function EditMessage(props: EditMessageProps) { export function EditMessage(props: EditMessageProps) {
const { chatStore, setChatStore } = useContext(AppChatStoreContext); const { chatStore, setChatStore } = useContext(AppChatStoreContext);
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
const { showEdit, setShowEdit, chat } = props; const { showEdit, setShowEdit, chat } = props;
const handleSwitchMessageType = () => {
if (typeof chat.content === "string") {
chat.content = [];
} else {
chat.content = "";
}
setChatStore({ ...chatStore });
};
return ( return (
<Dialog open={showEdit} onOpenChange={setShowEdit}> <Dialog open={showEdit} onOpenChange={setShowEdit}>
{/* <DialogTrigger> {/* <DialogTrigger>
@@ -46,19 +57,7 @@ export function EditMessage(props: EditMessageProps) {
<Button <Button
variant="destructive" variant="destructive"
className="w-full" className="w-full"
onClick={() => { onClick={() => setShowConfirmDialog(true)}
const confirm = window.confirm(
"Change message type will clear the content, are you sure?"
);
if (!confirm) return;
if (typeof chat.content === "string") {
chat.content = [];
} else {
chat.content = "";
}
setChatStore({ ...chatStore });
}}
> >
Switch to{" "} Switch to{" "}
{typeof chat.content === "string" {typeof chat.content === "string"
@@ -68,6 +67,13 @@ export function EditMessage(props: EditMessageProps) {
)} )}
<Button onClick={() => setShowEdit(false)}>Close</Button> <Button onClick={() => setShowEdit(false)}>Close</Button>
</DialogContent> </DialogContent>
<ConfirmationDialog
isOpen={showConfirmDialog}
onClose={() => setShowConfirmDialog(false)}
onConfirm={handleSwitchMessageType}
title="Switch Message Type"
description="Change message type will clear the content, are you sure?"
/>
</Dialog> </Dialog>
); );
} }

View File

@@ -0,0 +1,48 @@
import React from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
DialogDescription,
} from "./dialog";
import { Button } from "./button";
interface ConfirmationDialogProps {
isOpen: boolean;
onClose: () => void;
onConfirm: () => void;
title: string;
description: string;
}
export function ConfirmationDialog({
isOpen,
onClose,
onConfirm,
title,
description
}: ConfirmationDialogProps) {
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
Cancel
</Button>
<Button variant="destructive" onClick={() => {
onConfirm();
onClose();
}}>
Confirm
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,21 @@
import { ComponentProps, forwardRef } from "react";
import { cn } from "@/lib/utils";
const ControlledInput = forwardRef<HTMLInputElement, ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
className
)}
ref={ref}
{...props}
/>
);
}
);
ControlledInput.displayName = "ControlledInput";
export { ControlledInput };

View File

@@ -1,3 +1,5 @@
@import "highlight.js/styles/monokai.css";
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;

View File

@@ -1,5 +1,5 @@
import { IDBPDatabase, openDB } from "idb"; import { IDBPDatabase, openDB } from "idb";
import { createContext, useContext, useEffect, useState } from "react"; import { createContext, useContext, useEffect, useState, useRef } from "react"; // 添加了useRef
import "@/global.css"; import "@/global.css";
import { calculate_token_length } from "@/chatgpt"; import { calculate_token_length } from "@/chatgpt";
@@ -48,6 +48,7 @@ interface AppContextType {
defaultRenderMD: boolean; defaultRenderMD: boolean;
setDefaultRenderMD: (b: boolean) => void; setDefaultRenderMD: (b: boolean) => void;
handleNewChatStore: () => Promise<void>; handleNewChatStore: () => Promise<void>;
handleNewChatStoreWithOldOne: (chatStore: ChatStore) => Promise<void>;
} }
interface AppChatStoreContextType { interface AppChatStoreContextType {
@@ -96,6 +97,7 @@ import Search from "@/components/Search";
import Navbar from "@/components/navbar"; import Navbar from "@/components/navbar";
import ConversationTitle from "@/components/ConversationTitle."; import ConversationTitle from "@/components/ConversationTitle.";
import ImportDialog from "@/components/ImportDialog";
export function App() { export function App() {
// init selected index // init selected index
@@ -140,10 +142,6 @@ export function App() {
} }
if (ret.cost === undefined) ret.cost = 0; if (ret.cost === undefined) ret.cost = 0;
toast({
title: "Chat ready",
description: `Current API Endpoint: ${ret.apiEndpoint}`,
});
return ret; return ret;
}; };
@@ -198,9 +196,34 @@ export function App() {
window.location.reload(); window.location.reload();
}; };
// if there are any params in URL, create a new chatStore const [showImportDialog, setShowImportDialog] = useState(false);
// if there are any params in URL, show the alert dialog to import configure
useEffect(() => { useEffect(() => {
const run = async () => { const run = async () => {
const params = new URLSearchParams(window.location.search);
if (
params.get("api") ||
params.get("key") ||
params.get("sys") ||
params.get("mode") ||
params.get("model") ||
params.get("max") ||
params.get("temp") ||
params.get("dev") ||
params.get("whisper-api") ||
params.get("whisper-key") ||
params.get("tts-api") ||
params.get("tts-key")
) {
setShowImportDialog(true);
}
await db;
const allidx = await (await db).getAllKeys(STORAGE_NAME);
if (allidx.length === 0) {
handleNewChatStore();
}
setAllChatStoreIndexes(await (await db).getAllKeys(STORAGE_NAME));
/*
const chatStore = await getChatStoreByIndex(selectedChatIndex); const chatStore = await getChatStoreByIndex(selectedChatIndex);
const api = getDefaultParams("api", ""); const api = getDefaultParams("api", "");
const key = getDefaultParams("key", ""); const key = getDefaultParams("key", "");
@@ -222,12 +245,7 @@ export function App() {
console.log("create new chatStore because of params in URL"); console.log("create new chatStore because of params in URL");
handleNewChatStoreWithOldOne(chatStore); handleNewChatStoreWithOldOne(chatStore);
} }
await db; */
const allidx = await (await db).getAllKeys(STORAGE_NAME);
if (allidx.length === 0) {
handleNewChatStore();
}
setAllChatStoreIndexes(await (await db).getAllKeys(STORAGE_NAME));
}; };
run(); run();
}, []); }, []);
@@ -332,6 +350,7 @@ export function App() {
defaultRenderMD, defaultRenderMD,
setDefaultRenderMD, setDefaultRenderMD,
handleNewChatStore, handleNewChatStore,
handleNewChatStoreWithOldOne,
}} }}
> >
<Sidebar> <Sidebar>
@@ -410,6 +429,7 @@ export function App() {
selectedChatIndex={selectedChatIndex} selectedChatIndex={selectedChatIndex}
getChatStoreByIndex={getChatStoreByIndex} getChatStoreByIndex={getChatStoreByIndex}
> >
<ImportDialog open={showImportDialog} setOpen={setShowImportDialog} />
<Navbar /> <Navbar />
<ChatBOX /> <ChatBOX />
</AppChatStoreProvider> </AppChatStoreProvider>
@@ -427,9 +447,78 @@ const AppChatStoreProvider = ({
selectedChatIndex: number; selectedChatIndex: number;
getChatStoreByIndex: (index: number) => Promise<ChatStore>; getChatStoreByIndex: (index: number) => Promise<ChatStore>;
}) => { }) => {
console.log("[Render] AppChatStoreProvider");
const ctx = useContext(AppContext); const ctx = useContext(AppContext);
const { toast } = useToast();
const tabId = useRef<string>(Math.random().toString(36).substr(2, 9)).current;
useEffect(() => {
const channel = new BroadcastChannel("chat-store-access");
// 页面激活状态处理
const handleVisibilityChange = () => {
if (document.visibilityState === "visible") {
channel.postMessage({
type: "open",
index: selectedChatIndex,
tabId,
});
}
};
document.addEventListener("visibilitychange", handleVisibilityChange);
// 消息处理逻辑
const handleMessage = (event: MessageEvent) => {
// 忽略自身消息和无关索引消息
if (event.data.tabId === tabId) return;
if (event.data.index !== selectedChatIndex) return;
// 根据消息类型处理
switch (event.data.type) {
case "open":
// 收到open消息时发送确认回复并显示警告
channel.postMessage({
type: "ack",
index: selectedChatIndex,
tabId,
});
showConflictWarning();
break;
case "ack":
// 收到确认回复时显示警告
showConflictWarning();
break;
}
};
// 立即发送初始查询
channel.postMessage({
type: "open",
index: selectedChatIndex,
tabId,
});
// 绑定事件监听器
channel.addEventListener("message", handleMessage);
// 清理函数
return () => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
channel.removeEventListener("message", handleMessage);
channel.close();
};
}, [selectedChatIndex, toast, tabId]);
// 警告提示统一处理
const showConflictWarning = () => {
toast({
title: "访问冲突警告",
description: "当前会话已在其他浏览器标签打开, 请注意数据一致性!",
variant: "destructive",
duration: 8000,
});
};
const [chatStore, _setChatStore] = useState(newChatStore({})); const [chatStore, _setChatStore] = useState(newChatStore({}));
const setChatStore = async (chatStore: ChatStore) => { const setChatStore = async (chatStore: ChatStore) => {
console.log("recalculate postBeginIndex"); console.log("recalculate postBeginIndex");

View File

@@ -40,9 +40,32 @@ import { Switch } from "@/components/ui/switch";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { AppChatStoreContext, AppContext } from "./App"; import { AppChatStoreContext, AppContext } from "./App";
import APIListMenu from "@/components/ListAPI";
import { ImageGenDrawer } from "@/components/ImageGenDrawer"; import { ImageGenDrawer } from "@/components/ImageGenDrawer";
import { abort } from "process";
const createMessageFromCurrentBuffer = (
chunkMessages: string[],
reasoningChunks: string[],
tools: ToolCall[],
response_count: number
): ChatStoreMessage => {
return {
role: "assistant",
content: chunkMessages.join(""),
reasoning_content: reasoningChunks.join(""),
tool_calls: tools.length > 0 ? tools : undefined,
// 补全其他必填字段的默认值(根据你的类型定义)
hide: false,
token: calculate_token_length(
chunkMessages.join("") + reasoningChunks.join("")
), // 需要实际的token计算逻辑
example: false,
audio: null,
logprobs: null,
response_model_name: null,
usage: null,
response_count,
};
};
export default function ChatBOX() { export default function ChatBOX() {
const { db, selectedChatIndex, setSelectedChatIndex, handleNewChatStore } = const { db, selectedChatIndex, setSelectedChatIndex, handleNewChatStore } =
@@ -93,78 +116,105 @@ export default function ChatBOX() {
}; };
let response_model_name: string | null = null; let response_model_name: string | null = null;
let usage: Usage | null = null; let usage: Usage | null = null;
for await (const i of client.processStreamResponse(response, signal)) {
if (signal?.aborted) break;
response_model_name = i.model;
responseTokenCount += 1;
if (i.usage) {
usage = i.usage;
}
const c = i.choices[0]; try {
for await (const i of client.processStreamResponse(response, signal)) {
// skip if choice is empty (e.g. azure) if (signal?.aborted) break;
if (!c) continue; response_model_name = i.model;
responseTokenCount += 1;
const logprob = c?.logprobs?.content[0]?.logprob; if (i.usage) {
if (logprob !== undefined) { usage = i.usage;
logprobs.content.push({
token: c?.delta?.content ?? "",
logprob,
});
console.log(c?.delta?.content, logprob);
}
if (c?.delta?.content) {
allChunkMessage.push(c?.delta?.content ?? "");
}
if (c?.delta?.reasoning_content) {
allReasoningContentChunk.push(c?.delta?.reasoning_content ?? "");
}
const tool_calls = c?.delta?.tool_calls;
if (tool_calls) {
for (const tool_call of tool_calls) {
// init
if (tool_call.id) {
allChunkTool.push({
id: tool_call.id,
type: tool_call.type,
index: tool_call.index,
function: {
name: tool_call.function.name,
arguments: "",
},
});
continue;
}
// update tool call arguments
const tool = allChunkTool.find(
(tool) => tool.index === tool_call.index
);
if (!tool) {
console.log("tool (by index) not found", tool_call.index);
continue;
}
tool.function.arguments += tool_call.function.arguments;
} }
const c = i.choices[0];
// skip if choice is empty (e.g. azure)
if (!c) continue;
const logprob = c?.logprobs?.content[0]?.logprob;
if (logprob !== undefined) {
logprobs.content.push({
token: c?.delta?.content ?? "",
logprob,
});
console.log(c?.delta?.content, logprob);
}
if (c?.delta?.content) {
allChunkMessage.push(c?.delta?.content ?? "");
}
if (c?.delta?.reasoning_content) {
allReasoningContentChunk.push(c?.delta?.reasoning_content ?? "");
}
const tool_calls = c?.delta?.tool_calls;
if (tool_calls) {
for (const tool_call of tool_calls) {
// init
if (tool_call.id) {
allChunkTool.push({
id: tool_call.id,
type: tool_call.type,
index: tool_call.index,
function: {
name: tool_call.function.name,
arguments: "",
},
});
continue;
}
// update tool call arguments
const tool = allChunkTool.find(
(tool) => tool.index === tool_call.index
);
if (!tool) {
console.log("tool (by index) not found", tool_call.index);
continue;
}
tool.function.arguments += tool_call.function.arguments;
}
}
setGeneratingMessage(
(allReasoningContentChunk.length
? "----------\nreasoning:\n" +
allReasoningContentChunk.join("") +
"\n----------\n"
: "") +
allChunkMessage.join("") +
allChunkTool.map((tool) => {
return `Tool Call ID: ${tool.id}\nType: ${tool.type}\nFunction: ${tool.function.name}\nArguments: ${tool.function.arguments}`;
}) +
"\n" +
responseTokenCount +
" response count"
);
} }
setGeneratingMessage( } catch (e: any) {
(allReasoningContentChunk.length if (e.name === "AbortError") {
? "----------\nreasoning:\n" + // 1. 立即保存当前buffer中的内容
allReasoningContentChunk.join("") + if (allChunkMessage.length > 0 || allReasoningContentChunk.length > 0) {
"\n----------\n" const partialMsg = createMessageFromCurrentBuffer(
: "") + allChunkMessage,
allChunkMessage.join("") + allReasoningContentChunk,
allChunkTool.map((tool) => { allChunkTool,
return `Tool Call ID: ${tool.id}\nType: ${tool.type}\nFunction: ${tool.function.name}\nArguments: ${tool.function.arguments}`; responseTokenCount
}) );
); chatStore.history.push(partialMsg);
setChatStore({ ...chatStore });
}
// 2. 不隐藏错误,重新抛出给上层
throw e;
}
// 其他错误直接抛出
throw e;
} finally {
setShowGenerating(false);
setGeneratingMessage("");
} }
setShowGenerating(false);
const content = allChunkMessage.join(""); const content = allChunkMessage.join("");
const reasoning_content = allReasoningContentChunk.join(""); const reasoning_content = allReasoningContentChunk.join("");
@@ -210,7 +260,15 @@ export default function ChatBOX() {
audio: null, audio: null,
logprobs, logprobs,
response_model_name, response_model_name,
usage, usage: usage ?? {
prompt_tokens: prompt_tokens,
completion_tokens: responseTokenCount,
total_tokens: prompt_tokens + responseTokenCount,
response_model_name: response_model_name,
prompt_tokens_details: null,
completion_tokens_details: null,
},
response_count: responseTokenCount,
}; };
if (allChunkTool.length > 0) newMsg.tool_calls = allChunkTool; if (allChunkTool.length > 0) newMsg.tool_calls = allChunkTool;
@@ -558,21 +616,6 @@ export default function ChatBOX() {
<Tr>New Chat</Tr> <Tr>New Chat</Tr>
</Button> </Button>
)} )}
{showGenerating && (
<Button
size="sm"
className="ml-auto gap-1.5"
variant="destructive"
onClick={() => {
abortControllerRef.current.abort();
setShowGenerating(false);
setGeneratingMessage("");
}}
>
<Tr>Stop Generating</Tr>
<ScissorsIcon className="size-3.5" />
</Button>
)}
{chatStore.develop_mode && chatStore.history.length > 0 && ( {chatStore.develop_mode && chatStore.history.length > 0 && (
<Button <Button
variant="outline" variant="outline"
@@ -633,15 +676,32 @@ export default function ChatBOX() {
</div> </div>
<div className="sticky bottom-0 w-full z-20 bg-background"> <div className="sticky bottom-0 w-full z-20 bg-background">
{generatingMessage && ( {generatingMessage && (
<div className="flex items-center justify-end gap-2 p-2 m-2 rounded bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"> <div className="flex items-center justify-between gap-2 p-2 m-2 rounded bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"> <div className="flex items-center gap-2">
Follow <label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
</label> <Tr>Follow</Tr>
<Switch </label>
checked={follow} <Switch
onCheckedChange={setFollow} checked={follow}
aria-label="Toggle auto-scroll" onCheckedChange={setFollow}
/> aria-label="Toggle auto-scroll"
/>
</div>
<div className="flex items-center gap-2">
<Button
size="sm"
className="ml-auto gap-1.5"
variant="destructive"
onClick={() => {
abortControllerRef.current.abort();
setShowGenerating(false);
setGeneratingMessage("");
}}
>
<Tr>Stop Generating</Tr>
<ScissorsIcon className="size-3.5" />
</Button>
</div>
</div> </div>
)} )}
<form className="relative rounded-lg border bg-background focus-within:ring-1 focus-within:ring-ring p-1"> <form className="relative rounded-lg border bg-background focus-within:ring-1 focus-within:ring-ring p-1">

View File

@@ -50,7 +50,9 @@ function tr(text: string, langCode: "en-US" | "zh-CN") {
const translatedText = langMap[text.toLowerCase()]; const translatedText = langMap[text.toLowerCase()];
if (translatedText === undefined) { if (translatedText === undefined) {
console.log(`[Translation] not found for "${text}"`); if (langCode !== "en-US") {
console.log(`[Translation] not found for "${text}"`);
}
return text; return text;
} }

View File

@@ -141,6 +141,13 @@ const LANG_MAP: Record<string, string> = {
"Configure image generation settings": "配置图片生成设置", "Configure image generation settings": "配置图片生成设置",
"New Chat": "新对话", "New Chat": "新对话",
"Delete Chat": "删除对话", "Delete Chat": "删除对话",
"removed from context": "已从上下文中移除",
follow: "跟随",
"stop generating": "停止生成",
"there are some configurations in the URL, import them?":
"URL 中有一些配置,是否导入?",
"Import Configuration": "导入配置",
cancel: "取消",
}; };
export default LANG_MAP; export default LANG_MAP;

View File

@@ -76,6 +76,7 @@ export interface ChatStoreMessage {
created_at?: string; created_at?: string;
responsed_at?: string; responsed_at?: string;
completed_at?: string; completed_at?: string;
response_count?: number;
role: "system" | "user" | "assistant" | "tool"; role: "system" | "user" | "assistant" | "tool";
content: string | MessageDetail[]; content: string | MessageDetail[];

View File

@@ -3,8 +3,7 @@ import {
DefaultModel, DefaultModel,
CHATGPT_API_WEB_VERSION, CHATGPT_API_WEB_VERSION,
} from "@/const"; } from "@/const";
import { getDefaultParams } from "@/utils/getDefaultParam"; import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
import { ChatStore } from "@/types/chatstore";
import { models } from "@/types/models"; import { models } from "@/types/models";
interface NewChatStoreOptions { interface NewChatStoreOptions {
@@ -36,38 +35,30 @@ interface NewChatStoreOptions {
json_mode?: boolean; json_mode?: boolean;
logprobs?: boolean; logprobs?: boolean;
maxTokens?: number; maxTokens?: number;
use_this_history?: ChatStoreMessage[];
} }
export const newChatStore = (options: NewChatStoreOptions): ChatStore => { export const newChatStore = (options: NewChatStoreOptions): ChatStore => {
return { return {
chatgpt_api_web_version: CHATGPT_API_WEB_VERSION, chatgpt_api_web_version: CHATGPT_API_WEB_VERSION,
systemMessageContent: getDefaultParams( systemMessageContent: options.systemMessageContent ?? "",
"sys",
options.systemMessageContent ?? ""
),
toolsString: options.toolsString ?? "", toolsString: options.toolsString ?? "",
history: [], history: options.use_this_history ?? [],
postBeginIndex: 0, postBeginIndex: 0,
tokenMargin: 1024, tokenMargin: 1024,
totalTokens: 0, totalTokens: 0,
maxTokens: getDefaultParams( maxTokens:
"max", models[options.model ?? DefaultModel]?.maxToken ??
models[getDefaultParams("model", options.model ?? DefaultModel)] options.maxTokens ??
?.maxToken ?? 2048,
options.maxTokens ??
2048
),
maxGenTokens: 2048, maxGenTokens: 2048,
maxGenTokens_enabled: false, maxGenTokens_enabled: false,
apiKey: getDefaultParams("key", options.apiKey ?? ""), apiKey: options.apiKey ?? "",
apiEndpoint: getDefaultParams( apiEndpoint: options.apiEndpoint ?? DefaultAPIEndpoint,
"api", streamMode: options.streamMode ?? true,
options.apiEndpoint ?? DefaultAPIEndpoint model: options.model ?? DefaultModel,
),
streamMode: getDefaultParams("mode", options.streamMode ?? true),
model: getDefaultParams("model", options.model ?? DefaultModel),
cost: 0, cost: 0,
temperature: getDefaultParams("temp", options.temperature ?? 1), temperature: options.temperature ?? 1,
temperature_enabled: options.temperature_enabled ?? true, temperature_enabled: options.temperature_enabled ?? true,
top_p: options.top_p ?? 1, top_p: options.top_p ?? 1,
top_p_enabled: options.top_p_enabled ?? false, top_p_enabled: options.top_p_enabled ?? false,
@@ -75,17 +66,12 @@ export const newChatStore = (options: NewChatStoreOptions): ChatStore => {
presence_penalty_enabled: options.presence_penalty_enabled ?? false, presence_penalty_enabled: options.presence_penalty_enabled ?? false,
frequency_penalty: options.frequency_penalty ?? 0, frequency_penalty: options.frequency_penalty ?? 0,
frequency_penalty_enabled: options.frequency_penalty_enabled ?? false, frequency_penalty_enabled: options.frequency_penalty_enabled ?? false,
develop_mode: getDefaultParams("dev", options.dev ?? false), develop_mode: options.dev ?? false,
whisper_api: getDefaultParams( whisper_api:
"whisper-api", options.whisper_api ?? "https://api.openai.com/v1/audio/transcriptions",
options.whisper_api ?? "https://api.openai.com/v1/audio/transcriptions" whisper_key: options.whisper_key ?? "",
), tts_api: options.tts_api ?? "https://api.openai.com/v1/audio/speech",
whisper_key: getDefaultParams("whisper-key", options.whisper_key ?? ""), tts_key: options.tts_key ?? "",
tts_api: getDefaultParams(
"tts-api",
options.tts_api ?? "https://api.openai.com/v1/audio/speech"
),
tts_key: getDefaultParams("tts-key", options.tts_key ?? ""),
tts_voice: options.tts_voice ?? "alloy", tts_voice: options.tts_voice ?? "alloy",
tts_speed: options.tts_speed ?? 1.0, tts_speed: options.tts_speed ?? 1.0,
tts_speed_enabled: options.tts_speed_enabled ?? false, tts_speed_enabled: options.tts_speed_enabled ?? false,

View File

@@ -4,6 +4,13 @@ module.exports = {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: { theme: {
extend: { extend: {
typography: (theme) => ({
DEFAULT: {
css: {
},
},
}),
borderRadius: { borderRadius: {
lg: 'var(--radius)', lg: 'var(--radius)',
md: 'calc(var(--radius) - 2px)', md: 'calc(var(--radius) - 2px)',
@@ -85,5 +92,5 @@ module.exports = {
} }
} }
}, },
plugins: [require("tailwindcss-animate")], plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")],
}; };

5682
yarn.lock Normal file

File diff suppressed because it is too large Load Diff