Update pages.yml Update pages.yml Update pages.yml Update pages.yml Update pages.yml fix message bubble overflow on small screen refactor ListAPI component to simplify click handler for template selection chat store title fix: adjust MessageBubble component to allow full-width rendering on medium screens feat: enhance ConversationTitle component with full-width styling and click handler for title retrieval feat: add abort signal support for fetch and stream response handling in Chat component feat: add usage tracking and timestamps to ChatStoreMessage structure pwa feat: update theme colors to black in manifest and Vite config display standlone feat: add smooth scrolling to messages in Chatbox component feat: add handleNewChatStore function to App context and integrate in Chatbox for new chat functionality feat: refactor MessageBubble component to use ChatBubble and improve structure refactor(MessageBubble): move TTSPlay component into message area and reorganize action buttons ui(navbar): improve cost breakdown clarity and add accumulated cost tracking Revert "feat: refactor MessageBubble component to use ChatBubble and improve structure" This reverts commit d16984c7da896ee0d047dca0be3f4ad1703a5d2c. display string mesasge trimed fix typo fix scroll after send fix(MessageBubble): trim whitespace from reasoning content display feat(sidebar): optimize mobile performance with CSS transitions - Refactored mobile sidebar implementation to use direct CSS transforms instead of Sheet component - Added static overlay mask with opacity transition for mobile experience - Implemented custom close button with X icon to replace Sheet's default - Improved z-index handling for sidebar elements (chat-bubble z-index reduced to 30) - Preserved DOM structure during sidebar toggle to prevent unnecessary remounting - Unified PC/mobile behavior using CSS animation rather than dynamic mounting - Removed dependency on radix-ui Dialog components for mobile sidebar fix scroll fix sidebar style on mobile apply default render to markdown fix(ChatMessageList): set width to 100vw for full viewport coverage fix small overflow fix: overflow on PC break model name anywhere fix language
This commit is contained in:
4
.github/workflows/pages.yml
vendored
4
.github/workflows/pages.yml
vendored
@@ -39,10 +39,10 @@ jobs:
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v3
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v1
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
# Upload entire repository
|
||||
path: './dist/'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v1
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
content="A simple API playground for OpenAI ChatGPT API"
|
||||
/>
|
||||
<title>ChatGPT API Web</title>
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
4711
package-lock.json
generated
4711
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -42,6 +42,7 @@
|
||||
"@types/react-dom": "^18.0.0",
|
||||
"@types/ungap__structured-clone": "^1.2.0",
|
||||
"@ungap/structured-clone": "^1.2.1",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.4",
|
||||
@@ -74,6 +75,8 @@
|
||||
"tailwindcss": "^3.4.17",
|
||||
"theme-change": "^2.5.0",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.6"
|
||||
"vite": "^6.0.6",
|
||||
"vite-plugin-pwa": "^0.21.1",
|
||||
"workbox-window": "^7.3.0"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
15
public/manifest.json
Normal file
15
public/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "ChatGPT API Web",
|
||||
"short_name": "CAW",
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"background_color": "#000000",
|
||||
"theme_color": "#000000",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DefaultModel } from "@/const";
|
||||
import { MutableRefObject } from "react";
|
||||
|
||||
export interface ImageURL {
|
||||
url: string;
|
||||
@@ -91,7 +92,7 @@ export const getMessageText = (message: Message): string => {
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
return message.content;
|
||||
return message.content.trim();
|
||||
}
|
||||
return message.content
|
||||
.filter((c) => c.type === "text")
|
||||
@@ -213,7 +214,7 @@ class Chat {
|
||||
this.json_mode = json_mode;
|
||||
}
|
||||
|
||||
_fetch(stream = false, logprobs = false) {
|
||||
_fetch(stream = false, logprobs = false, signal: AbortSignal) {
|
||||
// perform role type check
|
||||
let hasNonSystemMessage = false;
|
||||
for (const msg of this.messages) {
|
||||
@@ -301,10 +302,11 @@ class Chat {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
signal,
|
||||
});
|
||||
}
|
||||
|
||||
async *processStreamResponse(resp: Response) {
|
||||
async *processStreamResponse(resp: Response, signal?: AbortSignal) {
|
||||
const reader = resp?.body?.pipeThrough(new TextDecoderStream()).getReader();
|
||||
if (reader === undefined) {
|
||||
console.log("reader is undefined");
|
||||
@@ -313,6 +315,11 @@ class Chat {
|
||||
let receiving = true;
|
||||
let buffer = "";
|
||||
while (receiving) {
|
||||
if (signal?.aborted) {
|
||||
reader.cancel();
|
||||
console.log("signal aborted in stream response");
|
||||
break;
|
||||
}
|
||||
const { value, done } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
|
||||
69
src/components/ConversationTitle..tsx
Normal file
69
src/components/ConversationTitle..tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { STORAGE_NAME } from "@/const";
|
||||
import { AppChatStoreContext, AppContext } from "@/pages/App";
|
||||
import { ChatStore } from "@/types/chatstore";
|
||||
import { memo, useContext, useEffect, useMemo, useState } from "react";
|
||||
|
||||
const ConversationTitle = ({ chatStoreIndex }: { chatStoreIndex: number }) => {
|
||||
const { db, selectedChatIndex } = useContext(AppContext);
|
||||
const [title, setTitle] = useState("");
|
||||
|
||||
const getTitle = async () => {
|
||||
const chatStore = (await (
|
||||
await db
|
||||
).get(STORAGE_NAME, chatStoreIndex)) as ChatStore;
|
||||
if (chatStore.history.length === 0) {
|
||||
setTitle(`${chatStoreIndex}`);
|
||||
return;
|
||||
}
|
||||
const content = chatStore.history[0]?.content;
|
||||
if (!content) {
|
||||
setTitle(`${chatStoreIndex}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof content === "string") {
|
||||
console.log(content);
|
||||
setTitle(content.substring(0, 39));
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
getTitle();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<span
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
try {
|
||||
getTitle();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const CachedConversationTitle = memo(
|
||||
({
|
||||
chatStoreIndex,
|
||||
selectedChatStoreIndex,
|
||||
}: {
|
||||
chatStoreIndex: number;
|
||||
selectedChatStoreIndex: number;
|
||||
}) => {
|
||||
return <ConversationTitle chatStoreIndex={chatStoreIndex} />;
|
||||
},
|
||||
(prevProps, nextProps) => {
|
||||
return nextProps.selectedChatStoreIndex === nextProps.chatStoreIndex;
|
||||
}
|
||||
);
|
||||
|
||||
export default CachedConversationTitle;
|
||||
@@ -209,6 +209,7 @@ export function ImageGenDrawer({ disableFactor }: Props) {
|
||||
logprobs: null,
|
||||
response_model_name: imageGenModel,
|
||||
reasoning_content: null,
|
||||
usage: null,
|
||||
});
|
||||
|
||||
setChatStore({ ...chatStore });
|
||||
@@ -221,7 +222,7 @@ export function ImageGenDrawer({ disableFactor }: Props) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{Tr("Generate")}
|
||||
<Tr>Generate</Tr>
|
||||
</Button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -151,7 +151,9 @@ function ToolsDropdownList() {
|
||||
|
||||
return (
|
||||
<div className="flex items-center space-x-4 mx-3">
|
||||
<p className="text-sm text-muted-foreground">{Tr(`Tools`)}</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<Tr>Tools</Tr>
|
||||
</p>
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline" className="w-[150px] justify-start">
|
||||
@@ -164,7 +166,9 @@ function ToolsDropdownList() {
|
||||
}
|
||||
</>
|
||||
) : (
|
||||
<>+ {Tr(`Set tools`)}</>
|
||||
<>
|
||||
+ <Tr>Set tools</Tr>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -172,7 +176,9 @@ function ToolsDropdownList() {
|
||||
<Command>
|
||||
<CommandInput placeholder="You can search..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>{Tr(`No results found.`)}</CommandEmpty>
|
||||
<CommandEmpty>
|
||||
<Tr>No results found.</Tr>
|
||||
</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{chatStore.toolsString && (
|
||||
<CommandItem
|
||||
@@ -188,7 +194,7 @@ function ToolsDropdownList() {
|
||||
setOpen(false);
|
||||
}}
|
||||
>
|
||||
<BrushIcon /> {Tr(`Clear tools`)}
|
||||
<BrushIcon /> <Tr>Clear tools</Tr>
|
||||
</CommandItem>
|
||||
)}
|
||||
{ctx.templateTools.map((t, index) => (
|
||||
@@ -247,23 +253,21 @@ const ChatTemplateItem = ({
|
||||
const { templates, setTemplates } = useContext(AppContext);
|
||||
|
||||
return (
|
||||
<li>
|
||||
<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
|
||||
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 }) });
|
||||
}}
|
||||
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"
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,13 @@ import Markdown from "react-markdown";
|
||||
import remarkMath from "remark-math";
|
||||
import rehypeKatex from "rehype-katex";
|
||||
import "katex/dist/katex.min.css";
|
||||
import { useContext, useState, useMemo } from "react";
|
||||
import {
|
||||
useContext,
|
||||
useState,
|
||||
useMemo,
|
||||
useInsertionEffect,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { ChatStoreMessage } from "@/types/chatstore";
|
||||
import { addTotalCost } from "@/utils/totalCost";
|
||||
|
||||
@@ -254,12 +260,16 @@ export default function Message(props: { messageIndex: number }) {
|
||||
const [renderMarkdown, setRenderWorkdown] = useState(defaultRenderMD);
|
||||
const [renderColor, setRenderColor] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setRenderWorkdown(defaultRenderMD);
|
||||
}, [defaultRenderMD]);
|
||||
|
||||
const { toast } = useToast();
|
||||
const copyToClipboard = async (text: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
toast({
|
||||
description: Tr("Message copied to clipboard!"),
|
||||
description: <Tr>Message copied to clipboard!</Tr>,
|
||||
});
|
||||
} catch (err) {
|
||||
const textArea = document.createElement("textarea");
|
||||
@@ -269,11 +279,11 @@ export default function Message(props: { messageIndex: number }) {
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
toast({
|
||||
description: Tr("Message copied to clipboard!"),
|
||||
description: <Tr>Message copied to clipboard!</Tr>,
|
||||
});
|
||||
} catch (err) {
|
||||
toast({
|
||||
description: Tr("Failed to copy to clipboard"),
|
||||
description: <Tr>Failed to copy to clipboard</Tr>,
|
||||
});
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
@@ -297,7 +307,7 @@ export default function Message(props: { messageIndex: number }) {
|
||||
{chat.role === "assistant" ? (
|
||||
<div className="border-b border-border dark:border-border-dark pb-4">
|
||||
{chat.reasoning_content ? (
|
||||
<Collapsible className="mb-3 w-[450px]">
|
||||
<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">
|
||||
@@ -311,8 +321,8 @@ export default function Message(props: { messageIndex: number }) {
|
||||
</CollapsibleTrigger>
|
||||
</div>
|
||||
</div>
|
||||
<CollapsibleContent className="ml-5 text-gray-500">
|
||||
{chat.reasoning_content}
|
||||
<CollapsibleContent className="ml-5 text-gray-500 message-content">
|
||||
{chat.reasoning_content.trim()}
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
) : null}
|
||||
@@ -324,7 +334,7 @@ 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-[75%]">
|
||||
<div className="message-content max-w-full md:max-w-[100%]">
|
||||
<Markdown
|
||||
remarkPlugins={[remarkMath]}
|
||||
rehypePlugins={[rehypeKatex]}
|
||||
@@ -356,7 +366,7 @@ export default function Message(props: { messageIndex: number }) {
|
||||
</Markdown>
|
||||
</div>
|
||||
) : (
|
||||
<div className="message-content max-w-full md:max-w-[75%]">
|
||||
<div className="message-content max-w-full md:max-w-[100%]">
|
||||
{chat.content &&
|
||||
(chat.logprobs && renderColor
|
||||
? chat.logprobs.content
|
||||
@@ -374,6 +384,7 @@ export default function Message(props: { messageIndex: number }) {
|
||||
: getMessageText(chat))}
|
||||
</div>
|
||||
)}
|
||||
<TTSPlay chat={chat} />
|
||||
</div>
|
||||
<div className="flex md:opacity-0 hover:opacity-100 transition-opacity">
|
||||
<ChatBubbleAction
|
||||
@@ -408,7 +419,6 @@ export default function Message(props: { messageIndex: number }) {
|
||||
{chatStore.tts_api && chatStore.tts_key && (
|
||||
<TTSButton chat={chat} />
|
||||
)}
|
||||
<TTSPlay chat={chat} />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -443,6 +453,7 @@ export default function Message(props: { messageIndex: number }) {
|
||||
: getMessageText(chat))}
|
||||
</div>
|
||||
)}
|
||||
<TTSPlay chat={chat} />
|
||||
</ChatBubbleMessage>
|
||||
<ChatBubbleActionWrapper>
|
||||
<ChatBubbleAction
|
||||
@@ -477,7 +488,6 @@ export default function Message(props: { messageIndex: number }) {
|
||||
{chatStore.tts_api && chatStore.tts_key && (
|
||||
<TTSButton chat={chat} />
|
||||
)}
|
||||
<TTSPlay chat={chat} />
|
||||
</ChatBubbleActionWrapper>
|
||||
</ChatBubble>
|
||||
)}
|
||||
@@ -529,7 +539,9 @@ export default function Message(props: { messageIndex: number }) {
|
||||
setChatStore({ ...chatStore });
|
||||
}}
|
||||
/>
|
||||
<span className="text-sm font-medium">{Tr("example")}</span>
|
||||
<span className="text-sm font-medium">
|
||||
<Tr>example</Tr>
|
||||
</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
@@ -538,7 +550,9 @@ export default function Message(props: { messageIndex: number }) {
|
||||
checked={renderMarkdown}
|
||||
onChange={() => setRenderWorkdown(!renderMarkdown)}
|
||||
/>
|
||||
<span className="text-sm font-medium">{Tr("render")}</span>
|
||||
<span className="text-sm font-medium">
|
||||
<Tr>render</Tr>
|
||||
</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
@@ -547,7 +561,9 @@ export default function Message(props: { messageIndex: number }) {
|
||||
checked={renderColor}
|
||||
onChange={() => setRenderColor(!renderColor)}
|
||||
/>
|
||||
<span className="text-sm font-medium">{Tr("color")}</span>
|
||||
<span className="text-sm font-medium">
|
||||
<Tr>color</Tr>
|
||||
</span>
|
||||
</label>
|
||||
{chat.response_model_name && (
|
||||
<>
|
||||
|
||||
@@ -78,7 +78,6 @@ 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 { title } from "process";
|
||||
|
||||
const TTS_VOICES: string[] = [
|
||||
"alloy",
|
||||
@@ -132,7 +131,7 @@ const SelectModel = (props: { help: string }) => {
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
<CogIcon className="w-4 h-4" />
|
||||
{Tr("Custom")}
|
||||
<Tr>Custom</Tr>
|
||||
</Label>
|
||||
<Checkbox
|
||||
checked={useCustomModel}
|
||||
@@ -764,7 +763,7 @@ export default (props: {}) => {
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline" className="flex-grow">
|
||||
{Tr("Settings")}
|
||||
<Tr>Settings</Tr>
|
||||
{(!chatStore.apiKey || !chatStore.apiEndpoint) && (
|
||||
<TriangleAlertIcon className="w-4 h-4 ml-1 text-yellow-500" />
|
||||
)}
|
||||
@@ -773,14 +772,18 @@ export default (props: {}) => {
|
||||
<SheetContent className="flex flex-col overflow-scroll">
|
||||
<NonOverflowScrollArea>
|
||||
<SheetHeader>
|
||||
<SheetTitle>{Tr("Settings")}</SheetTitle>
|
||||
<SheetTitle>
|
||||
<Tr>Settings</Tr>
|
||||
</SheetTitle>
|
||||
<SheetDescription>
|
||||
You can customize the settings here.
|
||||
<Tr>You can customize all the settings here</Tr>
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<Accordion type="multiple" className="w-full">
|
||||
<AccordionItem value="session">
|
||||
<AccordionTrigger>Session</AccordionTrigger>
|
||||
<AccordionTrigger>
|
||||
<Tr>Session</Tr>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
|
||||
@@ -828,7 +831,9 @@ export default (props: {}) => {
|
||||
{chatStore.toolsString.trim() && (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">{Tr(`Save Tools`)}</Button>
|
||||
<Button variant="outline">
|
||||
<Tr>Save Tools</Tr>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
@@ -894,14 +899,20 @@ export default (props: {}) => {
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="system">
|
||||
<AccordionTrigger>System</AccordionTrigger>
|
||||
<AccordionTrigger>
|
||||
<Tr>System</Tr>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<>
|
||||
<Card>
|
||||
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
|
||||
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
|
||||
<CardTitle>Accumulated Cost</CardTitle>
|
||||
<CardDescription>in all sessions</CardDescription>
|
||||
<CardTitle>
|
||||
<Tr>Accumulated Cost</Tr>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<Tr>in all sessions</Tr>
|
||||
</CardDescription>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<div className="flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-8 sm:py-6">
|
||||
@@ -924,25 +935,34 @@ export default (props: {}) => {
|
||||
setTotalCost(getTotalCost());
|
||||
}}
|
||||
>
|
||||
Reset Total Cost
|
||||
<Tr>Reset Total Cost</Tr>
|
||||
</Button>
|
||||
</div>
|
||||
<Choice
|
||||
field="develop_mode"
|
||||
help="开发者模式,开启后会显示更多选项及功能"
|
||||
help={tr(
|
||||
"Develop Mode, enable to show more options and features",
|
||||
langCode
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<DefaultRenderMDCheckbox />
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Language</Label>
|
||||
<Label>
|
||||
<Tr>Language</Tr>
|
||||
</Label>
|
||||
<Select value={langCode} onValueChange={setLangCode}>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select language" />
|
||||
<SelectValue
|
||||
placeholder={tr("Select language", langCode)}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Languages</SelectLabel>
|
||||
<SelectLabel>
|
||||
<Tr>Languages</Tr>
|
||||
</SelectLabel>
|
||||
{Object.keys(LANG_OPTIONS).map((opt) => (
|
||||
<SelectItem key={opt} value={opt}>
|
||||
{LANG_OPTIONS[opt].name}
|
||||
@@ -954,7 +974,9 @@ export default (props: {}) => {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Quick Actions</Label>
|
||||
<Label>
|
||||
<Tr>Quick Actions</Tr>
|
||||
</Label>
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -967,23 +989,25 @@ export default (props: {}) => {
|
||||
});
|
||||
}}
|
||||
>
|
||||
{Tr("Copy Setting Link")}
|
||||
<Tr>Copy Setting Link</Tr>
|
||||
</Button>
|
||||
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="destructive" className="w-full">
|
||||
{Tr("Clear History")}
|
||||
<Tr>Clear History</Tr>
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Are you absolutely sure?
|
||||
<Tr>Are you absolutely sure?</Tr>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
This action cannot be undone. This will
|
||||
permanently delete all chat history.
|
||||
<Tr>
|
||||
This action cannot be undone. This will
|
||||
permanently delete all chat history.
|
||||
</Tr>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
@@ -998,7 +1022,7 @@ export default (props: {}) => {
|
||||
setChatStore({ ...chatStore });
|
||||
}}
|
||||
>
|
||||
Yes, clear all history
|
||||
<Tr>Yes, clear all history</Tr>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -1025,7 +1049,7 @@ export default (props: {}) => {
|
||||
downloadAnchorNode.remove();
|
||||
}}
|
||||
>
|
||||
{Tr("Export")}
|
||||
<Tr>Export</Tr>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -1047,7 +1071,7 @@ export default (props: {}) => {
|
||||
setTemplates([...templates]);
|
||||
}}
|
||||
>
|
||||
{Tr("As template")}
|
||||
<Tr>As template</Tr>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
@@ -1067,7 +1091,7 @@ export default (props: {}) => {
|
||||
importFileRef.current.click();
|
||||
}}
|
||||
>
|
||||
Import
|
||||
<Tr>Import</Tr>
|
||||
</Button>
|
||||
|
||||
<input
|
||||
@@ -1116,28 +1140,38 @@ export default (props: {}) => {
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="chat">
|
||||
<AccordionTrigger>Chat</AccordionTrigger>
|
||||
<AccordionTrigger>
|
||||
<Tr>Chat</Tr>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Chat API</CardTitle>
|
||||
<CardTitle>
|
||||
<Tr>Chat API</Tr>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Configure the LLM API settings
|
||||
<Tr>Configure the LLM API settings</Tr>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<InputField
|
||||
field="apiKey"
|
||||
help="OPEN AI API 密钥,请勿泄漏此密钥"
|
||||
help={tr(
|
||||
"OpenAI API key, do not leak this key",
|
||||
langCode
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<InputField
|
||||
field="apiEndpoint"
|
||||
help="API 端点,方便在不支持的地区使用反向代理服务,默认为 https://api.openai.com/v1/chat/completions"
|
||||
help={tr(
|
||||
"API endpoint, useful for using reverse proxy services in unsupported regions, default to https://api.openai.com/v1/chat/completions",
|
||||
langCode
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<SetAPIsTemplate
|
||||
label="Chat API"
|
||||
label={tr("Chat API", langCode)}
|
||||
endpoint={chatStore.apiEndpoint}
|
||||
APIkey={chatStore.apiKey}
|
||||
temps={templateAPIs}
|
||||
@@ -1147,54 +1181,81 @@ export default (props: {}) => {
|
||||
</Card>
|
||||
<Separator className="my-3" />
|
||||
<SelectModel
|
||||
help="模型,默认 3.5。不同模型性能和定价也不同,请参考 API 文档。"
|
||||
help={tr(
|
||||
"Model, Different models have different performance and pricing, please refer to the API documentation",
|
||||
langCode
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<Slicer
|
||||
field="temperature"
|
||||
min={0}
|
||||
max={2}
|
||||
help="温度,数值越大模型生成文字的随机性越高。"
|
||||
help={tr(
|
||||
"Temperature, the higher the value, the higher the randomness of the generated text.",
|
||||
langCode
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<Choice
|
||||
field="streamMode"
|
||||
help="流模式,使用 stream mode 将可以动态看到生成内容,但无法准确计算 token 数量,在 token 数量过多时可能会裁切过多或过少历史消息"
|
||||
help={tr(
|
||||
"Stream Mode, use stream mode to see the generated content dynamically, but the token count cannot be accurately calculated, which may cause too much or too little history messages to be truncated when the token count is too large.",
|
||||
langCode
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<Choice
|
||||
field="logprobs"
|
||||
help="返回每个Token的概率"
|
||||
help={tr(
|
||||
"Logprobs, return the probability of each token",
|
||||
langCode
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<Number
|
||||
field="maxTokens"
|
||||
help="最大上下文 token 数量。此值会根据选择的模型自动设置。"
|
||||
help={tr(
|
||||
"Max context token count. This value will be set automatically based on the selected model.",
|
||||
langCode
|
||||
)}
|
||||
readOnly={false}
|
||||
{...props}
|
||||
/>
|
||||
<Number
|
||||
field="maxGenTokens"
|
||||
help="最大生成 Tokens 数量,可选值。"
|
||||
help={tr(
|
||||
"maxGenTokens is the maximum number of tokens that can be generated in a single request.",
|
||||
langCode
|
||||
)}
|
||||
readOnly={false}
|
||||
{...props}
|
||||
/>
|
||||
<Number
|
||||
field="tokenMargin"
|
||||
help="当 totalTokens > maxTokens - tokenMargin 时会触发历史消息裁切,chatgpt会“忘记”一部分对话中的消息(但所有历史消息仍然保存在本地)"
|
||||
help={tr(
|
||||
'When totalTokens > maxTokens - tokenMargin, the history message will be truncated, chatgpt will "forget" part of the messages in the conversation (but all history messages are still saved locally)',
|
||||
langCode
|
||||
)}
|
||||
readOnly={false}
|
||||
{...props}
|
||||
/>
|
||||
<Choice field="json_mode" help="JSON Mode" {...props} />
|
||||
<Number
|
||||
field="postBeginIndex"
|
||||
help="指示发送 API 请求时要”忘记“多少历史消息"
|
||||
help={tr(
|
||||
"Indicates how many history messages to 'forget' when sending API requests",
|
||||
langCode
|
||||
)}
|
||||
readOnly={true}
|
||||
{...props}
|
||||
/>
|
||||
<Number
|
||||
field="totalTokens"
|
||||
help="token总数,每次对话都会更新此参数,stream模式下该参数为估计值"
|
||||
help={tr(
|
||||
"Total token count, this parameter will be updated every time you chat, in stream mode this parameter is an estimate",
|
||||
langCode
|
||||
)}
|
||||
readOnly={true}
|
||||
{...props}
|
||||
/>
|
||||
@@ -1202,43 +1263,56 @@ export default (props: {}) => {
|
||||
field="top_p"
|
||||
min={0}
|
||||
max={1}
|
||||
help="Top P 采样方法。建议与温度采样方法二选一,不要同时开启。"
|
||||
help={tr(
|
||||
"Top P sampling method. It is recommended to choose one of the temperature sampling methods, do not enable both at the same time.",
|
||||
langCode
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<Number
|
||||
field="presence_penalty"
|
||||
help="存在惩罚度"
|
||||
help={tr("Presence Penalty", langCode)}
|
||||
readOnly={false}
|
||||
{...props}
|
||||
/>
|
||||
<Number
|
||||
field="frequency_penalty"
|
||||
help="频率惩罚度"
|
||||
help={tr("Frequency Penalty", langCode)}
|
||||
readOnly={false}
|
||||
{...props}
|
||||
/>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="speech">
|
||||
<AccordionTrigger>Speech Recognition</AccordionTrigger>
|
||||
<AccordionTrigger>
|
||||
<Tr>Speech Recognition</Tr>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Whisper API</CardTitle>
|
||||
<CardTitle>
|
||||
<Tr>Whisper API</Tr>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Configure speech recognition settings
|
||||
<Tr>Configure speech recognition settings</Tr>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<InputField
|
||||
field="whisper_key"
|
||||
help="Used for Whisper service. Defaults to the OpenAI key above, but can be configured separately here"
|
||||
help={tr(
|
||||
"Used for Whisper service. Defaults to the OpenAI key above, but can be configured separately here",
|
||||
langCode
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<InputField
|
||||
field="whisper_api"
|
||||
help="Whisper speech-to-text service. Service is enabled when this is set. Default: https://api.openai.com/v1/audio/transriptions"
|
||||
help={tr(
|
||||
"Whisper speech-to-text service. Service is enabled when this is set. Default: https://api.openai.com/v1/audio/transriptions",
|
||||
langCode
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<SetAPIsTemplate
|
||||
@@ -1254,24 +1328,34 @@ export default (props: {}) => {
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="tts">
|
||||
<AccordionTrigger>TTS</AccordionTrigger>
|
||||
<AccordionTrigger>
|
||||
<Tr>TTS</Tr>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>TTS API</CardTitle>
|
||||
<CardTitle>
|
||||
<Tr>TTS API</Tr>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Configure text-to-speech settings
|
||||
<Tr>Configure text-to-speech settings</Tr>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<InputField
|
||||
field="tts_key"
|
||||
help="Text-to-speech service API key. Defaults to the OpenAI key above, but can be configured separately here"
|
||||
help={tr(
|
||||
"Text-to-speech service API key. Defaults to the OpenAI key above, but can be configured separately here",
|
||||
langCode
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<InputField
|
||||
field="tts_api"
|
||||
help="TTS API endpoint. Service is enabled when this is set. Default: https://api.openai.com/v1/audio/speech"
|
||||
help={tr(
|
||||
"TTS API endpoint. Service is enabled when this is set. Default: https://api.openai.com/v1/audio/speech",
|
||||
langCode
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<SetAPIsTemplate
|
||||
@@ -1286,7 +1370,7 @@ export default (props: {}) => {
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
TTS Voice
|
||||
<Tr>TTS Voice</Tr>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
@@ -1295,9 +1379,11 @@ export default (props: {}) => {
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>TTS Voice</DialogTitle>
|
||||
<DialogTitle>
|
||||
<Tr>TTS Voice</Tr>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Select the voice style for text-to-speech
|
||||
<Tr>Select the voice style for text-to-speech</Tr>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
@@ -1315,7 +1401,9 @@ export default (props: {}) => {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Voices</SelectLabel>
|
||||
<SelectLabel>
|
||||
<Tr>Voices</Tr>
|
||||
</SelectLabel>
|
||||
{TTS_VOICES.map((opt) => (
|
||||
<SelectItem key={opt} value={opt}>
|
||||
{opt}
|
||||
@@ -1330,13 +1418,16 @@ export default (props: {}) => {
|
||||
min={0.25}
|
||||
max={4.0}
|
||||
field="tts_speed"
|
||||
help="Adjust the playback speed of text-to-speech"
|
||||
help={tr(
|
||||
"Adjust the playback speed of text-to-speech",
|
||||
langCode
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="flex items-center gap-2">
|
||||
TTS Format
|
||||
<Tr>TTS Format</Tr>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
@@ -1345,9 +1436,14 @@ export default (props: {}) => {
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>TTS Format</DialogTitle>
|
||||
<DialogTitle>
|
||||
<Tr>TTS Format</Tr>
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Select the audio format for text-to-speech output
|
||||
<Tr>
|
||||
Select the audio format for text-to-speech
|
||||
output
|
||||
</Tr>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
@@ -1365,7 +1461,9 @@ export default (props: {}) => {
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
<SelectLabel>Formats</SelectLabel>
|
||||
<SelectLabel>
|
||||
<Tr>Formats</Tr>
|
||||
</SelectLabel>
|
||||
{TTS_FORMAT.map((opt) => (
|
||||
<SelectItem key={opt} value={opt}>
|
||||
{opt}
|
||||
@@ -1379,28 +1477,38 @@ export default (props: {}) => {
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="image_gen">
|
||||
<AccordionTrigger>Image Generation</AccordionTrigger>
|
||||
<AccordionTrigger>
|
||||
<Tr>Image Generation</Tr>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Image Generation API</CardTitle>
|
||||
<CardTitle>
|
||||
<Tr>Image Generation API</Tr>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Configure image generation settings
|
||||
<Tr>Configure image generation settings</Tr>
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<InputField
|
||||
field="image_gen_key"
|
||||
help="Image generation service API key. Defaults to the OpenAI key above, but can be configured separately here"
|
||||
help={tr(
|
||||
"Image generation service API key. Defaults to the OpenAI key above, but can be configured separately here",
|
||||
langCode
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<InputField
|
||||
field="image_gen_api"
|
||||
help="Image generation API endpoint. Service is enabled when this is set. Default: https://api.openai.com/v1/images/generations"
|
||||
help={tr(
|
||||
"Image generation API endpoint. Service is enabled when this is set. Default: https://api.openai.com/v1/images/generations",
|
||||
langCode
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
<SetAPIsTemplate
|
||||
label="Image Gen API"
|
||||
label={tr("Image Gen API", langCode)}
|
||||
endpoint={chatStore.image_gen_api}
|
||||
APIkey={chatStore.image_gen_key}
|
||||
temps={templateAPIsImageGen}
|
||||
@@ -1411,7 +1519,9 @@ export default (props: {}) => {
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="templates">
|
||||
<AccordionTrigger>Saved Template</AccordionTrigger>
|
||||
<AccordionTrigger>
|
||||
<Tr>Saved Template</Tr>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
{templateAPIs.map((template, index) => (
|
||||
<div key={index}>
|
||||
@@ -1471,11 +1581,11 @@ export default (props: {}) => {
|
||||
</Accordion>
|
||||
<div className="pt-4 space-y-2">
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
chatgpt-api-web ChatStore {Tr("Version")}{" "}
|
||||
chatgpt-api-web ChatStore <Tr>Version</Tr>
|
||||
{chatStore.chatgpt_api_web_version}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground text-center">
|
||||
{Tr("Documents and source code are avaliable here")}:{" "}
|
||||
<Tr>Documents and source code are avaliable here</Tr>:{" "}
|
||||
<a
|
||||
className="underline hover:text-primary transition-colors"
|
||||
href="https://github.com/heimoshuiyu/chatgpt-api-web"
|
||||
|
||||
@@ -10,7 +10,7 @@ const VersionHint = () => {
|
||||
{chatStore.chatgpt_api_web_version < "v1.3.0" && (
|
||||
<p className="p-2 my-2 text-center dark:text-white">
|
||||
<br />
|
||||
{Tr("Warning: current chatStore version")}:{" "}
|
||||
<Tr>Warning: current chatStore version</Tr>:{" "}
|
||||
{chatStore.chatgpt_api_web_version} {"< v1.3.0"}
|
||||
<br />
|
||||
v1.3.0
|
||||
@@ -22,7 +22,7 @@ const VersionHint = () => {
|
||||
{chatStore.chatgpt_api_web_version < "v1.4.0" && (
|
||||
<p className="p-2 my-2 text-center dark:text-white">
|
||||
<br />
|
||||
{Tr("Warning: current chatStore version")}:{" "}
|
||||
<Tr>Warning: current chatStore version</Tr>:{" "}
|
||||
{chatStore.chatgpt_api_web_version} {"< v1.4.0"}
|
||||
<br />
|
||||
v1.4.0 增加了更多参数,继续使用旧版可能因参数确实导致未定义的行为
|
||||
@@ -34,7 +34,7 @@ const VersionHint = () => {
|
||||
<p className="p-2 my-2 text-center dark:text-white">
|
||||
<br />
|
||||
提示:当前会话版本 {chatStore.chatgpt_api_web_version}
|
||||
{Tr("Warning: current chatStore version")}:{" "}
|
||||
<Tr>Warning: current chatStore version</Tr>:{" "}
|
||||
{chatStore.chatgpt_api_web_version} {"< v1.6.0"}
|
||||
。
|
||||
<br />
|
||||
|
||||
@@ -81,7 +81,7 @@ export function EditMessageDetail({ chat, setShowEdit }: Props) {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{Tr("Edit URL")}
|
||||
<Tr>Edit URL</Tr>
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-blue-300 p-1 rounded m-1"
|
||||
@@ -110,7 +110,7 @@ export function EditMessageDetail({ chat, setShowEdit }: Props) {
|
||||
input.click();
|
||||
}}
|
||||
>
|
||||
{Tr("Upload")}
|
||||
<Tr>Upload</Tr>
|
||||
</Button>
|
||||
<span
|
||||
className="bg-blue-300 p-1 rounded m-1"
|
||||
@@ -155,7 +155,7 @@ export function EditMessageDetail({ chat, setShowEdit }: Props) {
|
||||
setChatStore({ ...chatStore });
|
||||
}}
|
||||
>
|
||||
{Tr("Add text")}
|
||||
<Tr>Add text</Tr>
|
||||
</Button>
|
||||
<Button
|
||||
className={"m-2 p-1 rounded bg-green-500"}
|
||||
@@ -171,7 +171,7 @@ export function EditMessageDetail({ chat, setShowEdit }: Props) {
|
||||
setChatStore({ ...chatStore });
|
||||
}}
|
||||
>
|
||||
{Tr("Add image")}
|
||||
<Tr>Add image</Tr>
|
||||
</Button>
|
||||
</div>
|
||||
<DrawerFooter>
|
||||
@@ -179,7 +179,7 @@ export function EditMessageDetail({ chat, setShowEdit }: Props) {
|
||||
className="bg-blue-500 p-2 rounded"
|
||||
onClick={() => setShowEdit(false)}
|
||||
>
|
||||
{Tr("Close")}
|
||||
<Tr>Close</Tr>
|
||||
</Button>
|
||||
</DrawerFooter>
|
||||
</DrawerContent>
|
||||
|
||||
@@ -73,7 +73,7 @@ export function EditMessageString({ chat, setShowEdit }: Props) {
|
||||
setChatStore({ ...chatStore });
|
||||
}}
|
||||
>
|
||||
{Tr("Delete this tool call")}
|
||||
<Tr>Delete this tool call</Tr>
|
||||
</button>
|
||||
</span>
|
||||
<hr className="my-2" />
|
||||
@@ -94,7 +94,7 @@ export function EditMessageString({ chat, setShowEdit }: Props) {
|
||||
setChatStore({ ...chatStore });
|
||||
}}
|
||||
>
|
||||
{Tr("Add a tool call")}
|
||||
<Tr>Add a tool call</Tr>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -39,13 +39,13 @@ const Navbar: React.FC = () => {
|
||||
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
|
||||
|
||||
return (
|
||||
<header className="flex sticky top-0 bg-background h-14 shrink-0 items-center border-b z-50">
|
||||
<header className="flex sticky top-0 bg-background h-14 shrink-0 items-center border-b z-30">
|
||||
<div className="flex flex-col w-full">
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
<div className="flex items-center gap-2 px-3">
|
||||
<SidebarTrigger />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
<h1 className="text-lg font-bold">{chatStore.model}</h1>
|
||||
<h1 className="text-lg font-bold break-all">{chatStore.model}</h1>
|
||||
</div>
|
||||
<div className="flex ml-auto gap-2 px-3">
|
||||
<Settings />
|
||||
@@ -63,13 +63,22 @@ const Navbar: React.FC = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
<ReceiptIcon className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">
|
||||
Generated Tokens: {chatStore.totalTokens.toString()}
|
||||
<Tr>Total Tokens</Tr>: {chatStore.totalTokens.toString()}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<WalletIcon className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">
|
||||
Cost: ${getTotalCost().toFixed(2)}
|
||||
<Tr>Session Cost</Tr>: ${chatStore.cost.toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center px-4 py-2 border-b">
|
||||
<div></div>
|
||||
<div className="flex items-center gap-2">
|
||||
<WalletIcon className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">
|
||||
<Tr>Accumulated Cost</Tr>: ${getTotalCost().toFixed(2)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,9 @@ export function SetAPIsTemplate({
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">{Tr(`Save ${label}`)}</Button>
|
||||
<Button variant="outline">
|
||||
<Tr>Save</Tr>${label}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
|
||||
@@ -177,7 +177,7 @@ const ChatBubbleActionWrapper = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute z-50 translate-y-full flex opacity-0 group-hover:opacity-100 transition-opacity duration-200",
|
||||
"absolute z-30 translate-y-full flex opacity-0 group-hover:opacity-100 transition-opacity duration-200",
|
||||
variant === "sent" ? "flex-row-reverse" : "",
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { VariantProps, cva } from "class-variance-authority";
|
||||
import { PanelLeft } from "lucide-react";
|
||||
import { PanelLeft, X } from "lucide-react";
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -192,21 +192,41 @@ const Sidebar = React.forwardRef<
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<SheetContent
|
||||
<div className="md:hidden">
|
||||
{/* 遮罩层 */}
|
||||
<div
|
||||
className={cn(
|
||||
"fixed inset-0 z-40 bg-black/80 transition-opacity duration-500",
|
||||
openMobile ? "opacity-100" : "opacity-0 pointer-events-none"
|
||||
)}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
setOpenMobile(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* 侧边栏 */}
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
data-mobile="true"
|
||||
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||
className={cn(
|
||||
"fixed inset-y-0 z-50 h-svh w-[--sidebar-width-mobile] bg-sidebar p-0 text-sidebar-foreground transition-transform duration-500 ease-in-out",
|
||||
side === "left"
|
||||
? "-translate-x-full left-0"
|
||||
: "translate-x-full right-0",
|
||||
openMobile && "translate-x-0"
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
"--sidebar-width-mobile": SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
side={side}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { ThemeProvider } from "@/components/ThemeProvider";
|
||||
|
||||
import "./registerSW"; // 添加此行
|
||||
|
||||
function Base() {
|
||||
const [langCode, _setLangCode] = useState("en-US");
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ const AddToolMsg = (props: {
|
||||
className="btn btn-info m-1 p-1"
|
||||
onClick={() => setShowAddToolMsg(false)}
|
||||
>
|
||||
{Tr("Cancle")}
|
||||
<Tr>Cancle</Tr>
|
||||
</button>
|
||||
<button
|
||||
className="rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
|
||||
@@ -75,6 +75,7 @@ const AddToolMsg = (props: {
|
||||
logprobs: null,
|
||||
response_model_name: null,
|
||||
reasoning_content: null,
|
||||
usage: null,
|
||||
});
|
||||
setChatStore({ ...chatStore });
|
||||
setNewToolCallID("");
|
||||
@@ -82,7 +83,7 @@ const AddToolMsg = (props: {
|
||||
setShowAddToolMsg(false);
|
||||
}}
|
||||
>
|
||||
{Tr("Add")}
|
||||
<Tr>Add</Tr>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -47,6 +47,7 @@ interface AppContextType {
|
||||
setTemplateTools: (t: TemplateTools[]) => void;
|
||||
defaultRenderMD: boolean;
|
||||
setDefaultRenderMD: (b: boolean) => void;
|
||||
handleNewChatStore: () => Promise<void>;
|
||||
}
|
||||
|
||||
interface AppChatStoreContextType {
|
||||
@@ -94,6 +95,7 @@ import { ModeToggle } from "@/components/ModeToggle";
|
||||
import Search from "@/components/Search";
|
||||
|
||||
import Navbar from "@/components/navbar";
|
||||
import ConversationTitle from "@/components/ConversationTitle.";
|
||||
|
||||
export function App() {
|
||||
// init selected index
|
||||
@@ -329,12 +331,15 @@ export function App() {
|
||||
setTemplateTools,
|
||||
defaultRenderMD,
|
||||
setDefaultRenderMD,
|
||||
handleNewChatStore,
|
||||
}}
|
||||
>
|
||||
<Sidebar>
|
||||
<SidebarHeader>
|
||||
<Button onClick={handleNewChatStore}>
|
||||
<span>{Tr("New")}</span>
|
||||
<span>
|
||||
<Tr>New Chat</Tr>
|
||||
</span>
|
||||
</Button>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
@@ -356,7 +361,12 @@ export function App() {
|
||||
asChild
|
||||
isActive={i === selectedChatIndex}
|
||||
>
|
||||
<span>{i}</span>
|
||||
<span>
|
||||
<ConversationTitle
|
||||
chatStoreIndex={i}
|
||||
selectedChatStoreIndex={selectedChatIndex}
|
||||
/>
|
||||
</span>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
@@ -372,7 +382,9 @@ export function App() {
|
||||
</div>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive">{Tr("DEL")}</Button>
|
||||
<Button variant="destructive">
|
||||
<Tr>Delete Chat</Tr>
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useContext, useRef } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Tr } from "@/translate";
|
||||
import { langCodeContext, tr, Tr } from "@/translate";
|
||||
import { addTotalCost } from "@/utils/totalCost";
|
||||
import ChatGPT, {
|
||||
calculate_token_length,
|
||||
@@ -42,10 +42,12 @@ 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";
|
||||
|
||||
export default function ChatBOX() {
|
||||
const { db, selectedChatIndex, setSelectedChatIndex } =
|
||||
const { db, selectedChatIndex, setSelectedChatIndex, handleNewChatStore } =
|
||||
useContext(AppContext);
|
||||
const { langCode, setLangCode } = useContext(langCodeContext);
|
||||
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
|
||||
// prevent error
|
||||
const [inputMsg, setInputMsg] = useState("");
|
||||
@@ -73,13 +75,14 @@ export default function ChatBOX() {
|
||||
if (messagesEndRef.current === null) return;
|
||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}, [showRetry, showGenerating, generatingMessage]);
|
||||
}, [showRetry, showGenerating, generatingMessage, chatStore]);
|
||||
|
||||
const client = new ChatGPT(chatStore.apiKey);
|
||||
|
||||
const _completeWithStreamMode = async (
|
||||
response: Response
|
||||
): Promise<Usage> => {
|
||||
response: Response,
|
||||
signal: AbortSignal
|
||||
): Promise<ChatStoreMessage> => {
|
||||
let responseTokenCount = 0; // including reasoning content and normal content
|
||||
const allChunkMessage: string[] = [];
|
||||
const allReasoningContentChunk: string[] = [];
|
||||
@@ -90,7 +93,8 @@ export default function ChatBOX() {
|
||||
};
|
||||
let response_model_name: string | null = null;
|
||||
let usage: Usage | null = null;
|
||||
for await (const i of client.processStreamResponse(response)) {
|
||||
for await (const i of client.processStreamResponse(response, signal)) {
|
||||
if (signal?.aborted) break;
|
||||
response_model_name = i.model;
|
||||
responseTokenCount += 1;
|
||||
if (i.usage) {
|
||||
@@ -165,22 +169,7 @@ export default function ChatBOX() {
|
||||
const reasoning_content = allReasoningContentChunk.join("");
|
||||
|
||||
console.log("save logprobs", logprobs);
|
||||
const newMsg: ChatStoreMessage = {
|
||||
role: "assistant",
|
||||
content,
|
||||
reasoning_content,
|
||||
hide: false,
|
||||
token:
|
||||
responseTokenCount -
|
||||
(usage?.completion_tokens_details?.reasoning_tokens ?? 0),
|
||||
example: false,
|
||||
audio: null,
|
||||
logprobs,
|
||||
response_model_name,
|
||||
};
|
||||
if (allChunkTool.length > 0) newMsg.tool_calls = allChunkTool;
|
||||
|
||||
chatStore.history.push(newMsg);
|
||||
// manually copy status from client to chatStore
|
||||
chatStore.maxTokens = client.max_tokens;
|
||||
chatStore.tokenMargin = client.tokens_margin;
|
||||
@@ -209,14 +198,43 @@ export default function ChatBOX() {
|
||||
ret.completion_tokens_details = usage.completion_tokens_details ?? null;
|
||||
}
|
||||
|
||||
return ret;
|
||||
const newMsg: ChatStoreMessage = {
|
||||
role: "assistant",
|
||||
content,
|
||||
reasoning_content,
|
||||
hide: false,
|
||||
token:
|
||||
responseTokenCount -
|
||||
(usage?.completion_tokens_details?.reasoning_tokens ?? 0),
|
||||
example: false,
|
||||
audio: null,
|
||||
logprobs,
|
||||
response_model_name,
|
||||
usage,
|
||||
};
|
||||
if (allChunkTool.length > 0) newMsg.tool_calls = allChunkTool;
|
||||
|
||||
return newMsg;
|
||||
};
|
||||
|
||||
const _completeWithFetchMode = async (response: Response): Promise<Usage> => {
|
||||
const _completeWithFetchMode = async (
|
||||
response: Response
|
||||
): Promise<ChatStoreMessage> => {
|
||||
const data = (await response.json()) as FetchResponse;
|
||||
const msg = client.processFetchResponse(data);
|
||||
|
||||
chatStore.history.push({
|
||||
setShowGenerating(false);
|
||||
|
||||
const usage: Usage = {
|
||||
prompt_tokens: data.usage.prompt_tokens ?? 0,
|
||||
completion_tokens: data.usage.completion_tokens ?? 0,
|
||||
total_tokens: data.usage.total_tokens ?? 0,
|
||||
response_model_name: data.model ?? null,
|
||||
prompt_tokens_details: data.usage.prompt_tokens_details ?? null,
|
||||
completion_tokens_details: data.usage.completion_tokens_details ?? null,
|
||||
};
|
||||
|
||||
const ret: ChatStoreMessage = {
|
||||
role: "assistant",
|
||||
content: msg.content,
|
||||
tool_calls: msg.tool_calls,
|
||||
@@ -224,22 +242,13 @@ export default function ChatBOX() {
|
||||
token: data.usage?.completion_tokens_details
|
||||
? data.usage.completion_tokens -
|
||||
data.usage.completion_tokens_details.reasoning_tokens
|
||||
: data.usage.completion_tokens ?? calculate_token_length(msg.content),
|
||||
: (data.usage.completion_tokens ?? calculate_token_length(msg.content)),
|
||||
example: false,
|
||||
audio: null,
|
||||
logprobs: data.choices[0]?.logprobs,
|
||||
response_model_name: data.model,
|
||||
reasoning_content: data.choices[0]?.message?.reasoning_content ?? null,
|
||||
});
|
||||
setShowGenerating(false);
|
||||
|
||||
const ret: Usage = {
|
||||
prompt_tokens: data.usage.prompt_tokens ?? 0,
|
||||
completion_tokens: data.usage.completion_tokens ?? 0,
|
||||
total_tokens: data.usage.total_tokens ?? 0,
|
||||
response_model_name: data.model ?? null,
|
||||
prompt_tokens_details: data.usage.prompt_tokens_details ?? null,
|
||||
completion_tokens_details: data.usage.completion_tokens_details ?? null,
|
||||
usage,
|
||||
};
|
||||
|
||||
return ret;
|
||||
@@ -288,28 +297,47 @@ export default function ChatBOX() {
|
||||
client.max_gen_tokens = chatStore.maxGenTokens;
|
||||
client.enable_max_gen_tokens = chatStore.maxGenTokens_enabled;
|
||||
|
||||
const created_at = new Date();
|
||||
|
||||
try {
|
||||
setShowGenerating(true);
|
||||
abortControllerRef.current = new AbortController();
|
||||
const response = await client._fetch(
|
||||
chatStore.streamMode,
|
||||
chatStore.logprobs
|
||||
chatStore.logprobs,
|
||||
abortControllerRef.current.signal
|
||||
);
|
||||
const responsed_at = new Date();
|
||||
const contentType = response.headers.get("content-type");
|
||||
let usage: Usage;
|
||||
let cs: ChatStoreMessage;
|
||||
if (contentType?.startsWith("text/event-stream")) {
|
||||
usage = await _completeWithStreamMode(response);
|
||||
cs = await _completeWithStreamMode(
|
||||
response,
|
||||
abortControllerRef.current.signal
|
||||
);
|
||||
} else if (contentType?.startsWith("application/json")) {
|
||||
usage = await _completeWithFetchMode(response);
|
||||
cs = await _completeWithFetchMode(response);
|
||||
} else {
|
||||
throw `unknown response content type ${contentType}`;
|
||||
}
|
||||
const usage = cs.usage;
|
||||
if (!usage) {
|
||||
throw "panic: usage is null";
|
||||
}
|
||||
|
||||
const completed_at = new Date();
|
||||
cs.created_at = created_at.toISOString();
|
||||
cs.responsed_at = responsed_at.toISOString();
|
||||
cs.completed_at = completed_at.toISOString();
|
||||
|
||||
chatStore.history.push(cs);
|
||||
console.log("new chatStore", cs);
|
||||
|
||||
// manually copy status from client to chatStore
|
||||
chatStore.maxTokens = client.max_tokens;
|
||||
chatStore.tokenMargin = client.tokens_margin;
|
||||
chatStore.totalTokens = client.total_tokens;
|
||||
|
||||
console.log("usage", usage);
|
||||
// estimate user's input message token
|
||||
const aboveTokens = chatStore.history
|
||||
.filter(({ hide }) => !hide)
|
||||
@@ -357,7 +385,11 @@ export default function ChatBOX() {
|
||||
|
||||
setShowRetry(false);
|
||||
setChatStore({ ...chatStore });
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
if (error.name === "AbortError") {
|
||||
console.log("abort complete");
|
||||
return;
|
||||
}
|
||||
setShowRetry(true);
|
||||
alert(error);
|
||||
} finally {
|
||||
@@ -368,9 +400,9 @@ export default function ChatBOX() {
|
||||
|
||||
// when user click the "send" button or ctrl+Enter in the textarea
|
||||
const send = async (msg = "", call_complete = true) => {
|
||||
if (messagesEndRef.current) {
|
||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
setTimeout(() => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}, 0);
|
||||
|
||||
const inputMsg = msg.trim();
|
||||
if (!inputMsg && images.length === 0) {
|
||||
@@ -395,6 +427,7 @@ export default function ChatBOX() {
|
||||
logprobs: null,
|
||||
response_model_name: null,
|
||||
reasoning_content: null,
|
||||
usage: null,
|
||||
});
|
||||
|
||||
// manually calculate token length
|
||||
@@ -411,40 +444,44 @@ export default function ChatBOX() {
|
||||
};
|
||||
|
||||
const userInputRef = useRef<HTMLInputElement>(null);
|
||||
const abortControllerRef = useRef<AbortController>(new AbortController());
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grow flex flex-col p-2 w-full">
|
||||
<div className="grow flex flex-col w-full">
|
||||
<ChatMessageList>
|
||||
{chatStore.history.length === 0 && (
|
||||
<Alert variant="default" className="my-3">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle>
|
||||
{Tr("This is a new chat session, start by typing a message")}
|
||||
<Tr>This is a new chat session, start by typing a message</Tr>
|
||||
</AlertTitle>
|
||||
<AlertDescription className="flex flex-col gap-1 mt-5">
|
||||
<div className="flex items-center gap-2">
|
||||
<CornerRightUpIcon className="h-4 w-4" />
|
||||
<span>
|
||||
{Tr(
|
||||
"Settings button located at the top right corner can be used to change the settings of this chat"
|
||||
)}
|
||||
<Tr>
|
||||
Settings button located at the top right corner can be
|
||||
used to change the settings of this chat
|
||||
</Tr>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<CornerLeftUpIcon className="h-4 w-4" />
|
||||
<span>
|
||||
{Tr(
|
||||
"'New' button located at the top left corner can be used to create a new chat"
|
||||
)}
|
||||
<Tr>
|
||||
'New' button located at the top left corner can be used to
|
||||
create a new chat
|
||||
</Tr>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<ArrowDownToDotIcon className="h-4 w-4" />
|
||||
<span>
|
||||
{Tr(
|
||||
"All chat history and settings are stored in the local browser"
|
||||
)}
|
||||
<Tr>
|
||||
All chat history and settings are stored in the local
|
||||
browser
|
||||
</Tr>
|
||||
</span>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
@@ -489,7 +526,7 @@ export default function ChatBOX() {
|
||||
</ChatBubble>
|
||||
)}
|
||||
<p className="text-center">
|
||||
{chatStore.history.length > 0 && (
|
||||
{chatStore.history.length > 0 && !showGenerating && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
@@ -505,7 +542,35 @@ export default function ChatBOX() {
|
||||
await complete();
|
||||
}}
|
||||
>
|
||||
{Tr("Re-Generate")}
|
||||
<Tr>Re-Generate</Tr>
|
||||
</Button>
|
||||
)}
|
||||
{chatStore.history.length > 0 && !showGenerating && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="m-2"
|
||||
disabled={showGenerating}
|
||||
onClick={() => {
|
||||
handleNewChatStore();
|
||||
}}
|
||||
>
|
||||
<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 && (
|
||||
@@ -517,22 +582,24 @@ export default function ChatBOX() {
|
||||
await complete();
|
||||
}}
|
||||
>
|
||||
{Tr("Completion")}
|
||||
<Tr>Completion</Tr>
|
||||
</Button>
|
||||
)}
|
||||
</p>
|
||||
<p className="p-2 my-2 text-center opacity-50 dark:text-white">
|
||||
{chatStore.postBeginIndex !== 0 && (
|
||||
{chatStore.postBeginIndex !== 0 && (
|
||||
<p className="p-2 my-2 text-center opacity-50 dark:text-white">
|
||||
<Alert variant="default">
|
||||
<InfoIcon className="h-4 w-4" />
|
||||
<AlertTitle>{Tr("Chat History Notice")}</AlertTitle>
|
||||
<AlertTitle>
|
||||
<Tr>Chat History Notice</Tr>
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
{Tr("Info: chat history is too long, forget messages")}:{" "}
|
||||
<Tr>Info: chat history is too long, forget messages</Tr>:{" "}
|
||||
{chatStore.postBeginIndex}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</p>
|
||||
</p>
|
||||
)}
|
||||
<VersionHint />
|
||||
{showRetry && (
|
||||
<p className="text-right p-2 my-2 dark:text-white">
|
||||
@@ -543,12 +610,12 @@ export default function ChatBOX() {
|
||||
await complete();
|
||||
}}
|
||||
>
|
||||
{Tr("Retry")}
|
||||
<Tr>Retry</Tr>
|
||||
</Button>
|
||||
</p>
|
||||
)}
|
||||
<div ref={messagesEndRef as any}></div>
|
||||
</ChatMessageList>
|
||||
<div id="message-end" ref={messagesEndRef as any}></div>
|
||||
{images.length > 0 && (
|
||||
<div className="flex flex-wrap">
|
||||
{images.map((image, index) => (
|
||||
@@ -581,7 +648,7 @@ export default function ChatBOX() {
|
||||
<ChatInput
|
||||
value={inputMsg}
|
||||
ref={userInputRef as any}
|
||||
placeholder="Type your message here..."
|
||||
placeholder={tr("Type your message here...", langCode)}
|
||||
onChange={(event: any) => {
|
||||
setInputMsg(event.target.value);
|
||||
autoHeight(event.target);
|
||||
@@ -620,7 +687,7 @@ export default function ChatBOX() {
|
||||
autoHeight(userInputRef.current);
|
||||
}}
|
||||
>
|
||||
Send Message
|
||||
<Tr>Send</Tr>
|
||||
<CornerDownLeftIcon className="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
12
src/registerSW.ts
Normal file
12
src/registerSW.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", () => {
|
||||
navigator.serviceWorker
|
||||
.register("/service-worker.js")
|
||||
.then((registration) => {
|
||||
console.log("SW registered:", registration);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("SW registration failed:", error);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createContext } from "react";
|
||||
import { createContext, useContext } from "react";
|
||||
import MAP_zh_CN from "@/translate/zh_CN";
|
||||
|
||||
interface LangOption {
|
||||
@@ -20,7 +20,26 @@ const LANG_OPTIONS: Record<string, LangOption> = {
|
||||
},
|
||||
};
|
||||
|
||||
const langCodeContext = createContext("en-US");
|
||||
// lowercase all langMap keys
|
||||
Object.keys(LANG_OPTIONS).forEach((langCode) => {
|
||||
const langMap = LANG_OPTIONS[langCode].langMap;
|
||||
const newLangMap: Record<string, string> = {};
|
||||
Object.keys(langMap).forEach((key) => {
|
||||
newLangMap[key.toLowerCase()] = langMap[key];
|
||||
});
|
||||
LANG_OPTIONS[langCode].langMap = newLangMap;
|
||||
});
|
||||
|
||||
type LangCode = "en-US" | "zh-CN";
|
||||
|
||||
interface LangCodeContextSchema {
|
||||
langCode: LangCode;
|
||||
setLangCode: (langCode: LangCode) => void;
|
||||
}
|
||||
const langCodeContext = createContext<LangCodeContextSchema>({
|
||||
langCode: "en-US",
|
||||
setLangCode: () => {},
|
||||
});
|
||||
|
||||
function tr(text: string, langCode: "en-US" | "zh-CN") {
|
||||
const option = LANG_OPTIONS[langCode];
|
||||
@@ -31,18 +50,18 @@ function tr(text: string, langCode: "en-US" | "zh-CN") {
|
||||
|
||||
const translatedText = langMap[text.toLowerCase()];
|
||||
if (translatedText === undefined) {
|
||||
console.log(`[Translation] not found for "${text}"`);
|
||||
return text;
|
||||
}
|
||||
|
||||
return translatedText;
|
||||
}
|
||||
|
||||
function Tr(text: string) {
|
||||
function Tr({ children }: { children: string }) {
|
||||
return (
|
||||
<langCodeContext.Consumer>
|
||||
{/* @ts-ignore */}
|
||||
{({ langCode }) => {
|
||||
return tr(text, langCode);
|
||||
return tr(children, langCode);
|
||||
}}
|
||||
</langCodeContext.Consumer>
|
||||
);
|
||||
|
||||
@@ -62,6 +62,85 @@ const LANG_MAP: Record<string, string> = {
|
||||
example: "示例",
|
||||
render: "渲染",
|
||||
"reset current": "清空当前会话",
|
||||
"send message": "发送",
|
||||
"type your message here...": "在此输入消息...",
|
||||
"total tokens": "总token数",
|
||||
"session cost": "本会话消费",
|
||||
"accumulated cost": "累计消费",
|
||||
session: "会话",
|
||||
"you can customize all the settings here": "您可以在此处自定义所有设置",
|
||||
"Image Gen API": "图片生成 API",
|
||||
"Image generation API endpoint. Service is enabled when this is set. Default: https://api.openai.com/v1/images/generations":
|
||||
"图片生成 API 端点。设置后服务将启用。默认: https://api.openai.com/v1/images/generations",
|
||||
"Image generation service API key. Defaults to the OpenAI key above, but can be configured separately here":
|
||||
"图片生成服务 API 密钥。默认为上面的 OpenAI 密钥,但可以在此处单独配置",
|
||||
"Adjust the playback speed of text-to-speech": "调整文本转语音的播放速度",
|
||||
"TTS API endpoint. Service is enabled when this is set. Default: https://api.openai.com/v1/audio/speech":
|
||||
"TTS API 端点。设置后服务将启用。默认: https://api.openai.com/v1/audio/speech",
|
||||
"Text-to-speech service API key. Defaults to the OpenAI key above, but can be configured separately here":
|
||||
"文本转语音服务 API 密钥。默认为上面的 OpenAI 密钥,但可以在此处单独配置",
|
||||
"Whisper speech-to-text service. Service is enabled when this is set. Default: https://api.openai.com/v1/audio/transriptions":
|
||||
"Whisper 语音转文本服务。设置后服务将启用。默认: https://api.openai.com/v1/audio/transriptions",
|
||||
"Used for Whisper service. Defaults to the OpenAI key above, but can be configured separately here":
|
||||
"用于 Whisper 服务。默认为上面的 OpenAI 密钥,但可以在此处单独配置",
|
||||
"Frequency Penalty": "频率惩罚",
|
||||
"Presence Penalty": "存在惩罚",
|
||||
"Top P sampling method. It is recommended to choose one of the temperature sampling methods, do not enable both at the same time.":
|
||||
"Top P 采样方法。建议选择其中一种温度采样方法,不要同时启用两种。",
|
||||
"Total token count, this parameter will be updated every time you chat, in stream mode this parameter is an estimate":
|
||||
"总 token 数,此参数将在每次对话时更新,在流式模式下此参数是一个估计",
|
||||
"Indicates how many history messages to 'forget' when sending API requests":
|
||||
"发送 API 请求时遗忘多少历史消息",
|
||||
'When totalTokens > maxTokens - tokenMargin, the history message will be truncated, chatgpt will "forget" part of the messages in the conversation (but all history messages are still saved locally)':
|
||||
"当 totalTokens > maxTokens - tokenMargin 时,历史消息将被截断,chatgpt 将“遗忘”对话中的部分消息(但所有历史消息仍然在本地保存)",
|
||||
"maxGenTokens is the maximum number of tokens that can be generated in a scingle request.":
|
||||
"maxGenTokens 是一次请求中可以生成的最大 token 数。",
|
||||
"Logprobs, return the probability of each token":
|
||||
"Logprobs,返回每个 token 的概率",
|
||||
"Stream Mode, use stream mode to see the generated content dynamically, but the token count cannot be accurately calculated, which may cause too much or too little history messages to be truncated when the token count is too large.":
|
||||
"流式模式,使用流式模式动态查看生成的内容,但无法准确计算 token 数,当 token 数过大时可能导致截断过多或过少的历史消息。",
|
||||
"Temperature, the higher the value, the higher the randomness of the generated text.":
|
||||
"温度,值越高,生成文本的随机性越高。",
|
||||
"Model, Different models have different performance and pricing, please refer to the API documentation":
|
||||
"模型,不同的模型具有不同的性能和定价,请参考 API 文档",
|
||||
"Chat API": "对话 API",
|
||||
"API endpoint, useful for using reverse proxy services in unsupported regions, default to https://api.openai.com/v1/chat/completions":
|
||||
"API 端点,用于在不受支持的地区使用反向代理服务,默认为 https://api.openai.com/v1/chat/completions",
|
||||
"OpenAI API key, do not leak this key": "OpenAI API 密钥,请勿泄漏",
|
||||
"Select language": "选择语言",
|
||||
"Develop Mode, enable to show more options and features":
|
||||
"开发者模式,启用以显示更多选项和功能",
|
||||
"Saved Template": "已保存模板",
|
||||
"Image Generation": "图片生成",
|
||||
TTS: "文本转语音",
|
||||
"Speech Recognition": "语音识别",
|
||||
System: "系统",
|
||||
"Max context token count. This value will be set automatically based on the selected model.":
|
||||
"最大上下文 token 数。此值将根据所选模型自动设置。",
|
||||
chat: "对话",
|
||||
save: "保存",
|
||||
"Configure speech recognition settings": "配置语音识别设置",
|
||||
"Whisper API": "Whisper API",
|
||||
Custom: "自定义",
|
||||
"Configure the LLM API settings": "配置 LLM API 设置",
|
||||
"TTS Voice": "TTS 语音",
|
||||
"TTS Format": "TTS 格式",
|
||||
Formats: "格式",
|
||||
"TTS API": "TTS API",
|
||||
"Configure text-to-speech settings": "配置文本转语音设置",
|
||||
Voices: "语音",
|
||||
"in all sessions": "所有会话",
|
||||
"Reset Total Cost": "重置总消费",
|
||||
Language: "语言",
|
||||
"Quick Actions": "快速操作",
|
||||
Import: "导入",
|
||||
Languages: "语言",
|
||||
"maxGenTokens is the maximum number of tokens that can be generated in a single request.":
|
||||
"maxGenTokens 是一次请求中可以生成的最大 token 数。",
|
||||
"Image Generation API": "图片生成 API",
|
||||
"Configure image generation settings": "配置图片生成设置",
|
||||
"New Chat": "新对话",
|
||||
"Delete Chat": "删除对话",
|
||||
};
|
||||
|
||||
export default LANG_MAP;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Logprobs, Message, MessageDetail, ToolCall } from "@/chatgpt";
|
||||
import { Logprobs, Message, MessageDetail, ToolCall, Usage } from "@/chatgpt";
|
||||
|
||||
/**
|
||||
* ChatStore is the main object of the chatgpt-api-web,
|
||||
@@ -71,6 +71,11 @@ export interface ChatStoreMessage {
|
||||
audio: Blob | null;
|
||||
logprobs: Logprobs | null;
|
||||
response_model_name: string | null;
|
||||
usage: Usage | null;
|
||||
|
||||
created_at?: string;
|
||||
responsed_at?: string;
|
||||
completed_at?: string;
|
||||
|
||||
role: "system" | "user" | "assistant" | "tool";
|
||||
content: string | MessageDetail[];
|
||||
|
||||
@@ -1,9 +1,30 @@
|
||||
import { defineConfig } from "vite";
|
||||
import path from "path";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { VitePWA } from "vite-plugin-pwa";
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [],
|
||||
plugins: [
|
||||
react(),
|
||||
VitePWA({
|
||||
registerType: "autoUpdate",
|
||||
includeAssets: ['favicon.png'],
|
||||
manifest: {
|
||||
name: "ChatGPT-API-WEB",
|
||||
short_name: "CAW",
|
||||
description: "ChatGPT API Web Interface",
|
||||
theme_color: "#000000",
|
||||
icons: [
|
||||
{
|
||||
src: "favicon.png",
|
||||
sizes: "256x256",
|
||||
type: "image/png",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
base: "./",
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
Reference in New Issue
Block a user