Compare commits
32 Commits
79d5ded088
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
7793d94514
|
|||
|
24973eabfe
|
|||
|
6078d8a2c3
|
|||
|
d830b92fbf
|
|||
|
7694ed6792
|
|||
|
6b8426868a
|
|||
| e18dd9b680 | |||
|
13295bd24d
|
|||
|
aa83f10657
|
|||
| 95b319db7d | |||
|
8f24489959
|
|||
|
39c3860c78
|
|||
|
812ce3cc1f
|
|||
|
667b334dfc
|
|||
|
9b32948cfa
|
|||
|
9fbd9b98c2
|
|||
|
14df7bebac
|
|||
|
e4919bb91f
|
|||
|
2a39ff885a
|
|||
|
c03dbef798
|
|||
|
8cd43bec72
|
|||
|
ed5f561148
|
|||
|
|
8d4a9b840a | ||
|
|
8db892caf7 | ||
|
|
d18040dca1 | ||
|
|
a5f7447f4f | ||
|
|
332a645e34 | ||
|
c37a99f06d
|
|||
|
|
3e89e88c1d | ||
|
5b4a0507ae
|
|||
|
75bf4a419d
|
|||
|
7dea556a56
|
@@ -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
90
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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
7654
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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) {
|
||||||
|
|||||||
129
src/components/ImportDialog.tsx
Normal file
129
src/components/ImportDialog.tsx
Normal 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;
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 &&
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
187
src/components/TemplateAttributeDialog.tsx
Normal file
187
src/components/TemplateAttributeDialog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
48
src/components/ui/confirmation-dialog.tsx
Normal file
48
src/components/ui/confirmation-dialog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
src/components/ui/controlled-input.tsx
Normal file
21
src/components/ui/controlled-input.tsx
Normal 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 };
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
@import "highlight.js/styles/monokai.css";
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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")],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user