Compare commits
29 Commits
79d5ded088
...
d830b92fbf
| Author | SHA1 | Date | |
|---|---|---|---|
|
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
|
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-group": "^1.1.1",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@types/ungap__structured-clone": "^1.2.0",
|
||||
@@ -47,6 +48,7 @@
|
||||
"cmdk": "1.0.4",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.5.1",
|
||||
"highlight.js": "^11.11.1",
|
||||
"idb": "^8.0.1",
|
||||
"input-otp": "^1.4.1",
|
||||
"lucide-react": "^0.469.0",
|
||||
@@ -58,6 +60,7 @@
|
||||
"react-markdown": "^9.0.3",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"recharts": "^2.15.0",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-math": "^6.0.0",
|
||||
"sakura.css": "^1.5.0",
|
||||
@@ -3884,6 +3887,34 @@
|
||||
"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": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -6103,6 +6134,15 @@
|
||||
"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": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
|
||||
@@ -6875,6 +6915,12 @@
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"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": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
|
||||
@@ -6882,6 +6928,18 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "4.7.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz",
|
||||
@@ -6911,6 +6969,21 @@
|
||||
"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": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
@@ -8534,6 +8607,23 @@
|
||||
"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": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@heroicons/react": "^2.2.0",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@monaco-editor/react": "^4.7.0",
|
||||
"@radix-ui/react-accordion": "^1.2.2",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.4",
|
||||
"@radix-ui/react-aspect-ratio": "^1.1.1",
|
||||
@@ -38,6 +39,7 @@
|
||||
"@radix-ui/react-toggle": "^1.1.1",
|
||||
"@radix-ui/react-toggle-group": "^1.1.1",
|
||||
"@radix-ui/react-tooltip": "^1.1.6",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@types/react": "^18.0.0",
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@types/ungap__structured-clone": "^1.2.0",
|
||||
@@ -48,6 +50,7 @@
|
||||
"cmdk": "1.0.4",
|
||||
"date-fns": "^4.1.0",
|
||||
"embla-carousel-react": "^8.5.1",
|
||||
"highlight.js": "^11.11.1",
|
||||
"idb": "^8.0.1",
|
||||
"input-otp": "^1.4.1",
|
||||
"lucide-react": "^0.469.0",
|
||||
@@ -59,6 +62,7 @@
|
||||
"react-markdown": "^9.0.3",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"recharts": "^2.15.0",
|
||||
"rehype-highlight": "^7.0.2",
|
||||
"rehype-katex": "^7.0.1",
|
||||
"remark-math": "^6.0.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
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 { ChatStore, TemplateAPI, TemplateChatStore } from "@/types/chatstore";
|
||||
import { Tr } from "@/translate";
|
||||
|
||||
import React, { useContext, useState, useRef } from "react";
|
||||
import {
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuTrigger,
|
||||
} from "@/components/ui/navigation-menu";
|
||||
ChatStore,
|
||||
TemplateAPI,
|
||||
TemplateChatStore,
|
||||
TemplateTools,
|
||||
} from "@/types/chatstore";
|
||||
import { Tr } from "@/translate";
|
||||
import Editor from "@monaco-editor/react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useContext } from "react";
|
||||
import { AppChatStoreContext, AppContext } from "@/pages/App";
|
||||
import {
|
||||
NavigationMenu,
|
||||
@@ -37,14 +36,17 @@ import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTrigger,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
} from "./ui/dialog";
|
||||
import { DialogTitle } from "@radix-ui/react-dialog";
|
||||
import { Textarea } from "./ui/textarea";
|
||||
import { Label } from "./ui/label";
|
||||
import { Input } from "./ui/input";
|
||||
import { SetAPIsTemplate } from "./setAPIsTemplate";
|
||||
import { isVailedJSON } from "@/utils/isVailedJSON";
|
||||
import { toast } from 'sonner';
|
||||
import { ConfirmationDialog } from "./ui/confirmation-dialog";
|
||||
|
||||
interface APITemplateDropdownProps {
|
||||
label: string;
|
||||
@@ -52,6 +54,81 @@ interface APITemplateDropdownProps {
|
||||
apiField: 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({
|
||||
label,
|
||||
shortLabel,
|
||||
@@ -72,73 +149,147 @@ function APIsDropdownList({
|
||||
setTemplateAPIsWhisper,
|
||||
setTemplateTools,
|
||||
} = 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 setAPI = setTemplateAPIs;
|
||||
if (label === "Chat API") {
|
||||
API = templateAPIs;
|
||||
setAPI = setTemplateAPIs;
|
||||
} else if (label === "Whisper API") {
|
||||
API = templateAPIsWhisper;
|
||||
setAPI = setTemplateAPIsWhisper;
|
||||
} else if (label === "TTS API") {
|
||||
API = templateAPIsTTS;
|
||||
setAPI = setTemplateAPIsTTS;
|
||||
} else if (label === "Image Gen API") {
|
||||
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 (
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger>
|
||||
<span className="lg:hidden">{shortLabel}</span>
|
||||
<span className="hidden lg:inline">
|
||||
{label}{" "}
|
||||
{API.find(
|
||||
(t: TemplateAPI) =>
|
||||
chatStore[apiField as keyof ChatStore] === t.endpoint &&
|
||||
chatStore[keyField as keyof ChatStore] === t.key
|
||||
)?.name &&
|
||||
`: ${
|
||||
API.find(
|
||||
(t: TemplateAPI) =>
|
||||
chatStore[apiField as keyof ChatStore] === t.endpoint &&
|
||||
chatStore[keyField as keyof ChatStore] === t.key
|
||||
)?.name
|
||||
}`}
|
||||
</span>
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px] ">
|
||||
{API.map((t: TemplateAPI, index: number) => (
|
||||
<li key={index}>
|
||||
<NavigationMenuLink asChild>
|
||||
<a
|
||||
onClick={() => {
|
||||
// @ts-ignore
|
||||
chatStore[apiField as keyof ChatStore] = t.endpoint;
|
||||
// @ts-ignore
|
||||
chatStore[keyField] = t.key;
|
||||
setChatStore({
|
||||
...chatStore,
|
||||
});
|
||||
}}
|
||||
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",
|
||||
chatStore[apiField as keyof ChatStore] === t.endpoint &&
|
||||
chatStore[keyField as keyof ChatStore] === t.key
|
||||
? "bg-accent text-accent-foreground"
|
||||
: ""
|
||||
)}
|
||||
>
|
||||
<div className="text-sm font-medium leading-none">
|
||||
{t.name}
|
||||
</div>
|
||||
<p className="line-clamp-2 text-sm leading-snug text-muted-foreground">
|
||||
{new URL(t.endpoint).host}
|
||||
</p>
|
||||
</a>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
<div className="flex items-center space-x-4 mx-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<Tr>{label}</Tr>
|
||||
</p>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="w-[150px] justify-start">
|
||||
{API.find(
|
||||
(t: TemplateAPI) =>
|
||||
chatStore[apiField as keyof ChatStore] === t.endpoint &&
|
||||
chatStore[keyField as keyof ChatStore] === t.key
|
||||
)?.name || `+ ${shortLabel}`}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" side="bottom" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search template..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
<Tr>No results found.</Tr>
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{API.map((t: TemplateAPI, index: number) => (
|
||||
<CommandItem
|
||||
key={index}
|
||||
value={t.name}
|
||||
onSelect={() => {
|
||||
setChatStore({
|
||||
...chatStore,
|
||||
[apiField]: t.endpoint,
|
||||
[keyField]: t.key,
|
||||
});
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span>{t.name}</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEdit(t);
|
||||
}}
|
||||
>
|
||||
<EditIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(t);
|
||||
}}
|
||||
>
|
||||
<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>
|
||||
</CommandItem>
|
||||
)}
|
||||
{ctx.templateTools.map((t, index) => (
|
||||
{ctx.templateTools.map((t: TemplateTools, index: number) => (
|
||||
<CommandItem
|
||||
key={index}
|
||||
value={t.toolsString}
|
||||
@@ -218,170 +369,278 @@ function ToolsDropdownList() {
|
||||
);
|
||||
}
|
||||
|
||||
function ChatTemplateDropdownList() {
|
||||
const ctx = useContext(AppContext);
|
||||
interface EditChatTemplateDialogProps {
|
||||
template: TemplateChatStore;
|
||||
onSave: (updatedTemplate: TemplateChatStore) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
|
||||
const { templates, setTemplates } = useContext(AppContext);
|
||||
function EditChatTemplateDialog({ template, onSave, onClose }: EditChatTemplateDialogProps) {
|
||||
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 (
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger>
|
||||
<span className="lg:hidden">Chat Template</span>
|
||||
<span className="hidden lg:inline">Chat Template</span>
|
||||
</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<ul className="grid w-[400px] gap-3 p-4 md:w-[500px] md:grid-cols-2 lg:w-[600px]">
|
||||
{templates.map((t: TemplateChatStore, index: number) => (
|
||||
<ChatTemplateItem key={index} t={t} index={index} />
|
||||
))}
|
||||
</ul>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
<Dialog open onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-4xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Template</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="name">Template Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
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 = ({
|
||||
t,
|
||||
index,
|
||||
}: {
|
||||
t: TemplateChatStore;
|
||||
index: number;
|
||||
}) => {
|
||||
const [dialogOpen, setDialogOpen] = React.useState(false);
|
||||
function ChatTemplateDropdownList() {
|
||||
const ctx = useContext(AppContext);
|
||||
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
|
||||
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 (
|
||||
<li
|
||||
onClick={() => {
|
||||
// Update chatStore with the selected template
|
||||
if (chatStore.history.length > 0 || chatStore.systemMessageContent) {
|
||||
console.log("you clicked", t.name);
|
||||
const confirm = window.confirm(
|
||||
"This will replace the current chat history. Are you sure?"
|
||||
);
|
||||
if (!confirm) return;
|
||||
}
|
||||
setChatStore({ ...newChatStore({ ...chatStore, ...t }) });
|
||||
}}
|
||||
>
|
||||
<NavigationMenuLink asChild>
|
||||
<a
|
||||
className={cn(
|
||||
"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"
|
||||
)}
|
||||
>
|
||||
<div className="text-sm font-medium leading-non">{t.name}</div>
|
||||
<div onClick={(e) => e.stopPropagation()}>
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<EditIcon />
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit Template</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Label>Template Name</Label>
|
||||
<Input
|
||||
value={t.name}
|
||||
onBlur={(e) => {
|
||||
t.name = e.target.value;
|
||||
templates[index] = t;
|
||||
setTemplates([...templates]);
|
||||
}}
|
||||
/>
|
||||
<p>
|
||||
Raw JSON allows you to modify any content within the template.
|
||||
You can remove unnecessary fields, and non-existent fields
|
||||
will be inherited from the current session.
|
||||
</p>
|
||||
<Textarea
|
||||
className="h-64"
|
||||
value={JSON.stringify(t, null, 2)}
|
||||
onBlur={(e) => {
|
||||
try {
|
||||
const json = JSON.parse(
|
||||
e.target.value
|
||||
) as TemplateChatStore;
|
||||
json.name = t.name;
|
||||
templates[index] = json;
|
||||
setTemplates([...templates]);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert("Invalid JSON");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
variant={"destructive"}
|
||||
onClick={() => {
|
||||
let confirm = window.confirm(
|
||||
"Are you sure you want to delete this template?"
|
||||
);
|
||||
if (!confirm) return;
|
||||
templates.splice(index, 1);
|
||||
setTemplates([...templates]);
|
||||
setDialogOpen(false);
|
||||
}}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Button type="submit" onClick={() => setDialogOpen(false)}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</a>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
<div className="flex items-center space-x-4 mx-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<Tr>Chat Template</Tr>
|
||||
</p>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="w-[150px] justify-start">
|
||||
<Tr>Select Template</Tr>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" side="bottom" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search template..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>
|
||||
<Tr>No results found.</Tr>
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{templates.map((t: TemplateChatStore, index: number) => (
|
||||
<CommandItem
|
||||
key={index}
|
||||
value={t.name}
|
||||
onSelect={() => handleTemplateSelect(t)}
|
||||
>
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<span>{t.name}</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEdit(t);
|
||||
}}
|
||||
>
|
||||
<EditIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDelete(t);
|
||||
}}
|
||||
>
|
||||
<DeleteIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{editingTemplate && (
|
||||
<EditChatTemplateDialog
|
||||
template={editingTemplate}
|
||||
onSave={handleSave}
|
||||
onClose={() => setEditingTemplate(null)}
|
||||
/>
|
||||
)}
|
||||
<ConfirmationDialog
|
||||
isOpen={confirmDialogOpen}
|
||||
onClose={() => {
|
||||
setConfirmDialogOpen(false);
|
||||
setTemplateToApply(null);
|
||||
}}
|
||||
onConfirm={() => templateToApply && applyTemplate(templateToApply)}
|
||||
title="Replace Chat History"
|
||||
description="This will replace the current chat history. Are you sure?"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const APIListMenu: React.FC = () => {
|
||||
const ctx = useContext(AppContext);
|
||||
return (
|
||||
<div className="flex flex-col my-2 gap-2 w-full">
|
||||
{ctx.templateTools.length > 0 && <ToolsDropdownList />}
|
||||
<NavigationMenu>
|
||||
<NavigationMenuList>
|
||||
{ctx.templates.length > 0 && <ChatTemplateDropdownList />}
|
||||
{ctx.templateAPIs.length > 0 && (
|
||||
<APIsDropdownList
|
||||
label="Chat API"
|
||||
shortLabel="Chat"
|
||||
apiField="apiEndpoint"
|
||||
keyField="apiKey"
|
||||
/>
|
||||
)}
|
||||
{ctx.templateAPIsWhisper.length > 0 && (
|
||||
<APIsDropdownList
|
||||
label="Whisper API"
|
||||
shortLabel="Whisper"
|
||||
apiField="whisper_api"
|
||||
keyField="whisper_key"
|
||||
/>
|
||||
)}
|
||||
{ctx.templateAPIsTTS.length > 0 && (
|
||||
<APIsDropdownList
|
||||
label="TTS API"
|
||||
shortLabel="TTS"
|
||||
apiField="tts_api"
|
||||
keyField="tts_key"
|
||||
/>
|
||||
)}
|
||||
{ctx.templateAPIsImageGen.length > 0 && (
|
||||
<APIsDropdownList
|
||||
label="Image Gen API"
|
||||
shortLabel="ImgGen"
|
||||
apiField="image_gen_api"
|
||||
keyField="image_gen_key"
|
||||
/>
|
||||
)}
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
{ctx.templates.length > 0 && <ChatTemplateDropdownList />}
|
||||
{ctx.templateAPIs.length > 0 && (
|
||||
<APIsDropdownList
|
||||
label="Chat API"
|
||||
shortLabel="Chat"
|
||||
apiField="apiEndpoint"
|
||||
keyField="apiKey"
|
||||
/>
|
||||
)}
|
||||
{ctx.templateAPIsWhisper.length > 0 && (
|
||||
<APIsDropdownList
|
||||
label="Whisper API"
|
||||
shortLabel="Whisper"
|
||||
apiField="whisper_api"
|
||||
keyField="whisper_key"
|
||||
/>
|
||||
)}
|
||||
{ctx.templateAPIsTTS.length > 0 && (
|
||||
<APIsDropdownList
|
||||
label="TTS API"
|
||||
shortLabel="TTS"
|
||||
apiField="tts_api"
|
||||
keyField="tts_key"
|
||||
/>
|
||||
)}
|
||||
{ctx.templateAPIsImageGen.length > 0 && (
|
||||
<APIsDropdownList
|
||||
label="Image Gen API"
|
||||
shortLabel="ImgGen"
|
||||
apiField="image_gen_api"
|
||||
keyField="image_gen_key"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { LightBulbIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import Markdown from "react-markdown";
|
||||
import remarkMath from "remark-math";
|
||||
import rehypeKatex from "rehype-katex";
|
||||
import rehypeHighlight from "rehype-highlight";
|
||||
import "katex/dist/katex.min.css";
|
||||
import {
|
||||
useContext,
|
||||
@@ -51,10 +52,12 @@ function MessageHide({ chat }: HideMessageProps) {
|
||||
return (
|
||||
<>
|
||||
<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 className="flex mt-2 justify-center">
|
||||
<Badge variant="destructive">Removed from context</Badge>
|
||||
<Badge variant="destructive">
|
||||
<Tr>Removed from context</Tr>
|
||||
</Badge>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
@@ -73,7 +76,7 @@ function MessageDetail({ chat, renderMarkdown }: MessageDetailProps) {
|
||||
{chat.content.map((mdt) =>
|
||||
mdt.type === "text" ? (
|
||||
chat.hide ? (
|
||||
mdt.text?.split("\n")[0].slice(0, 16) + " ..."
|
||||
mdt.text?.trim().slice(0, 16) + " ..."
|
||||
) : renderMarkdown ? (
|
||||
<Markdown>{mdt.text}</Markdown>
|
||||
) : (
|
||||
@@ -305,26 +308,28 @@ export default function Message(props: { messageIndex: number }) {
|
||||
</div>
|
||||
)}
|
||||
{chat.role === "assistant" ? (
|
||||
<div className="border-b border-border dark:border-border-dark pb-4">
|
||||
<div className="pb-4">
|
||||
{chat.reasoning_content ? (
|
||||
<Collapsible className="mb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<h4 className="text-sm font-semibold text-gray-500">
|
||||
{chat.response_model_name}
|
||||
</h4>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<LightBulbIcon className="h-3 w-3 text-gray-500" />
|
||||
<span className="sr-only">Toggle</span>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
<Card className="bg-muted hover:bg-muted/80 mb-5 w-full">
|
||||
<Collapsible>
|
||||
<div className="flex items-center justify-between px-3 py-1">
|
||||
<div className="flex items-center">
|
||||
<h4 className="font-semibold text-sm">
|
||||
Think Content of {chat.response_model_name}
|
||||
</h4>
|
||||
<CollapsibleTrigger asChild>
|
||||
<Button variant="ghost" size="sm">
|
||||
<LightBulbIcon className="h-3 w-3 text-gray-500" />
|
||||
<span className="sr-only">Toggle</span>
|
||||
</Button>
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<CollapsibleContent className="ml-5 text-gray-500 message-content">
|
||||
{chat.reasoning_content.trim()}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
<CollapsibleContent className="ml-5 text-gray-500 message-content p">
|
||||
{chat.reasoning_content.trim()}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</Card>
|
||||
) : null}
|
||||
<div>
|
||||
{chat.hide ? (
|
||||
@@ -334,37 +339,35 @@ export default function Message(props: { messageIndex: number }) {
|
||||
) : chat.tool_calls ? (
|
||||
<MessageToolCall chat={chat} copyToClipboard={copyToClipboard} />
|
||||
) : renderMarkdown ? (
|
||||
<div className="message-content max-w-full md:max-w-[100%]">
|
||||
<Markdown
|
||||
remarkPlugins={[remarkMath]}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
//break={true}
|
||||
components={{
|
||||
code: ({ children }) => (
|
||||
<code className="bg-muted px-1 py-0.5 rounded">
|
||||
{children}
|
||||
</code>
|
||||
),
|
||||
pre: ({ children }) => (
|
||||
<pre className="bg-muted p-4 rounded-lg overflow-auto">
|
||||
{children}
|
||||
</pre>
|
||||
),
|
||||
a: ({ href, children }) => (
|
||||
<a
|
||||
href={href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
),
|
||||
}}
|
||||
>
|
||||
{getMessageText(chat)}
|
||||
</Markdown>
|
||||
</div>
|
||||
<Markdown
|
||||
remarkPlugins={[remarkMath]}
|
||||
rehypePlugins={[rehypeKatex, rehypeHighlight]}
|
||||
disallowedElements={[
|
||||
"script",
|
||||
"iframe",
|
||||
"object",
|
||||
"embed",
|
||||
"hr",
|
||||
]}
|
||||
// allowElement={(element) => {
|
||||
// return [
|
||||
// "p",
|
||||
// "em",
|
||||
// "strong",
|
||||
// "del",
|
||||
// "code",
|
||||
// "inlineCode",
|
||||
// "blockquote",
|
||||
// "ul",
|
||||
// "ol",
|
||||
// "li",
|
||||
// "pre",
|
||||
// ].includes(element.tagName);
|
||||
// }}
|
||||
className={"prose max-w-none md:max-w-[75%]"}
|
||||
>
|
||||
{getMessageText(chat)}
|
||||
</Markdown>
|
||||
) : (
|
||||
<div className="message-content max-w-full md:max-w-[100%]">
|
||||
{chat.content &&
|
||||
@@ -433,7 +436,18 @@ export default function Message(props: { messageIndex: number }) {
|
||||
) : chat.role === "tool" ? (
|
||||
<MessageToolResp chat={chat} copyToClipboard={copyToClipboard} />
|
||||
) : 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">
|
||||
{chat.content &&
|
||||
|
||||
@@ -10,7 +10,6 @@ import { tr, Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
||||
import { isVailedJSON } from "@/utils/isVailedJSON";
|
||||
import { SetAPIsTemplate } from "@/components/setAPIsTemplate";
|
||||
import { autoHeight } from "@/utils/textAreaHelp";
|
||||
import { getDefaultParams } from "@/utils/getDefaultParam";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
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 { AppChatStoreContext, AppContext } from "@/pages/App";
|
||||
import { toast } from "@/hooks/use-toast";
|
||||
import { TemplateAttributeDialog } from "@/components/TemplateAttributeDialog";
|
||||
|
||||
const TTS_VOICES: string[] = [
|
||||
"alloy",
|
||||
@@ -153,10 +153,7 @@ const SelectModel = (props: { help: string }) => {
|
||||
value={chatStore.model}
|
||||
onValueChange={(model: string) => {
|
||||
chatStore.model = model;
|
||||
chatStore.maxTokens = getDefaultParams(
|
||||
"max",
|
||||
models[model].maxToken
|
||||
);
|
||||
chatStore.maxTokens = models[model].maxToken;
|
||||
setChatStore({ ...chatStore });
|
||||
}}
|
||||
>
|
||||
@@ -743,6 +740,7 @@ export default (props: {}) => {
|
||||
// @ts-ignore
|
||||
const { langCode, setLangCode } = useContext(langCodeContext);
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [showTemplateDialog, setShowTemplateDialog] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
themeChange(false);
|
||||
@@ -808,7 +806,7 @@ export default (props: {}) => {
|
||||
<LongInput
|
||||
label="System Prompt"
|
||||
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}
|
||||
/>
|
||||
|
||||
@@ -1055,21 +1053,7 @@ export default (props: {}) => {
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
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]);
|
||||
}}
|
||||
onClick={() => setShowTemplateDialog(true)}
|
||||
>
|
||||
<Tr>As template</Tr>
|
||||
</Button>
|
||||
@@ -1598,6 +1582,25 @@ export default (props: {}) => {
|
||||
</div>
|
||||
</NonOverflowScrollArea>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
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, 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 { TemplateChatStore } from "@/types/chatstore";
|
||||
import { ChatStore } from "@/types/chatstore";
|
||||
import { getDefaultParams } from "@/utils/getDefaultParam";
|
||||
import { useContext } from "react";
|
||||
|
||||
const Templates = () => {
|
||||
@@ -18,51 +17,6 @@ const Templates = () => {
|
||||
const newChatStore: ChatStore = structuredClone(t);
|
||||
// @ts-ignore
|
||||
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;
|
||||
|
||||
// manage undefined value because of version update
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "@/components/ui/dialog";
|
||||
import { Button } from "./ui/button";
|
||||
import { AppChatStoreContext, AppContext } from "../pages/App";
|
||||
import { ConfirmationDialog } from "./ui/confirmation-dialog";
|
||||
|
||||
interface EditMessageProps {
|
||||
chat: ChatStoreMessage;
|
||||
@@ -22,9 +23,19 @@ interface EditMessageProps {
|
||||
}
|
||||
export function EditMessage(props: EditMessageProps) {
|
||||
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
|
||||
const [showConfirmDialog, setShowConfirmDialog] = useState(false);
|
||||
|
||||
const { showEdit, setShowEdit, chat } = props;
|
||||
|
||||
const handleSwitchMessageType = () => {
|
||||
if (typeof chat.content === "string") {
|
||||
chat.content = [];
|
||||
} else {
|
||||
chat.content = "";
|
||||
}
|
||||
setChatStore({ ...chatStore });
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={showEdit} onOpenChange={setShowEdit}>
|
||||
{/* <DialogTrigger>
|
||||
@@ -46,19 +57,7 @@ export function EditMessage(props: EditMessageProps) {
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
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 });
|
||||
}}
|
||||
onClick={() => setShowConfirmDialog(true)}
|
||||
>
|
||||
Switch to{" "}
|
||||
{typeof chat.content === "string"
|
||||
@@ -68,6 +67,13 @@ export function EditMessage(props: EditMessageProps) {
|
||||
)}
|
||||
<Button onClick={() => setShowEdit(false)}>Close</Button>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
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 components;
|
||||
@tailwind utilities;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 { calculate_token_length } from "@/chatgpt";
|
||||
@@ -48,6 +48,7 @@ interface AppContextType {
|
||||
defaultRenderMD: boolean;
|
||||
setDefaultRenderMD: (b: boolean) => void;
|
||||
handleNewChatStore: () => Promise<void>;
|
||||
handleNewChatStoreWithOldOne: (chatStore: ChatStore) => Promise<void>;
|
||||
}
|
||||
|
||||
interface AppChatStoreContextType {
|
||||
@@ -96,6 +97,7 @@ import Search from "@/components/Search";
|
||||
|
||||
import Navbar from "@/components/navbar";
|
||||
import ConversationTitle from "@/components/ConversationTitle.";
|
||||
import ImportDialog from "@/components/ImportDialog";
|
||||
|
||||
export function App() {
|
||||
// init selected index
|
||||
@@ -140,10 +142,6 @@ export function App() {
|
||||
}
|
||||
if (ret.cost === undefined) ret.cost = 0;
|
||||
|
||||
toast({
|
||||
title: "Chat ready",
|
||||
description: `Current API Endpoint: ${ret.apiEndpoint}`,
|
||||
});
|
||||
return ret;
|
||||
};
|
||||
|
||||
@@ -198,9 +196,34 @@ export function App() {
|
||||
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(() => {
|
||||
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 api = getDefaultParams("api", "");
|
||||
const key = getDefaultParams("key", "");
|
||||
@@ -222,12 +245,7 @@ export function App() {
|
||||
console.log("create new chatStore because of params in URL");
|
||||
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();
|
||||
}, []);
|
||||
@@ -332,6 +350,7 @@ export function App() {
|
||||
defaultRenderMD,
|
||||
setDefaultRenderMD,
|
||||
handleNewChatStore,
|
||||
handleNewChatStoreWithOldOne,
|
||||
}}
|
||||
>
|
||||
<Sidebar>
|
||||
@@ -410,6 +429,7 @@ export function App() {
|
||||
selectedChatIndex={selectedChatIndex}
|
||||
getChatStoreByIndex={getChatStoreByIndex}
|
||||
>
|
||||
<ImportDialog open={showImportDialog} setOpen={setShowImportDialog} />
|
||||
<Navbar />
|
||||
<ChatBOX />
|
||||
</AppChatStoreProvider>
|
||||
@@ -427,9 +447,78 @@ const AppChatStoreProvider = ({
|
||||
selectedChatIndex: number;
|
||||
getChatStoreByIndex: (index: number) => Promise<ChatStore>;
|
||||
}) => {
|
||||
console.log("[Render] AppChatStoreProvider");
|
||||
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 setChatStore = async (chatStore: ChatStore) => {
|
||||
console.log("recalculate postBeginIndex");
|
||||
|
||||
@@ -40,9 +40,32 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
|
||||
import { AppChatStoreContext, AppContext } from "./App";
|
||||
import APIListMenu from "@/components/ListAPI";
|
||||
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() {
|
||||
const { db, selectedChatIndex, setSelectedChatIndex, handleNewChatStore } =
|
||||
@@ -93,78 +116,105 @@ export default function ChatBOX() {
|
||||
};
|
||||
let response_model_name: string | 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];
|
||||
|
||||
// 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;
|
||||
try {
|
||||
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];
|
||||
|
||||
// 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(
|
||||
(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}`;
|
||||
})
|
||||
);
|
||||
} catch (e: any) {
|
||||
if (e.name === "AbortError") {
|
||||
// 1. 立即保存当前buffer中的内容
|
||||
if (allChunkMessage.length > 0 || allReasoningContentChunk.length > 0) {
|
||||
const partialMsg = createMessageFromCurrentBuffer(
|
||||
allChunkMessage,
|
||||
allReasoningContentChunk,
|
||||
allChunkTool,
|
||||
responseTokenCount
|
||||
);
|
||||
chatStore.history.push(partialMsg);
|
||||
setChatStore({ ...chatStore });
|
||||
}
|
||||
// 2. 不隐藏错误,重新抛出给上层
|
||||
throw e;
|
||||
}
|
||||
// 其他错误直接抛出
|
||||
throw e;
|
||||
} finally {
|
||||
setShowGenerating(false);
|
||||
setGeneratingMessage("");
|
||||
}
|
||||
setShowGenerating(false);
|
||||
|
||||
const content = allChunkMessage.join("");
|
||||
const reasoning_content = allReasoningContentChunk.join("");
|
||||
|
||||
@@ -210,7 +260,15 @@ export default function ChatBOX() {
|
||||
audio: null,
|
||||
logprobs,
|
||||
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;
|
||||
|
||||
@@ -558,21 +616,6 @@ export default function ChatBOX() {
|
||||
<Tr>New Chat</Tr>
|
||||
</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 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -633,15 +676,32 @@ export default function ChatBOX() {
|
||||
</div>
|
||||
<div className="sticky bottom-0 w-full z-20 bg-background">
|
||||
{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">
|
||||
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
Follow
|
||||
</label>
|
||||
<Switch
|
||||
checked={follow}
|
||||
onCheckedChange={setFollow}
|
||||
aria-label="Toggle auto-scroll"
|
||||
/>
|
||||
<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">
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||
<Tr>Follow</Tr>
|
||||
</label>
|
||||
<Switch
|
||||
checked={follow}
|
||||
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>
|
||||
)}
|
||||
<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()];
|
||||
if (translatedText === undefined) {
|
||||
console.log(`[Translation] not found for "${text}"`);
|
||||
if (langCode !== "en-US") {
|
||||
console.log(`[Translation] not found for "${text}"`);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
|
||||
@@ -141,6 +141,13 @@ const LANG_MAP: Record<string, string> = {
|
||||
"Configure image generation settings": "配置图片生成设置",
|
||||
"New 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;
|
||||
|
||||
@@ -76,6 +76,7 @@ export interface ChatStoreMessage {
|
||||
created_at?: string;
|
||||
responsed_at?: string;
|
||||
completed_at?: string;
|
||||
response_count?: number;
|
||||
|
||||
role: "system" | "user" | "assistant" | "tool";
|
||||
content: string | MessageDetail[];
|
||||
|
||||
@@ -3,8 +3,7 @@ import {
|
||||
DefaultModel,
|
||||
CHATGPT_API_WEB_VERSION,
|
||||
} from "@/const";
|
||||
import { getDefaultParams } from "@/utils/getDefaultParam";
|
||||
import { ChatStore } from "@/types/chatstore";
|
||||
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
|
||||
import { models } from "@/types/models";
|
||||
|
||||
interface NewChatStoreOptions {
|
||||
@@ -36,38 +35,30 @@ interface NewChatStoreOptions {
|
||||
json_mode?: boolean;
|
||||
logprobs?: boolean;
|
||||
maxTokens?: number;
|
||||
use_this_history?: ChatStoreMessage[];
|
||||
}
|
||||
|
||||
export const newChatStore = (options: NewChatStoreOptions): ChatStore => {
|
||||
return {
|
||||
chatgpt_api_web_version: CHATGPT_API_WEB_VERSION,
|
||||
systemMessageContent: getDefaultParams(
|
||||
"sys",
|
||||
options.systemMessageContent ?? ""
|
||||
),
|
||||
systemMessageContent: options.systemMessageContent ?? "",
|
||||
toolsString: options.toolsString ?? "",
|
||||
history: [],
|
||||
history: options.use_this_history ?? [],
|
||||
postBeginIndex: 0,
|
||||
tokenMargin: 1024,
|
||||
totalTokens: 0,
|
||||
maxTokens: getDefaultParams(
|
||||
"max",
|
||||
models[getDefaultParams("model", options.model ?? DefaultModel)]
|
||||
?.maxToken ??
|
||||
options.maxTokens ??
|
||||
2048
|
||||
),
|
||||
maxTokens:
|
||||
models[options.model ?? DefaultModel]?.maxToken ??
|
||||
options.maxTokens ??
|
||||
2048,
|
||||
maxGenTokens: 2048,
|
||||
maxGenTokens_enabled: false,
|
||||
apiKey: getDefaultParams("key", options.apiKey ?? ""),
|
||||
apiEndpoint: getDefaultParams(
|
||||
"api",
|
||||
options.apiEndpoint ?? DefaultAPIEndpoint
|
||||
),
|
||||
streamMode: getDefaultParams("mode", options.streamMode ?? true),
|
||||
model: getDefaultParams("model", options.model ?? DefaultModel),
|
||||
apiKey: options.apiKey ?? "",
|
||||
apiEndpoint: options.apiEndpoint ?? DefaultAPIEndpoint,
|
||||
streamMode: options.streamMode ?? true,
|
||||
model: options.model ?? DefaultModel,
|
||||
cost: 0,
|
||||
temperature: getDefaultParams("temp", options.temperature ?? 1),
|
||||
temperature: options.temperature ?? 1,
|
||||
temperature_enabled: options.temperature_enabled ?? true,
|
||||
top_p: options.top_p ?? 1,
|
||||
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,
|
||||
frequency_penalty: options.frequency_penalty ?? 0,
|
||||
frequency_penalty_enabled: options.frequency_penalty_enabled ?? false,
|
||||
develop_mode: getDefaultParams("dev", options.dev ?? false),
|
||||
whisper_api: getDefaultParams(
|
||||
"whisper-api",
|
||||
options.whisper_api ?? "https://api.openai.com/v1/audio/transcriptions"
|
||||
),
|
||||
whisper_key: getDefaultParams("whisper-key", options.whisper_key ?? ""),
|
||||
tts_api: getDefaultParams(
|
||||
"tts-api",
|
||||
options.tts_api ?? "https://api.openai.com/v1/audio/speech"
|
||||
),
|
||||
tts_key: getDefaultParams("tts-key", options.tts_key ?? ""),
|
||||
develop_mode: options.dev ?? false,
|
||||
whisper_api:
|
||||
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",
|
||||
tts_key: options.tts_key ?? "",
|
||||
tts_voice: options.tts_voice ?? "alloy",
|
||||
tts_speed: options.tts_speed ?? 1.0,
|
||||
tts_speed_enabled: options.tts_speed_enabled ?? false,
|
||||
|
||||
@@ -4,6 +4,13 @@ module.exports = {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
typography: (theme) => ({
|
||||
DEFAULT: {
|
||||
css: {
|
||||
|
||||
},
|
||||
},
|
||||
}),
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
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