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
|
- name: Setup Pages
|
||||||
uses: actions/configure-pages@v3
|
uses: actions/configure-pages@v3
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-pages-artifact@v1
|
uses: actions/upload-pages-artifact@v3
|
||||||
with:
|
with:
|
||||||
# Upload entire repository
|
# Upload entire repository
|
||||||
path: './dist/'
|
path: './dist/'
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
id: deployment
|
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"
|
content="A simple API playground for OpenAI ChatGPT API"
|
||||||
/>
|
/>
|
||||||
<title>ChatGPT API Web</title>
|
<title>ChatGPT API Web</title>
|
||||||
|
<link rel="manifest" href="manifest.json" />
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<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/react-dom": "^18.0.0",
|
||||||
"@types/ungap__structured-clone": "^1.2.0",
|
"@types/ungap__structured-clone": "^1.2.0",
|
||||||
"@ungap/structured-clone": "^1.2.1",
|
"@ungap/structured-clone": "^1.2.1",
|
||||||
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "1.0.4",
|
"cmdk": "1.0.4",
|
||||||
@@ -74,6 +75,8 @@
|
|||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"theme-change": "^2.5.0",
|
"theme-change": "^2.5.0",
|
||||||
"typescript": "^5.7.2",
|
"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 { DefaultModel } from "@/const";
|
||||||
|
import { MutableRefObject } from "react";
|
||||||
|
|
||||||
export interface ImageURL {
|
export interface ImageURL {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -91,7 +92,7 @@ export const getMessageText = (message: Message): string => {
|
|||||||
})
|
})
|
||||||
.join("\n");
|
.join("\n");
|
||||||
}
|
}
|
||||||
return message.content;
|
return message.content.trim();
|
||||||
}
|
}
|
||||||
return message.content
|
return message.content
|
||||||
.filter((c) => c.type === "text")
|
.filter((c) => c.type === "text")
|
||||||
@@ -213,7 +214,7 @@ class Chat {
|
|||||||
this.json_mode = json_mode;
|
this.json_mode = json_mode;
|
||||||
}
|
}
|
||||||
|
|
||||||
_fetch(stream = false, logprobs = false) {
|
_fetch(stream = false, logprobs = false, signal: AbortSignal) {
|
||||||
// perform role type check
|
// perform role type check
|
||||||
let hasNonSystemMessage = false;
|
let hasNonSystemMessage = false;
|
||||||
for (const msg of this.messages) {
|
for (const msg of this.messages) {
|
||||||
@@ -301,10 +302,11 @@ class Chat {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(body),
|
body: JSON.stringify(body),
|
||||||
|
signal,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async *processStreamResponse(resp: Response) {
|
async *processStreamResponse(resp: Response, signal?: AbortSignal) {
|
||||||
const reader = resp?.body?.pipeThrough(new TextDecoderStream()).getReader();
|
const reader = resp?.body?.pipeThrough(new TextDecoderStream()).getReader();
|
||||||
if (reader === undefined) {
|
if (reader === undefined) {
|
||||||
console.log("reader is undefined");
|
console.log("reader is undefined");
|
||||||
@@ -313,6 +315,11 @@ class Chat {
|
|||||||
let receiving = true;
|
let receiving = true;
|
||||||
let buffer = "";
|
let buffer = "";
|
||||||
while (receiving) {
|
while (receiving) {
|
||||||
|
if (signal?.aborted) {
|
||||||
|
reader.cancel();
|
||||||
|
console.log("signal aborted in stream response");
|
||||||
|
break;
|
||||||
|
}
|
||||||
const { value, done } = await reader.read();
|
const { value, done } = await reader.read();
|
||||||
if (done) break;
|
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,
|
logprobs: null,
|
||||||
response_model_name: imageGenModel,
|
response_model_name: imageGenModel,
|
||||||
reasoning_content: null,
|
reasoning_content: null,
|
||||||
|
usage: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
setChatStore({ ...chatStore });
|
setChatStore({ ...chatStore });
|
||||||
@@ -221,7 +222,7 @@ export function ImageGenDrawer({ disableFactor }: Props) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("Generate")}
|
<Tr>Generate</Tr>
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -151,7 +151,9 @@ function ToolsDropdownList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center space-x-4 mx-3">
|
<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}>
|
<Popover open={open} onOpenChange={setOpen}>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant="outline" className="w-[150px] justify-start">
|
<Button variant="outline" className="w-[150px] justify-start">
|
||||||
@@ -164,7 +166,9 @@ function ToolsDropdownList() {
|
|||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>+ {Tr(`Set tools`)}</>
|
<>
|
||||||
|
+ <Tr>Set tools</Tr>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -172,7 +176,9 @@ function ToolsDropdownList() {
|
|||||||
<Command>
|
<Command>
|
||||||
<CommandInput placeholder="You can search..." />
|
<CommandInput placeholder="You can search..." />
|
||||||
<CommandList>
|
<CommandList>
|
||||||
<CommandEmpty>{Tr(`No results found.`)}</CommandEmpty>
|
<CommandEmpty>
|
||||||
|
<Tr>No results found.</Tr>
|
||||||
|
</CommandEmpty>
|
||||||
<CommandGroup>
|
<CommandGroup>
|
||||||
{chatStore.toolsString && (
|
{chatStore.toolsString && (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
@@ -188,7 +194,7 @@ function ToolsDropdownList() {
|
|||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BrushIcon /> {Tr(`Clear tools`)}
|
<BrushIcon /> <Tr>Clear tools</Tr>
|
||||||
</CommandItem>
|
</CommandItem>
|
||||||
)}
|
)}
|
||||||
{ctx.templateTools.map((t, index) => (
|
{ctx.templateTools.map((t, index) => (
|
||||||
@@ -247,15 +253,10 @@ const ChatTemplateItem = ({
|
|||||||
const { templates, setTemplates } = useContext(AppContext);
|
const { templates, setTemplates } = useContext(AppContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li>
|
<li
|
||||||
<NavigationMenuLink asChild>
|
|
||||||
<a
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
// Update chatStore with the selected template
|
// Update chatStore with the selected template
|
||||||
if (
|
if (chatStore.history.length > 0 || chatStore.systemMessageContent) {
|
||||||
chatStore.history.length > 0 ||
|
|
||||||
chatStore.systemMessageContent
|
|
||||||
) {
|
|
||||||
console.log("you clicked", t.name);
|
console.log("you clicked", t.name);
|
||||||
const confirm = window.confirm(
|
const confirm = window.confirm(
|
||||||
"This will replace the current chat history. Are you sure?"
|
"This will replace the current chat history. Are you sure?"
|
||||||
@@ -264,6 +265,9 @@ const ChatTemplateItem = ({
|
|||||||
}
|
}
|
||||||
setChatStore({ ...newChatStore({ ...chatStore, ...t }) });
|
setChatStore({ ...newChatStore({ ...chatStore, ...t }) });
|
||||||
}}
|
}}
|
||||||
|
>
|
||||||
|
<NavigationMenuLink asChild>
|
||||||
|
<a
|
||||||
className={cn(
|
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"
|
"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 remarkMath from "remark-math";
|
||||||
import rehypeKatex from "rehype-katex";
|
import rehypeKatex from "rehype-katex";
|
||||||
import "katex/dist/katex.min.css";
|
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 { ChatStoreMessage } from "@/types/chatstore";
|
||||||
import { addTotalCost } from "@/utils/totalCost";
|
import { addTotalCost } from "@/utils/totalCost";
|
||||||
|
|
||||||
@@ -254,12 +260,16 @@ export default function Message(props: { messageIndex: number }) {
|
|||||||
const [renderMarkdown, setRenderWorkdown] = useState(defaultRenderMD);
|
const [renderMarkdown, setRenderWorkdown] = useState(defaultRenderMD);
|
||||||
const [renderColor, setRenderColor] = useState(false);
|
const [renderColor, setRenderColor] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setRenderWorkdown(defaultRenderMD);
|
||||||
|
}, [defaultRenderMD]);
|
||||||
|
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
const copyToClipboard = async (text: string) => {
|
const copyToClipboard = async (text: string) => {
|
||||||
try {
|
try {
|
||||||
await navigator.clipboard.writeText(text);
|
await navigator.clipboard.writeText(text);
|
||||||
toast({
|
toast({
|
||||||
description: Tr("Message copied to clipboard!"),
|
description: <Tr>Message copied to clipboard!</Tr>,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const textArea = document.createElement("textarea");
|
const textArea = document.createElement("textarea");
|
||||||
@@ -269,11 +279,11 @@ export default function Message(props: { messageIndex: number }) {
|
|||||||
try {
|
try {
|
||||||
document.execCommand("copy");
|
document.execCommand("copy");
|
||||||
toast({
|
toast({
|
||||||
description: Tr("Message copied to clipboard!"),
|
description: <Tr>Message copied to clipboard!</Tr>,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
toast({
|
toast({
|
||||||
description: Tr("Failed to copy to clipboard"),
|
description: <Tr>Failed to copy to clipboard</Tr>,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
document.body.removeChild(textArea);
|
document.body.removeChild(textArea);
|
||||||
@@ -297,7 +307,7 @@ export default function Message(props: { messageIndex: number }) {
|
|||||||
{chat.role === "assistant" ? (
|
{chat.role === "assistant" ? (
|
||||||
<div className="border-b border-border dark:border-border-dark pb-4">
|
<div className="border-b border-border dark:border-border-dark pb-4">
|
||||||
{chat.reasoning_content ? (
|
{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 justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h4 className="text-sm font-semibold text-gray-500">
|
<h4 className="text-sm font-semibold text-gray-500">
|
||||||
@@ -311,8 +321,8 @@ export default function Message(props: { messageIndex: number }) {
|
|||||||
</CollapsibleTrigger>
|
</CollapsibleTrigger>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CollapsibleContent className="ml-5 text-gray-500">
|
<CollapsibleContent className="ml-5 text-gray-500 message-content">
|
||||||
{chat.reasoning_content}
|
{chat.reasoning_content.trim()}
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -324,7 +334,7 @@ export default function Message(props: { messageIndex: number }) {
|
|||||||
) : chat.tool_calls ? (
|
) : chat.tool_calls ? (
|
||||||
<MessageToolCall chat={chat} copyToClipboard={copyToClipboard} />
|
<MessageToolCall chat={chat} copyToClipboard={copyToClipboard} />
|
||||||
) : renderMarkdown ? (
|
) : renderMarkdown ? (
|
||||||
<div className="message-content max-w-full md:max-w-[75%]">
|
<div className="message-content max-w-full md:max-w-[100%]">
|
||||||
<Markdown
|
<Markdown
|
||||||
remarkPlugins={[remarkMath]}
|
remarkPlugins={[remarkMath]}
|
||||||
rehypePlugins={[rehypeKatex]}
|
rehypePlugins={[rehypeKatex]}
|
||||||
@@ -356,7 +366,7 @@ export default function Message(props: { messageIndex: number }) {
|
|||||||
</Markdown>
|
</Markdown>
|
||||||
</div>
|
</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.content &&
|
||||||
(chat.logprobs && renderColor
|
(chat.logprobs && renderColor
|
||||||
? chat.logprobs.content
|
? chat.logprobs.content
|
||||||
@@ -374,6 +384,7 @@ export default function Message(props: { messageIndex: number }) {
|
|||||||
: getMessageText(chat))}
|
: getMessageText(chat))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<TTSPlay chat={chat} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex md:opacity-0 hover:opacity-100 transition-opacity">
|
<div className="flex md:opacity-0 hover:opacity-100 transition-opacity">
|
||||||
<ChatBubbleAction
|
<ChatBubbleAction
|
||||||
@@ -408,7 +419,6 @@ export default function Message(props: { messageIndex: number }) {
|
|||||||
{chatStore.tts_api && chatStore.tts_key && (
|
{chatStore.tts_api && chatStore.tts_key && (
|
||||||
<TTSButton chat={chat} />
|
<TTSButton chat={chat} />
|
||||||
)}
|
)}
|
||||||
<TTSPlay chat={chat} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -443,6 +453,7 @@ export default function Message(props: { messageIndex: number }) {
|
|||||||
: getMessageText(chat))}
|
: getMessageText(chat))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<TTSPlay chat={chat} />
|
||||||
</ChatBubbleMessage>
|
</ChatBubbleMessage>
|
||||||
<ChatBubbleActionWrapper>
|
<ChatBubbleActionWrapper>
|
||||||
<ChatBubbleAction
|
<ChatBubbleAction
|
||||||
@@ -477,7 +488,6 @@ export default function Message(props: { messageIndex: number }) {
|
|||||||
{chatStore.tts_api && chatStore.tts_key && (
|
{chatStore.tts_api && chatStore.tts_key && (
|
||||||
<TTSButton chat={chat} />
|
<TTSButton chat={chat} />
|
||||||
)}
|
)}
|
||||||
<TTSPlay chat={chat} />
|
|
||||||
</ChatBubbleActionWrapper>
|
</ChatBubbleActionWrapper>
|
||||||
</ChatBubble>
|
</ChatBubble>
|
||||||
)}
|
)}
|
||||||
@@ -529,7 +539,9 @@ export default function Message(props: { messageIndex: number }) {
|
|||||||
setChatStore({ ...chatStore });
|
setChatStore({ ...chatStore });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm font-medium">{Tr("example")}</span>
|
<span className="text-sm font-medium">
|
||||||
|
<Tr>example</Tr>
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
@@ -538,7 +550,9 @@ export default function Message(props: { messageIndex: number }) {
|
|||||||
checked={renderMarkdown}
|
checked={renderMarkdown}
|
||||||
onChange={() => setRenderWorkdown(!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>
|
||||||
<label className="flex items-center gap-2">
|
<label className="flex items-center gap-2">
|
||||||
<input
|
<input
|
||||||
@@ -547,7 +561,9 @@ export default function Message(props: { messageIndex: number }) {
|
|||||||
checked={renderColor}
|
checked={renderColor}
|
||||||
onChange={() => setRenderColor(!renderColor)}
|
onChange={() => setRenderColor(!renderColor)}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm font-medium">{Tr("color")}</span>
|
<span className="text-sm font-medium">
|
||||||
|
<Tr>color</Tr>
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
{chat.response_model_name && (
|
{chat.response_model_name && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ import { Slider } from "@/components/ui/slider";
|
|||||||
import { NonOverflowScrollArea, ScrollArea } from "@/components/ui/scroll-area";
|
import { NonOverflowScrollArea, ScrollArea } from "@/components/ui/scroll-area";
|
||||||
import { AppChatStoreContext, AppContext } from "@/pages/App";
|
import { AppChatStoreContext, AppContext } from "@/pages/App";
|
||||||
import { toast } from "@/hooks/use-toast";
|
import { toast } from "@/hooks/use-toast";
|
||||||
import { title } from "process";
|
|
||||||
|
|
||||||
const TTS_VOICES: string[] = [
|
const TTS_VOICES: string[] = [
|
||||||
"alloy",
|
"alloy",
|
||||||
@@ -132,7 +131,7 @@ const SelectModel = (props: { help: string }) => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Label className="flex items-center gap-2">
|
<Label className="flex items-center gap-2">
|
||||||
<CogIcon className="w-4 h-4" />
|
<CogIcon className="w-4 h-4" />
|
||||||
{Tr("Custom")}
|
<Tr>Custom</Tr>
|
||||||
</Label>
|
</Label>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={useCustomModel}
|
checked={useCustomModel}
|
||||||
@@ -764,7 +763,7 @@ export default (props: {}) => {
|
|||||||
<Sheet open={open} onOpenChange={setOpen}>
|
<Sheet open={open} onOpenChange={setOpen}>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
<Button variant="outline" className="flex-grow">
|
<Button variant="outline" className="flex-grow">
|
||||||
{Tr("Settings")}
|
<Tr>Settings</Tr>
|
||||||
{(!chatStore.apiKey || !chatStore.apiEndpoint) && (
|
{(!chatStore.apiKey || !chatStore.apiEndpoint) && (
|
||||||
<TriangleAlertIcon className="w-4 h-4 ml-1 text-yellow-500" />
|
<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">
|
<SheetContent className="flex flex-col overflow-scroll">
|
||||||
<NonOverflowScrollArea>
|
<NonOverflowScrollArea>
|
||||||
<SheetHeader>
|
<SheetHeader>
|
||||||
<SheetTitle>{Tr("Settings")}</SheetTitle>
|
<SheetTitle>
|
||||||
|
<Tr>Settings</Tr>
|
||||||
|
</SheetTitle>
|
||||||
<SheetDescription>
|
<SheetDescription>
|
||||||
You can customize the settings here.
|
<Tr>You can customize all the settings here</Tr>
|
||||||
</SheetDescription>
|
</SheetDescription>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<Accordion type="multiple" className="w-full">
|
<Accordion type="multiple" className="w-full">
|
||||||
<AccordionItem value="session">
|
<AccordionItem value="session">
|
||||||
<AccordionTrigger>Session</AccordionTrigger>
|
<AccordionTrigger>
|
||||||
|
<Tr>Session</Tr>
|
||||||
|
</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
|
<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() && (
|
{chatStore.toolsString.trim() && (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline">{Tr(`Save Tools`)}</Button>
|
<Button variant="outline">
|
||||||
|
<Tr>Save Tools</Tr>
|
||||||
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -894,14 +899,20 @@ export default (props: {}) => {
|
|||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem value="system">
|
<AccordionItem value="system">
|
||||||
<AccordionTrigger>System</AccordionTrigger>
|
<AccordionTrigger>
|
||||||
|
<Tr>System</Tr>
|
||||||
|
</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<>
|
<>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
|
<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">
|
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
|
||||||
<CardTitle>Accumulated Cost</CardTitle>
|
<CardTitle>
|
||||||
<CardDescription>in all sessions</CardDescription>
|
<Tr>Accumulated Cost</Tr>
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
<Tr>in all sessions</Tr>
|
||||||
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex">
|
<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">
|
<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());
|
setTotalCost(getTotalCost());
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Reset Total Cost
|
<Tr>Reset Total Cost</Tr>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Choice
|
<Choice
|
||||||
field="develop_mode"
|
field="develop_mode"
|
||||||
help="开发者模式,开启后会显示更多选项及功能"
|
help={tr(
|
||||||
|
"Develop Mode, enable to show more options and features",
|
||||||
|
langCode
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<DefaultRenderMDCheckbox />
|
<DefaultRenderMDCheckbox />
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Language</Label>
|
<Label>
|
||||||
|
<Tr>Language</Tr>
|
||||||
|
</Label>
|
||||||
<Select value={langCode} onValueChange={setLangCode}>
|
<Select value={langCode} onValueChange={setLangCode}>
|
||||||
<SelectTrigger className="w-full">
|
<SelectTrigger className="w-full">
|
||||||
<SelectValue placeholder="Select language" />
|
<SelectValue
|
||||||
|
placeholder={tr("Select language", langCode)}
|
||||||
|
/>
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectLabel>Languages</SelectLabel>
|
<SelectLabel>
|
||||||
|
<Tr>Languages</Tr>
|
||||||
|
</SelectLabel>
|
||||||
{Object.keys(LANG_OPTIONS).map((opt) => (
|
{Object.keys(LANG_OPTIONS).map((opt) => (
|
||||||
<SelectItem key={opt} value={opt}>
|
<SelectItem key={opt} value={opt}>
|
||||||
{LANG_OPTIONS[opt].name}
|
{LANG_OPTIONS[opt].name}
|
||||||
@@ -954,7 +974,9 @@ export default (props: {}) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label>Quick Actions</Label>
|
<Label>
|
||||||
|
<Tr>Quick Actions</Tr>
|
||||||
|
</Label>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -967,23 +989,25 @@ export default (props: {}) => {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("Copy Setting Link")}
|
<Tr>Copy Setting Link</Tr>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="destructive" className="w-full">
|
<Button variant="destructive" className="w-full">
|
||||||
{Tr("Clear History")}
|
<Tr>Clear History</Tr>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
Are you absolutely sure?
|
<Tr>Are you absolutely sure?</Tr>
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
|
<Tr>
|
||||||
This action cannot be undone. This will
|
This action cannot be undone. This will
|
||||||
permanently delete all chat history.
|
permanently delete all chat history.
|
||||||
|
</Tr>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@@ -998,7 +1022,7 @@ export default (props: {}) => {
|
|||||||
setChatStore({ ...chatStore });
|
setChatStore({ ...chatStore });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Yes, clear all history
|
<Tr>Yes, clear all history</Tr>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -1025,7 +1049,7 @@ export default (props: {}) => {
|
|||||||
downloadAnchorNode.remove();
|
downloadAnchorNode.remove();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("Export")}
|
<Tr>Export</Tr>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -1047,7 +1071,7 @@ export default (props: {}) => {
|
|||||||
setTemplates([...templates]);
|
setTemplates([...templates]);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("As template")}
|
<Tr>As template</Tr>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
@@ -1067,7 +1091,7 @@ export default (props: {}) => {
|
|||||||
importFileRef.current.click();
|
importFileRef.current.click();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Import
|
<Tr>Import</Tr>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
@@ -1116,28 +1140,38 @@ export default (props: {}) => {
|
|||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem value="chat">
|
<AccordionItem value="chat">
|
||||||
<AccordionTrigger>Chat</AccordionTrigger>
|
<AccordionTrigger>
|
||||||
|
<Tr>Chat</Tr>
|
||||||
|
</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Chat API</CardTitle>
|
<CardTitle>
|
||||||
|
<Tr>Chat API</Tr>
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Configure the LLM API settings
|
<Tr>Configure the LLM API settings</Tr>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<InputField
|
<InputField
|
||||||
field="apiKey"
|
field="apiKey"
|
||||||
help="OPEN AI API 密钥,请勿泄漏此密钥"
|
help={tr(
|
||||||
|
"OpenAI API key, do not leak this key",
|
||||||
|
langCode
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
field="apiEndpoint"
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<SetAPIsTemplate
|
<SetAPIsTemplate
|
||||||
label="Chat API"
|
label={tr("Chat API", langCode)}
|
||||||
endpoint={chatStore.apiEndpoint}
|
endpoint={chatStore.apiEndpoint}
|
||||||
APIkey={chatStore.apiKey}
|
APIkey={chatStore.apiKey}
|
||||||
temps={templateAPIs}
|
temps={templateAPIs}
|
||||||
@@ -1147,54 +1181,81 @@ export default (props: {}) => {
|
|||||||
</Card>
|
</Card>
|
||||||
<Separator className="my-3" />
|
<Separator className="my-3" />
|
||||||
<SelectModel
|
<SelectModel
|
||||||
help="模型,默认 3.5。不同模型性能和定价也不同,请参考 API 文档。"
|
help={tr(
|
||||||
|
"Model, Different models have different performance and pricing, please refer to the API documentation",
|
||||||
|
langCode
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<Slicer
|
<Slicer
|
||||||
field="temperature"
|
field="temperature"
|
||||||
min={0}
|
min={0}
|
||||||
max={2}
|
max={2}
|
||||||
help="温度,数值越大模型生成文字的随机性越高。"
|
help={tr(
|
||||||
|
"Temperature, the higher the value, the higher the randomness of the generated text.",
|
||||||
|
langCode
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<Choice
|
<Choice
|
||||||
field="streamMode"
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<Choice
|
<Choice
|
||||||
field="logprobs"
|
field="logprobs"
|
||||||
help="返回每个Token的概率"
|
help={tr(
|
||||||
|
"Logprobs, return the probability of each token",
|
||||||
|
langCode
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<Number
|
<Number
|
||||||
field="maxTokens"
|
field="maxTokens"
|
||||||
help="最大上下文 token 数量。此值会根据选择的模型自动设置。"
|
help={tr(
|
||||||
|
"Max context token count. This value will be set automatically based on the selected model.",
|
||||||
|
langCode
|
||||||
|
)}
|
||||||
readOnly={false}
|
readOnly={false}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<Number
|
<Number
|
||||||
field="maxGenTokens"
|
field="maxGenTokens"
|
||||||
help="最大生成 Tokens 数量,可选值。"
|
help={tr(
|
||||||
|
"maxGenTokens is the maximum number of tokens that can be generated in a single request.",
|
||||||
|
langCode
|
||||||
|
)}
|
||||||
readOnly={false}
|
readOnly={false}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<Number
|
<Number
|
||||||
field="tokenMargin"
|
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}
|
readOnly={false}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<Choice field="json_mode" help="JSON Mode" {...props} />
|
<Choice field="json_mode" help="JSON Mode" {...props} />
|
||||||
<Number
|
<Number
|
||||||
field="postBeginIndex"
|
field="postBeginIndex"
|
||||||
help="指示发送 API 请求时要”忘记“多少历史消息"
|
help={tr(
|
||||||
|
"Indicates how many history messages to 'forget' when sending API requests",
|
||||||
|
langCode
|
||||||
|
)}
|
||||||
readOnly={true}
|
readOnly={true}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<Number
|
<Number
|
||||||
field="totalTokens"
|
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}
|
readOnly={true}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -1202,43 +1263,56 @@ export default (props: {}) => {
|
|||||||
field="top_p"
|
field="top_p"
|
||||||
min={0}
|
min={0}
|
||||||
max={1}
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<Number
|
<Number
|
||||||
field="presence_penalty"
|
field="presence_penalty"
|
||||||
help="存在惩罚度"
|
help={tr("Presence Penalty", langCode)}
|
||||||
readOnly={false}
|
readOnly={false}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<Number
|
<Number
|
||||||
field="frequency_penalty"
|
field="frequency_penalty"
|
||||||
help="频率惩罚度"
|
help={tr("Frequency Penalty", langCode)}
|
||||||
readOnly={false}
|
readOnly={false}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem value="speech">
|
<AccordionItem value="speech">
|
||||||
<AccordionTrigger>Speech Recognition</AccordionTrigger>
|
<AccordionTrigger>
|
||||||
|
<Tr>Speech Recognition</Tr>
|
||||||
|
</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Whisper API</CardTitle>
|
<CardTitle>
|
||||||
|
<Tr>Whisper API</Tr>
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Configure speech recognition settings
|
<Tr>Configure speech recognition settings</Tr>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<InputField
|
<InputField
|
||||||
field="whisper_key"
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
field="whisper_api"
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<SetAPIsTemplate
|
<SetAPIsTemplate
|
||||||
@@ -1254,24 +1328,34 @@ export default (props: {}) => {
|
|||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem value="tts">
|
<AccordionItem value="tts">
|
||||||
<AccordionTrigger>TTS</AccordionTrigger>
|
<AccordionTrigger>
|
||||||
|
<Tr>TTS</Tr>
|
||||||
|
</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>TTS API</CardTitle>
|
<CardTitle>
|
||||||
|
<Tr>TTS API</Tr>
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Configure text-to-speech settings
|
<Tr>Configure text-to-speech settings</Tr>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<InputField
|
<InputField
|
||||||
field="tts_key"
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
field="tts_api"
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<SetAPIsTemplate
|
<SetAPIsTemplate
|
||||||
@@ -1286,7 +1370,7 @@ export default (props: {}) => {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="flex items-center gap-2">
|
<Label className="flex items-center gap-2">
|
||||||
TTS Voice
|
<Tr>TTS Voice</Tr>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
@@ -1295,9 +1379,11 @@ export default (props: {}) => {
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>TTS Voice</DialogTitle>
|
<DialogTitle>
|
||||||
|
<Tr>TTS Voice</Tr>
|
||||||
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Select the voice style for text-to-speech
|
<Tr>Select the voice style for text-to-speech</Tr>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -1315,7 +1401,9 @@ export default (props: {}) => {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectLabel>Voices</SelectLabel>
|
<SelectLabel>
|
||||||
|
<Tr>Voices</Tr>
|
||||||
|
</SelectLabel>
|
||||||
{TTS_VOICES.map((opt) => (
|
{TTS_VOICES.map((opt) => (
|
||||||
<SelectItem key={opt} value={opt}>
|
<SelectItem key={opt} value={opt}>
|
||||||
{opt}
|
{opt}
|
||||||
@@ -1330,13 +1418,16 @@ export default (props: {}) => {
|
|||||||
min={0.25}
|
min={0.25}
|
||||||
max={4.0}
|
max={4.0}
|
||||||
field="tts_speed"
|
field="tts_speed"
|
||||||
help="Adjust the playback speed of text-to-speech"
|
help={tr(
|
||||||
|
"Adjust the playback speed of text-to-speech",
|
||||||
|
langCode
|
||||||
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label className="flex items-center gap-2">
|
<Label className="flex items-center gap-2">
|
||||||
TTS Format
|
<Tr>TTS Format</Tr>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="ghost" size="icon">
|
<Button variant="ghost" size="icon">
|
||||||
@@ -1345,9 +1436,14 @@ export default (props: {}) => {
|
|||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>TTS Format</DialogTitle>
|
<DialogTitle>
|
||||||
|
<Tr>TTS Format</Tr>
|
||||||
|
</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Select the audio format for text-to-speech output
|
<Tr>
|
||||||
|
Select the audio format for text-to-speech
|
||||||
|
output
|
||||||
|
</Tr>
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -1365,7 +1461,9 @@ export default (props: {}) => {
|
|||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectGroup>
|
<SelectGroup>
|
||||||
<SelectLabel>Formats</SelectLabel>
|
<SelectLabel>
|
||||||
|
<Tr>Formats</Tr>
|
||||||
|
</SelectLabel>
|
||||||
{TTS_FORMAT.map((opt) => (
|
{TTS_FORMAT.map((opt) => (
|
||||||
<SelectItem key={opt} value={opt}>
|
<SelectItem key={opt} value={opt}>
|
||||||
{opt}
|
{opt}
|
||||||
@@ -1379,28 +1477,38 @@ export default (props: {}) => {
|
|||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem value="image_gen">
|
<AccordionItem value="image_gen">
|
||||||
<AccordionTrigger>Image Generation</AccordionTrigger>
|
<AccordionTrigger>
|
||||||
|
<Tr>Image Generation</Tr>
|
||||||
|
</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Image Generation API</CardTitle>
|
<CardTitle>
|
||||||
|
<Tr>Image Generation API</Tr>
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Configure image generation settings
|
<Tr>Configure image generation settings</Tr>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="space-y-4">
|
<CardContent className="space-y-4">
|
||||||
<InputField
|
<InputField
|
||||||
field="image_gen_key"
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<InputField
|
<InputField
|
||||||
field="image_gen_api"
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
<SetAPIsTemplate
|
<SetAPIsTemplate
|
||||||
label="Image Gen API"
|
label={tr("Image Gen API", langCode)}
|
||||||
endpoint={chatStore.image_gen_api}
|
endpoint={chatStore.image_gen_api}
|
||||||
APIkey={chatStore.image_gen_key}
|
APIkey={chatStore.image_gen_key}
|
||||||
temps={templateAPIsImageGen}
|
temps={templateAPIsImageGen}
|
||||||
@@ -1411,7 +1519,9 @@ export default (props: {}) => {
|
|||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
<AccordionItem value="templates">
|
<AccordionItem value="templates">
|
||||||
<AccordionTrigger>Saved Template</AccordionTrigger>
|
<AccordionTrigger>
|
||||||
|
<Tr>Saved Template</Tr>
|
||||||
|
</AccordionTrigger>
|
||||||
<AccordionContent>
|
<AccordionContent>
|
||||||
{templateAPIs.map((template, index) => (
|
{templateAPIs.map((template, index) => (
|
||||||
<div key={index}>
|
<div key={index}>
|
||||||
@@ -1471,11 +1581,11 @@ export default (props: {}) => {
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
<div className="pt-4 space-y-2">
|
<div className="pt-4 space-y-2">
|
||||||
<p className="text-sm text-muted-foreground text-center">
|
<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}
|
{chatStore.chatgpt_api_web_version}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-muted-foreground text-center">
|
<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
|
<a
|
||||||
className="underline hover:text-primary transition-colors"
|
className="underline hover:text-primary transition-colors"
|
||||||
href="https://github.com/heimoshuiyu/chatgpt-api-web"
|
href="https://github.com/heimoshuiyu/chatgpt-api-web"
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const VersionHint = () => {
|
|||||||
{chatStore.chatgpt_api_web_version < "v1.3.0" && (
|
{chatStore.chatgpt_api_web_version < "v1.3.0" && (
|
||||||
<p className="p-2 my-2 text-center dark:text-white">
|
<p className="p-2 my-2 text-center dark:text-white">
|
||||||
<br />
|
<br />
|
||||||
{Tr("Warning: current chatStore version")}:{" "}
|
<Tr>Warning: current chatStore version</Tr>:{" "}
|
||||||
{chatStore.chatgpt_api_web_version} {"< v1.3.0"}
|
{chatStore.chatgpt_api_web_version} {"< v1.3.0"}
|
||||||
<br />
|
<br />
|
||||||
v1.3.0
|
v1.3.0
|
||||||
@@ -22,7 +22,7 @@ const VersionHint = () => {
|
|||||||
{chatStore.chatgpt_api_web_version < "v1.4.0" && (
|
{chatStore.chatgpt_api_web_version < "v1.4.0" && (
|
||||||
<p className="p-2 my-2 text-center dark:text-white">
|
<p className="p-2 my-2 text-center dark:text-white">
|
||||||
<br />
|
<br />
|
||||||
{Tr("Warning: current chatStore version")}:{" "}
|
<Tr>Warning: current chatStore version</Tr>:{" "}
|
||||||
{chatStore.chatgpt_api_web_version} {"< v1.4.0"}
|
{chatStore.chatgpt_api_web_version} {"< v1.4.0"}
|
||||||
<br />
|
<br />
|
||||||
v1.4.0 增加了更多参数,继续使用旧版可能因参数确实导致未定义的行为
|
v1.4.0 增加了更多参数,继续使用旧版可能因参数确实导致未定义的行为
|
||||||
@@ -34,7 +34,7 @@ const VersionHint = () => {
|
|||||||
<p className="p-2 my-2 text-center dark:text-white">
|
<p className="p-2 my-2 text-center dark:text-white">
|
||||||
<br />
|
<br />
|
||||||
提示:当前会话版本 {chatStore.chatgpt_api_web_version}
|
提示:当前会话版本 {chatStore.chatgpt_api_web_version}
|
||||||
{Tr("Warning: current chatStore version")}:{" "}
|
<Tr>Warning: current chatStore version</Tr>:{" "}
|
||||||
{chatStore.chatgpt_api_web_version} {"< v1.6.0"}
|
{chatStore.chatgpt_api_web_version} {"< v1.6.0"}
|
||||||
。
|
。
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export function EditMessageDetail({ chat, setShowEdit }: Props) {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("Edit URL")}
|
<Tr>Edit URL</Tr>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="bg-blue-300 p-1 rounded m-1"
|
className="bg-blue-300 p-1 rounded m-1"
|
||||||
@@ -110,7 +110,7 @@ export function EditMessageDetail({ chat, setShowEdit }: Props) {
|
|||||||
input.click();
|
input.click();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("Upload")}
|
<Tr>Upload</Tr>
|
||||||
</Button>
|
</Button>
|
||||||
<span
|
<span
|
||||||
className="bg-blue-300 p-1 rounded m-1"
|
className="bg-blue-300 p-1 rounded m-1"
|
||||||
@@ -155,7 +155,7 @@ export function EditMessageDetail({ chat, setShowEdit }: Props) {
|
|||||||
setChatStore({ ...chatStore });
|
setChatStore({ ...chatStore });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("Add text")}
|
<Tr>Add text</Tr>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className={"m-2 p-1 rounded bg-green-500"}
|
className={"m-2 p-1 rounded bg-green-500"}
|
||||||
@@ -171,7 +171,7 @@ export function EditMessageDetail({ chat, setShowEdit }: Props) {
|
|||||||
setChatStore({ ...chatStore });
|
setChatStore({ ...chatStore });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("Add image")}
|
<Tr>Add image</Tr>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<DrawerFooter>
|
<DrawerFooter>
|
||||||
@@ -179,7 +179,7 @@ export function EditMessageDetail({ chat, setShowEdit }: Props) {
|
|||||||
className="bg-blue-500 p-2 rounded"
|
className="bg-blue-500 p-2 rounded"
|
||||||
onClick={() => setShowEdit(false)}
|
onClick={() => setShowEdit(false)}
|
||||||
>
|
>
|
||||||
{Tr("Close")}
|
<Tr>Close</Tr>
|
||||||
</Button>
|
</Button>
|
||||||
</DrawerFooter>
|
</DrawerFooter>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ export function EditMessageString({ chat, setShowEdit }: Props) {
|
|||||||
setChatStore({ ...chatStore });
|
setChatStore({ ...chatStore });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("Delete this tool call")}
|
<Tr>Delete this tool call</Tr>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
<hr className="my-2" />
|
<hr className="my-2" />
|
||||||
@@ -94,7 +94,7 @@ export function EditMessageString({ chat, setShowEdit }: Props) {
|
|||||||
setChatStore({ ...chatStore });
|
setChatStore({ ...chatStore });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("Add a tool call")}
|
<Tr>Add a tool call</Tr>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -39,13 +39,13 @@ const Navbar: React.FC = () => {
|
|||||||
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
|
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
|
||||||
|
|
||||||
return (
|
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-col w-full">
|
||||||
<div className="flex flex-1 items-center gap-2">
|
<div className="flex flex-1 items-center gap-2">
|
||||||
<div className="flex items-center gap-2 px-3">
|
<div className="flex items-center gap-2 px-3">
|
||||||
<SidebarTrigger />
|
<SidebarTrigger />
|
||||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
<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>
|
||||||
<div className="flex ml-auto gap-2 px-3">
|
<div className="flex ml-auto gap-2 px-3">
|
||||||
<Settings />
|
<Settings />
|
||||||
@@ -63,13 +63,22 @@ const Navbar: React.FC = () => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ReceiptIcon className="w-4 h-4" />
|
<ReceiptIcon className="w-4 h-4" />
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
Generated Tokens: {chatStore.totalTokens.toString()}
|
<Tr>Total Tokens</Tr>: {chatStore.totalTokens.toString()}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<WalletIcon className="w-4 h-4" />
|
<WalletIcon className="w-4 h-4" />
|
||||||
<span className="text-sm font-medium">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ export function SetAPIsTemplate({
|
|||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button variant="outline">{Tr(`Save ${label}`)}</Button>
|
<Button variant="outline">
|
||||||
|
<Tr>Save</Tr>${label}
|
||||||
|
</Button>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="sm:max-w-md">
|
<DialogContent className="sm:max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ const ChatBubbleActionWrapper = React.forwardRef<
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
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" : "",
|
variant === "sent" ? "flex-row-reverse" : "",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { Slot } from "@radix-ui/react-slot";
|
import { Slot } from "@radix-ui/react-slot";
|
||||||
import { VariantProps, cva } from "class-variance-authority";
|
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 { useIsMobile } from "@/hooks/use-mobile";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -192,21 +192,41 @@ const Sidebar = React.forwardRef<
|
|||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
return (
|
return (
|
||||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
<div className="md:hidden">
|
||||||
<SheetContent
|
{/* 遮罩层 */}
|
||||||
|
<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-sidebar="sidebar"
|
||||||
data-mobile="true"
|
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={
|
style={
|
||||||
{
|
{
|
||||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
"--sidebar-width-mobile": SIDEBAR_WIDTH_MOBILE,
|
||||||
} as React.CSSProperties
|
} as React.CSSProperties
|
||||||
}
|
}
|
||||||
side={side}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className="flex h-full w-full flex-col">{children}</div>
|
<div className="flex h-full w-full flex-col">{children}</div>
|
||||||
</SheetContent>
|
</div>
|
||||||
</Sheet>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { SidebarProvider } from "@/components/ui/sidebar";
|
|||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
import { ThemeProvider } from "@/components/ThemeProvider";
|
import { ThemeProvider } from "@/components/ThemeProvider";
|
||||||
|
|
||||||
|
import "./registerSW"; // 添加此行
|
||||||
|
|
||||||
function Base() {
|
function Base() {
|
||||||
const [langCode, _setLangCode] = useState("en-US");
|
const [langCode, _setLangCode] = useState("en-US");
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const AddToolMsg = (props: {
|
|||||||
className="btn btn-info m-1 p-1"
|
className="btn btn-info m-1 p-1"
|
||||||
onClick={() => setShowAddToolMsg(false)}
|
onClick={() => setShowAddToolMsg(false)}
|
||||||
>
|
>
|
||||||
{Tr("Cancle")}
|
<Tr>Cancle</Tr>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
|
className="rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
|
||||||
@@ -75,6 +75,7 @@ const AddToolMsg = (props: {
|
|||||||
logprobs: null,
|
logprobs: null,
|
||||||
response_model_name: null,
|
response_model_name: null,
|
||||||
reasoning_content: null,
|
reasoning_content: null,
|
||||||
|
usage: null,
|
||||||
});
|
});
|
||||||
setChatStore({ ...chatStore });
|
setChatStore({ ...chatStore });
|
||||||
setNewToolCallID("");
|
setNewToolCallID("");
|
||||||
@@ -82,7 +83,7 @@ const AddToolMsg = (props: {
|
|||||||
setShowAddToolMsg(false);
|
setShowAddToolMsg(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("Add")}
|
<Tr>Add</Tr>
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ interface AppContextType {
|
|||||||
setTemplateTools: (t: TemplateTools[]) => void;
|
setTemplateTools: (t: TemplateTools[]) => void;
|
||||||
defaultRenderMD: boolean;
|
defaultRenderMD: boolean;
|
||||||
setDefaultRenderMD: (b: boolean) => void;
|
setDefaultRenderMD: (b: boolean) => void;
|
||||||
|
handleNewChatStore: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AppChatStoreContextType {
|
interface AppChatStoreContextType {
|
||||||
@@ -94,6 +95,7 @@ import { ModeToggle } from "@/components/ModeToggle";
|
|||||||
import Search from "@/components/Search";
|
import Search from "@/components/Search";
|
||||||
|
|
||||||
import Navbar from "@/components/navbar";
|
import Navbar from "@/components/navbar";
|
||||||
|
import ConversationTitle from "@/components/ConversationTitle.";
|
||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
// init selected index
|
// init selected index
|
||||||
@@ -329,12 +331,15 @@ export function App() {
|
|||||||
setTemplateTools,
|
setTemplateTools,
|
||||||
defaultRenderMD,
|
defaultRenderMD,
|
||||||
setDefaultRenderMD,
|
setDefaultRenderMD,
|
||||||
|
handleNewChatStore,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Sidebar>
|
<Sidebar>
|
||||||
<SidebarHeader>
|
<SidebarHeader>
|
||||||
<Button onClick={handleNewChatStore}>
|
<Button onClick={handleNewChatStore}>
|
||||||
<span>{Tr("New")}</span>
|
<span>
|
||||||
|
<Tr>New Chat</Tr>
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
@@ -356,7 +361,12 @@ export function App() {
|
|||||||
asChild
|
asChild
|
||||||
isActive={i === selectedChatIndex}
|
isActive={i === selectedChatIndex}
|
||||||
>
|
>
|
||||||
<span>{i}</span>
|
<span>
|
||||||
|
<ConversationTitle
|
||||||
|
chatStoreIndex={i}
|
||||||
|
selectedChatStoreIndex={selectedChatIndex}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</SidebarMenuButton>
|
</SidebarMenuButton>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
);
|
);
|
||||||
@@ -372,7 +382,9 @@ export function App() {
|
|||||||
</div>
|
</div>
|
||||||
<AlertDialog>
|
<AlertDialog>
|
||||||
<AlertDialogTrigger asChild>
|
<AlertDialogTrigger asChild>
|
||||||
<Button variant="destructive">{Tr("DEL")}</Button>
|
<Button variant="destructive">
|
||||||
|
<Tr>Delete Chat</Tr>
|
||||||
|
</Button>
|
||||||
</AlertDialogTrigger>
|
</AlertDialogTrigger>
|
||||||
<AlertDialogContent>
|
<AlertDialogContent>
|
||||||
<AlertDialogHeader>
|
<AlertDialogHeader>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useContext, useRef } from "react";
|
import { useContext, useRef } from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Tr } from "@/translate";
|
import { langCodeContext, tr, Tr } from "@/translate";
|
||||||
import { addTotalCost } from "@/utils/totalCost";
|
import { addTotalCost } from "@/utils/totalCost";
|
||||||
import ChatGPT, {
|
import ChatGPT, {
|
||||||
calculate_token_length,
|
calculate_token_length,
|
||||||
@@ -42,10 +42,12 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|||||||
import { AppChatStoreContext, AppContext } from "./App";
|
import { AppChatStoreContext, AppContext } from "./App";
|
||||||
import APIListMenu from "@/components/ListAPI";
|
import APIListMenu from "@/components/ListAPI";
|
||||||
import { ImageGenDrawer } from "@/components/ImageGenDrawer";
|
import { ImageGenDrawer } from "@/components/ImageGenDrawer";
|
||||||
|
import { abort } from "process";
|
||||||
|
|
||||||
export default function ChatBOX() {
|
export default function ChatBOX() {
|
||||||
const { db, selectedChatIndex, setSelectedChatIndex } =
|
const { db, selectedChatIndex, setSelectedChatIndex, handleNewChatStore } =
|
||||||
useContext(AppContext);
|
useContext(AppContext);
|
||||||
|
const { langCode, setLangCode } = useContext(langCodeContext);
|
||||||
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
|
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
|
||||||
// prevent error
|
// prevent error
|
||||||
const [inputMsg, setInputMsg] = useState("");
|
const [inputMsg, setInputMsg] = useState("");
|
||||||
@@ -73,13 +75,14 @@ export default function ChatBOX() {
|
|||||||
if (messagesEndRef.current === null) return;
|
if (messagesEndRef.current === null) return;
|
||||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
||||||
}
|
}
|
||||||
}, [showRetry, showGenerating, generatingMessage]);
|
}, [showRetry, showGenerating, generatingMessage, chatStore]);
|
||||||
|
|
||||||
const client = new ChatGPT(chatStore.apiKey);
|
const client = new ChatGPT(chatStore.apiKey);
|
||||||
|
|
||||||
const _completeWithStreamMode = async (
|
const _completeWithStreamMode = async (
|
||||||
response: Response
|
response: Response,
|
||||||
): Promise<Usage> => {
|
signal: AbortSignal
|
||||||
|
): Promise<ChatStoreMessage> => {
|
||||||
let responseTokenCount = 0; // including reasoning content and normal content
|
let responseTokenCount = 0; // including reasoning content and normal content
|
||||||
const allChunkMessage: string[] = [];
|
const allChunkMessage: string[] = [];
|
||||||
const allReasoningContentChunk: string[] = [];
|
const allReasoningContentChunk: string[] = [];
|
||||||
@@ -90,7 +93,8 @@ export default function ChatBOX() {
|
|||||||
};
|
};
|
||||||
let response_model_name: string | null = null;
|
let response_model_name: string | null = null;
|
||||||
let usage: Usage | null = null;
|
let usage: Usage | null = null;
|
||||||
for await (const i of client.processStreamResponse(response)) {
|
for await (const i of client.processStreamResponse(response, signal)) {
|
||||||
|
if (signal?.aborted) break;
|
||||||
response_model_name = i.model;
|
response_model_name = i.model;
|
||||||
responseTokenCount += 1;
|
responseTokenCount += 1;
|
||||||
if (i.usage) {
|
if (i.usage) {
|
||||||
@@ -165,22 +169,7 @@ export default function ChatBOX() {
|
|||||||
const reasoning_content = allReasoningContentChunk.join("");
|
const reasoning_content = allReasoningContentChunk.join("");
|
||||||
|
|
||||||
console.log("save logprobs", logprobs);
|
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
|
// manually copy status from client to chatStore
|
||||||
chatStore.maxTokens = client.max_tokens;
|
chatStore.maxTokens = client.max_tokens;
|
||||||
chatStore.tokenMargin = client.tokens_margin;
|
chatStore.tokenMargin = client.tokens_margin;
|
||||||
@@ -209,14 +198,43 @@ export default function ChatBOX() {
|
|||||||
ret.completion_tokens_details = usage.completion_tokens_details ?? null;
|
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 data = (await response.json()) as FetchResponse;
|
||||||
const msg = client.processFetchResponse(data);
|
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",
|
role: "assistant",
|
||||||
content: msg.content,
|
content: msg.content,
|
||||||
tool_calls: msg.tool_calls,
|
tool_calls: msg.tool_calls,
|
||||||
@@ -224,22 +242,13 @@ export default function ChatBOX() {
|
|||||||
token: data.usage?.completion_tokens_details
|
token: data.usage?.completion_tokens_details
|
||||||
? data.usage.completion_tokens -
|
? data.usage.completion_tokens -
|
||||||
data.usage.completion_tokens_details.reasoning_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,
|
example: false,
|
||||||
audio: null,
|
audio: null,
|
||||||
logprobs: data.choices[0]?.logprobs,
|
logprobs: data.choices[0]?.logprobs,
|
||||||
response_model_name: data.model,
|
response_model_name: data.model,
|
||||||
reasoning_content: data.choices[0]?.message?.reasoning_content ?? null,
|
reasoning_content: data.choices[0]?.message?.reasoning_content ?? null,
|
||||||
});
|
usage,
|
||||||
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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
@@ -288,28 +297,47 @@ export default function ChatBOX() {
|
|||||||
client.max_gen_tokens = chatStore.maxGenTokens;
|
client.max_gen_tokens = chatStore.maxGenTokens;
|
||||||
client.enable_max_gen_tokens = chatStore.maxGenTokens_enabled;
|
client.enable_max_gen_tokens = chatStore.maxGenTokens_enabled;
|
||||||
|
|
||||||
|
const created_at = new Date();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setShowGenerating(true);
|
setShowGenerating(true);
|
||||||
|
abortControllerRef.current = new AbortController();
|
||||||
const response = await client._fetch(
|
const response = await client._fetch(
|
||||||
chatStore.streamMode,
|
chatStore.streamMode,
|
||||||
chatStore.logprobs
|
chatStore.logprobs,
|
||||||
|
abortControllerRef.current.signal
|
||||||
);
|
);
|
||||||
|
const responsed_at = new Date();
|
||||||
const contentType = response.headers.get("content-type");
|
const contentType = response.headers.get("content-type");
|
||||||
let usage: Usage;
|
let cs: ChatStoreMessage;
|
||||||
if (contentType?.startsWith("text/event-stream")) {
|
if (contentType?.startsWith("text/event-stream")) {
|
||||||
usage = await _completeWithStreamMode(response);
|
cs = await _completeWithStreamMode(
|
||||||
|
response,
|
||||||
|
abortControllerRef.current.signal
|
||||||
|
);
|
||||||
} else if (contentType?.startsWith("application/json")) {
|
} else if (contentType?.startsWith("application/json")) {
|
||||||
usage = await _completeWithFetchMode(response);
|
cs = await _completeWithFetchMode(response);
|
||||||
} else {
|
} else {
|
||||||
throw `unknown response content type ${contentType}`;
|
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
|
// manually copy status from client to chatStore
|
||||||
chatStore.maxTokens = client.max_tokens;
|
chatStore.maxTokens = client.max_tokens;
|
||||||
chatStore.tokenMargin = client.tokens_margin;
|
chatStore.tokenMargin = client.tokens_margin;
|
||||||
chatStore.totalTokens = client.total_tokens;
|
chatStore.totalTokens = client.total_tokens;
|
||||||
|
|
||||||
console.log("usage", usage);
|
|
||||||
// estimate user's input message token
|
// estimate user's input message token
|
||||||
const aboveTokens = chatStore.history
|
const aboveTokens = chatStore.history
|
||||||
.filter(({ hide }) => !hide)
|
.filter(({ hide }) => !hide)
|
||||||
@@ -357,7 +385,11 @@ export default function ChatBOX() {
|
|||||||
|
|
||||||
setShowRetry(false);
|
setShowRetry(false);
|
||||||
setChatStore({ ...chatStore });
|
setChatStore({ ...chatStore });
|
||||||
} catch (error) {
|
} catch (error: any) {
|
||||||
|
if (error.name === "AbortError") {
|
||||||
|
console.log("abort complete");
|
||||||
|
return;
|
||||||
|
}
|
||||||
setShowRetry(true);
|
setShowRetry(true);
|
||||||
alert(error);
|
alert(error);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -368,9 +400,9 @@ export default function ChatBOX() {
|
|||||||
|
|
||||||
// when user click the "send" button or ctrl+Enter in the textarea
|
// when user click the "send" button or ctrl+Enter in the textarea
|
||||||
const send = async (msg = "", call_complete = true) => {
|
const send = async (msg = "", call_complete = true) => {
|
||||||
if (messagesEndRef.current) {
|
setTimeout(() => {
|
||||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||||
}
|
}, 0);
|
||||||
|
|
||||||
const inputMsg = msg.trim();
|
const inputMsg = msg.trim();
|
||||||
if (!inputMsg && images.length === 0) {
|
if (!inputMsg && images.length === 0) {
|
||||||
@@ -395,6 +427,7 @@ export default function ChatBOX() {
|
|||||||
logprobs: null,
|
logprobs: null,
|
||||||
response_model_name: null,
|
response_model_name: null,
|
||||||
reasoning_content: null,
|
reasoning_content: null,
|
||||||
|
usage: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// manually calculate token length
|
// manually calculate token length
|
||||||
@@ -411,40 +444,44 @@ export default function ChatBOX() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const userInputRef = useRef<HTMLInputElement>(null);
|
const userInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const abortControllerRef = useRef<AbortController>(new AbortController());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="grow flex flex-col p-2 w-full">
|
<div className="grow flex flex-col w-full">
|
||||||
<ChatMessageList>
|
<ChatMessageList>
|
||||||
{chatStore.history.length === 0 && (
|
{chatStore.history.length === 0 && (
|
||||||
<Alert variant="default" className="my-3">
|
<Alert variant="default" className="my-3">
|
||||||
<InfoIcon className="h-4 w-4" />
|
<InfoIcon className="h-4 w-4" />
|
||||||
<AlertTitle>
|
<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>
|
</AlertTitle>
|
||||||
<AlertDescription className="flex flex-col gap-1 mt-5">
|
<AlertDescription className="flex flex-col gap-1 mt-5">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CornerRightUpIcon className="h-4 w-4" />
|
<CornerRightUpIcon className="h-4 w-4" />
|
||||||
<span>
|
<span>
|
||||||
{Tr(
|
<Tr>
|
||||||
"Settings button located at the top right corner can be used to change the settings of this chat"
|
Settings button located at the top right corner can be
|
||||||
)}
|
used to change the settings of this chat
|
||||||
|
</Tr>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<CornerLeftUpIcon className="h-4 w-4" />
|
<CornerLeftUpIcon className="h-4 w-4" />
|
||||||
<span>
|
<span>
|
||||||
{Tr(
|
<Tr>
|
||||||
"'New' button located at the top left corner can be used to create a new chat"
|
'New' button located at the top left corner can be used to
|
||||||
)}
|
create a new chat
|
||||||
|
</Tr>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ArrowDownToDotIcon className="h-4 w-4" />
|
<ArrowDownToDotIcon className="h-4 w-4" />
|
||||||
<span>
|
<span>
|
||||||
{Tr(
|
<Tr>
|
||||||
"All chat history and settings are stored in the local browser"
|
All chat history and settings are stored in the local
|
||||||
)}
|
browser
|
||||||
|
</Tr>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
@@ -489,7 +526,7 @@ export default function ChatBOX() {
|
|||||||
</ChatBubble>
|
</ChatBubble>
|
||||||
)}
|
)}
|
||||||
<p className="text-center">
|
<p className="text-center">
|
||||||
{chatStore.history.length > 0 && (
|
{chatStore.history.length > 0 && !showGenerating && (
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -505,7 +542,35 @@ export default function ChatBOX() {
|
|||||||
await complete();
|
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>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{chatStore.develop_mode && chatStore.history.length > 0 && (
|
{chatStore.develop_mode && chatStore.history.length > 0 && (
|
||||||
@@ -517,22 +582,24 @@ export default function ChatBOX() {
|
|||||||
await complete();
|
await complete();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("Completion")}
|
<Tr>Completion</Tr>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</p>
|
</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">
|
<Alert variant="default">
|
||||||
<InfoIcon className="h-4 w-4" />
|
<InfoIcon className="h-4 w-4" />
|
||||||
<AlertTitle>{Tr("Chat History Notice")}</AlertTitle>
|
<AlertTitle>
|
||||||
|
<Tr>Chat History Notice</Tr>
|
||||||
|
</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{Tr("Info: chat history is too long, forget messages")}:{" "}
|
<Tr>Info: chat history is too long, forget messages</Tr>:{" "}
|
||||||
{chatStore.postBeginIndex}
|
{chatStore.postBeginIndex}
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
<VersionHint />
|
<VersionHint />
|
||||||
{showRetry && (
|
{showRetry && (
|
||||||
<p className="text-right p-2 my-2 dark:text-white">
|
<p className="text-right p-2 my-2 dark:text-white">
|
||||||
@@ -543,12 +610,12 @@ export default function ChatBOX() {
|
|||||||
await complete();
|
await complete();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("Retry")}
|
<Tr>Retry</Tr>
|
||||||
</Button>
|
</Button>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div ref={messagesEndRef as any}></div>
|
|
||||||
</ChatMessageList>
|
</ChatMessageList>
|
||||||
|
<div id="message-end" ref={messagesEndRef as any}></div>
|
||||||
{images.length > 0 && (
|
{images.length > 0 && (
|
||||||
<div className="flex flex-wrap">
|
<div className="flex flex-wrap">
|
||||||
{images.map((image, index) => (
|
{images.map((image, index) => (
|
||||||
@@ -581,7 +648,7 @@ export default function ChatBOX() {
|
|||||||
<ChatInput
|
<ChatInput
|
||||||
value={inputMsg}
|
value={inputMsg}
|
||||||
ref={userInputRef as any}
|
ref={userInputRef as any}
|
||||||
placeholder="Type your message here..."
|
placeholder={tr("Type your message here...", langCode)}
|
||||||
onChange={(event: any) => {
|
onChange={(event: any) => {
|
||||||
setInputMsg(event.target.value);
|
setInputMsg(event.target.value);
|
||||||
autoHeight(event.target);
|
autoHeight(event.target);
|
||||||
@@ -620,7 +687,7 @@ export default function ChatBOX() {
|
|||||||
autoHeight(userInputRef.current);
|
autoHeight(userInputRef.current);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Send Message
|
<Tr>Send</Tr>
|
||||||
<CornerDownLeftIcon className="size-3.5" />
|
<CornerDownLeftIcon className="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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";
|
import MAP_zh_CN from "@/translate/zh_CN";
|
||||||
|
|
||||||
interface LangOption {
|
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") {
|
function tr(text: string, langCode: "en-US" | "zh-CN") {
|
||||||
const option = LANG_OPTIONS[langCode];
|
const option = LANG_OPTIONS[langCode];
|
||||||
@@ -31,18 +50,18 @@ function tr(text: string, langCode: "en-US" | "zh-CN") {
|
|||||||
|
|
||||||
const translatedText = langMap[text.toLowerCase()];
|
const translatedText = langMap[text.toLowerCase()];
|
||||||
if (translatedText === undefined) {
|
if (translatedText === undefined) {
|
||||||
|
console.log(`[Translation] not found for "${text}"`);
|
||||||
return text;
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
return translatedText;
|
return translatedText;
|
||||||
}
|
}
|
||||||
|
function Tr({ children }: { children: string }) {
|
||||||
function Tr(text: string) {
|
|
||||||
return (
|
return (
|
||||||
<langCodeContext.Consumer>
|
<langCodeContext.Consumer>
|
||||||
{/* @ts-ignore */}
|
{/* @ts-ignore */}
|
||||||
{({ langCode }) => {
|
{({ langCode }) => {
|
||||||
return tr(text, langCode);
|
return tr(children, langCode);
|
||||||
}}
|
}}
|
||||||
</langCodeContext.Consumer>
|
</langCodeContext.Consumer>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -62,6 +62,85 @@ const LANG_MAP: Record<string, string> = {
|
|||||||
example: "示例",
|
example: "示例",
|
||||||
render: "渲染",
|
render: "渲染",
|
||||||
"reset current": "清空当前会话",
|
"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;
|
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,
|
* ChatStore is the main object of the chatgpt-api-web,
|
||||||
@@ -71,6 +71,11 @@ export interface ChatStoreMessage {
|
|||||||
audio: Blob | null;
|
audio: Blob | null;
|
||||||
logprobs: Logprobs | null;
|
logprobs: Logprobs | null;
|
||||||
response_model_name: string | null;
|
response_model_name: string | null;
|
||||||
|
usage: Usage | null;
|
||||||
|
|
||||||
|
created_at?: string;
|
||||||
|
responsed_at?: string;
|
||||||
|
completed_at?: string;
|
||||||
|
|
||||||
role: "system" | "user" | "assistant" | "tool";
|
role: "system" | "user" | "assistant" | "tool";
|
||||||
content: string | MessageDetail[];
|
content: string | MessageDetail[];
|
||||||
|
|||||||
@@ -1,9 +1,30 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig } from "vite";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { VitePWA } from "vite-plugin-pwa";
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
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: "./",
|
base: "./",
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
Reference in New Issue
Block a user