Files
chatgpt-api-web/src/components/Settings.tsx

1470 lines
53 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { themeChange } from "theme-change";
import { useRef, useCallback } from "react";
import { useContext, useEffect, useState, Dispatch } from "react";
import React from "react";
import { clearTotalCost, getTotalCost } from "@/utils/totalCost";
import { ChatStore, TemplateChatStore, TemplateTools } from "@/types/chatstore";
import { models } from "@/types/models";
import { tr, Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
import { isVailedJSON } from "@/utils/isVailedJSON";
import { SetAPIsTemplate } from "@/components/setAPIsTemplate";
import { autoHeight } from "@/utils/textAreaHelp";
import { getDefaultParams } from "@/utils/getDefaultParam";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Checkbox } from "@/components/ui/checkbox";
import { Badge } from "@/components/ui/badge";
import { Textarea } from "@/components/ui/textarea";
import {
BanIcon,
CheckIcon,
CircleEllipsisIcon,
CogIcon,
EyeIcon,
InfoIcon,
KeyIcon,
ListIcon,
MoveHorizontalIcon,
SaveIcon,
TriangleAlertIcon,
} from "lucide-react";
import { Separator } from "@/components/ui/separator";
import { Slider } from "@/components/ui/slider";
import { NonOverflowScrollArea, ScrollArea } from "@/components/ui/scroll-area";
import { AppChatStoreContext, AppContext } from "@/pages/App";
import { toast } from "@/hooks/use-toast";
import { title } from "process";
const TTS_VOICES: string[] = [
"alloy",
"echo",
"fable",
"onyx",
"nova",
"shimmer",
];
const TTS_FORMAT: string[] = ["mp3", "opus", "aac", "flac"];
const Help = (props: { children: any; help: string; field: string }) => {
return (
<div className="b-2">
<label className="form-control w-full">{props.children}</label>
</div>
);
};
const SelectModel = (props: { help: string }) => {
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
let shouldIUseCustomModel: boolean = true;
for (const model in models) {
if (chatStore.model === model) {
shouldIUseCustomModel = false;
}
}
const [useCustomModel, setUseCustomModel] = useState(shouldIUseCustomModel);
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<Label className="flex items-center gap-2">
<ListIcon className="w-4 h-4" />
Model
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" size="icon">
<InfoIcon className="w-4 h-4" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Model Selection</DialogTitle>
<DialogDescription>{props.help}</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</Label>
<div className="flex items-center gap-2">
<Label className="flex items-center gap-2">
<CogIcon className="w-4 h-4" />
{Tr("Custom")}
</Label>
<Checkbox
checked={useCustomModel}
onCheckedChange={() => setUseCustomModel(!useCustomModel)}
/>
</div>
</div>
{useCustomModel ? (
<Input
value={chatStore.model}
onBlur={(e: React.ChangeEvent<HTMLInputElement>) => {
chatStore.model = e.target.value;
setChatStore({ ...chatStore });
}}
/>
) : (
<Select
value={chatStore.model}
onValueChange={(model: string) => {
chatStore.model = model;
chatStore.maxTokens = getDefaultParams(
"max",
models[model].maxToken
);
setChatStore({ ...chatStore });
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a model" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Models</SelectLabel>
{Object.keys(models).map((opt) => (
<SelectItem key={opt} value={opt}>
{opt}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
)}
</div>
);
};
const LongInput = React.memo(
(props: {
field: "systemMessageContent" | "toolsString";
label: string;
help: string;
}) => {
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [localValue, setLocalValue] = useState(chatStore[props.field]);
// Update height when value changes
useEffect(() => {
if (textareaRef.current) {
autoHeight(textareaRef.current);
}
}, [localValue]);
// Sync local value with chatStore when it changes externally
useEffect(() => {
setLocalValue(chatStore[props.field]);
}, [chatStore[props.field]]);
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
setLocalValue(event.target.value);
};
const handleBlur = () => {
if (localValue !== chatStore[props.field]) {
chatStore[props.field] = localValue;
setChatStore({ ...chatStore });
}
};
return (
<div>
<Label htmlFor="name" className="text-right">
{props.label}{" "}
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" size="icon">
<InfoIcon />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>{props.label} Help</DialogTitle>
</DialogHeader>
{props.help}
</DialogContent>
</Dialog>
</Label>
<Textarea
ref={textareaRef}
mockOnChange={false}
className="h-24 w-full"
value={localValue}
onChange={handleChange}
onBlur={handleBlur}
/>
</div>
);
}
);
const InputField = (props: {
field:
| "apiKey"
| "apiEndpoint"
| "whisper_api"
| "whisper_key"
| "tts_api"
| "tts_key"
| "image_gen_api"
| "image_gen_key";
help: string;
}) => {
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
const [hideInput, setHideInput] = useState(true);
return (
<>
<div className="space-y-2">
<Label className="flex items-center gap-2">
<KeyIcon className="w-4 h-4" />
{props.field}
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" size="icon">
<InfoIcon className="w-4 h-4" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{props.field}</DialogTitle>
<DialogDescription>{props.help}</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</Label>
<div className="flex w-full items-center space-x-2">
<Input
type={hideInput ? "password" : "text"}
value={chatStore[props.field]}
onBlur={(event: React.ChangeEvent<HTMLInputElement>) => {
chatStore[props.field] = event.target.value;
setChatStore({ ...chatStore });
}}
/>
<Button
variant="ghost"
size="icon"
onClick={() => setHideInput(!hideInput)}
>
{hideInput ? (
<EyeIcon className="h-4 w-4" />
) : (
<KeyIcon className="h-4 w-4" />
)}
</Button>
</div>
</div>
</>
);
};
const Slicer = (props: {
field: "temperature" | "top_p" | "tts_speed";
help: string;
min: number;
max: number;
}) => {
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
const enable_filed_name: "temperature_enabled" | "top_p_enabled" =
`${props.field}_enabled` as any;
const enabled = chatStore[enable_filed_name];
if (enabled === null || enabled === undefined) {
if (props.field === "temperature") {
chatStore[enable_filed_name] = true;
}
if (props.field === "top_p") {
chatStore[enable_filed_name] = false;
}
}
const setEnabled = (state: boolean) => {
chatStore[enable_filed_name] = state;
setChatStore({ ...chatStore });
};
return (
<div className="space-y-2">
<Label className="flex items-center gap-2">
<MoveHorizontalIcon className="w-4 h-4" />
{props.field}
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" size="icon">
<InfoIcon className="w-4 h-4" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{props.field}</DialogTitle>
<DialogDescription>{props.help}</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
<Checkbox
checked={chatStore[enable_filed_name]}
onCheckedChange={(checked: boolean) => setEnabled(!!checked)}
/>
{!chatStore[enable_filed_name] && (
<span className="text-xs text-muted-foreground">disabled</span>
)}
</Label>
{enabled && (
<div className="flex items-center gap-4">
<div className="flex-1">
<Slider
disabled={!enabled}
min={props.min}
max={props.max}
step={0.01}
value={[chatStore[props.field]]}
onValueChange={(value) => {
chatStore[props.field] = value[0];
setChatStore({ ...chatStore });
}}
/>
</div>
<Input
type="number"
disabled={!enabled}
className="w-24"
value={chatStore[props.field]}
onBlur={(e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseFloat(e.target.value);
chatStore[props.field] = value;
setChatStore({ ...chatStore });
}}
/>
</div>
)}
</div>
);
};
const Number = (props: {
field:
| "totalTokens"
| "maxTokens"
| "maxGenTokens"
| "tokenMargin"
| "postBeginIndex"
| "presence_penalty"
| "frequency_penalty";
readOnly: boolean;
help: string;
}) => {
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
return (
<div className="space-y-2">
<Label className="flex items-center gap-2">
<CircleEllipsisIcon className="h-4 w-4" />
{props.field}
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" size="icon">
<InfoIcon className="h-4 w-4" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{props.field}</DialogTitle>
<DialogDescription>{props.help}</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
{props.field === "maxGenTokens" && (
<Checkbox
checked={chatStore.maxGenTokens_enabled}
onCheckedChange={() => {
const newChatStore = { ...chatStore };
newChatStore.maxGenTokens_enabled =
!newChatStore.maxGenTokens_enabled;
setChatStore({ ...newChatStore });
}}
/>
)}
{props.field === "presence_penalty" && (
<Checkbox
checked={chatStore.presence_penalty_enabled}
onCheckedChange={() => {
const newChatStore = { ...chatStore };
newChatStore.presence_penalty_enabled =
!newChatStore.presence_penalty_enabled;
setChatStore({ ...newChatStore });
}}
/>
)}
{props.field === "frequency_penalty" && (
<Checkbox
checked={chatStore.frequency_penalty_enabled}
onCheckedChange={() => {
const newChatStore = { ...chatStore };
newChatStore.frequency_penalty_enabled =
!newChatStore.frequency_penalty_enabled;
setChatStore({ ...newChatStore });
}}
/>
)}
</Label>
<Input
type="number"
readOnly={props.readOnly}
disabled={
props.field === "maxGenTokens" && !chatStore.maxGenTokens_enabled
}
value={chatStore[props.field]}
onBlur={(event: React.ChangeEvent<HTMLInputElement>) => {
let newNumber = parseFloat(event.target.value);
if (newNumber < 0) newNumber = 0;
chatStore[props.field] = newNumber;
setChatStore({ ...chatStore });
}}
/>
</div>
);
};
const Choice = (props: {
field: "streamMode" | "develop_mode" | "json_mode" | "logprobs";
help: string;
}) => {
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
return (
<div className="flex items-center space-x-2">
<div className="flex items-center">
<Checkbox
id={`${props.field}-checkbox`}
checked={chatStore[props.field]}
onCheckedChange={(checked: boolean) => {
chatStore[props.field] = checked;
setChatStore({ ...chatStore });
}}
/>
</div>
<label
htmlFor={`${props.field}-checkbox`}
className="flex items-center gap-2 font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{props.field}
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" size="icon">
<InfoIcon />
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>{props.field} Help</DialogTitle>
</DialogHeader>
{props.help}
</DialogContent>
</Dialog>
</label>
</div>
);
};
const APIShowBlock = (props: {
index: number;
label: string;
type: string;
apiField: string;
keyField: string;
}) => {
const {
templates,
setTemplates,
templateAPIs,
setTemplateAPIs,
templateAPIsWhisper,
setTemplateAPIsWhisper,
templateAPIsTTS,
setTemplateAPIsTTS,
templateAPIsImageGen,
setTemplateAPIsImageGen,
templateTools,
setTemplateTools,
selectedChatIndex,
} = useContext(AppContext);
return (
<div className="border-b border-gray-200 pb-4 pt-4">
<Badge variant="outline">{props.type}</Badge> <Label>{props.label}</Label>
<div className="mt-4">
<div className="grid w-full max-w-sm items-center gap-1.5 mt-2">
<Label>Endpoint</Label>
<div className="w-72">
<pre className="text-xs whitespace-pre-wrap">{props.apiField}</pre>
</div>
</div>
<div className="grid w-full max-w-sm items-center gap-1.5 mt-2">
<Label>Key</Label>
{props.keyField ? (
<div className="w-72">
<pre className="text-xs whitespace-pre-wrap">
{props.keyField}
</pre>
</div>
) : (
<span className="text-gray-500 italic">empty</span>
)}
</div>
</div>
<Button
variant="outline"
size="sm"
className="mt-2 mr-2"
onClick={() => {
const name = prompt(`Give template ${props.label} a new name`);
if (!name) return;
if (props.type === "Chat") {
templateAPIs[props.index].name = name;
setTemplateAPIs(structuredClone(templateAPIs));
} else if (props.type === "Whisper") {
templateAPIsWhisper[props.index].name = name;
setTemplateAPIsWhisper(structuredClone(templateAPIsWhisper));
} else if (props.type === "TTS") {
templateAPIsTTS[props.index].name = name;
setTemplateAPIsTTS(structuredClone(templateAPIsTTS));
} else if (props.type === "ImgGen") {
templateAPIsImageGen[props.index].name = name;
setTemplateAPIsImageGen(structuredClone(templateAPIsImageGen));
}
}}
>
Change Name
</Button>
<Button
variant="destructive"
size="sm"
className="mt-2"
onClick={() => {
if (
!confirm(
`Are you sure to delete ${props.label}(${props.type}) API?`
)
) {
return;
}
if (props.type === "Chat") {
templateAPIs.splice(props.index, 1);
setTemplateAPIs(structuredClone(templateAPIs));
} else if (props.type === "Whisper") {
templateAPIsWhisper.splice(props.index, 1);
setTemplateAPIsWhisper(structuredClone(templateAPIsWhisper));
} else if (props.type === "TTS") {
templateAPIsTTS.splice(props.index, 1);
setTemplateAPIsTTS(structuredClone(templateAPIsTTS));
} else if (props.type === "ImgGen") {
templateAPIsImageGen.splice(props.index, 1);
setTemplateAPIsImageGen(structuredClone(templateAPIsImageGen));
}
}}
>
Delete
</Button>
</div>
);
};
const ToolsShowBlock = (props: {
index: number;
label: string;
content: string;
}) => {
const {
templates,
setTemplates,
templateAPIs,
setTemplateAPIs,
templateAPIsWhisper,
setTemplateAPIsWhisper,
templateAPIsTTS,
setTemplateAPIsTTS,
templateAPIsImageGen,
setTemplateAPIsImageGen,
templateTools,
setTemplateTools,
selectedChatIndex,
} = useContext(AppContext);
return (
<div className="border-b border-gray-200 pb-4 pt-4">
<Badge variant="outline">Tool</Badge> <Label>{props.label}</Label>
<div className="mt-4">
<div className="grid w-full max-w-sm items-center gap-1.5 mt-2">
<Label>Content</Label>
<ScrollArea className="w-72 whitespace-nowrap rounded-md border">
<pre className="text-xs">
{JSON.stringify(JSON.parse(props.content), null, 2)}
</pre>
</ScrollArea>
</div>
</div>
<Button
variant="outline"
size="sm"
className="mt-2 mr-2"
onClick={() => {
const name = prompt(`Give the tool ${props.label} a new name`);
if (!name) return;
templateTools[props.index].name = name;
setTemplateTools(structuredClone(templateTools));
}}
>
Edit
</Button>
<Button
variant="destructive"
size="sm"
className="mt-2"
onClick={() => {
if (!confirm(`Are you sure to delete ${props.label} Tool?`)) {
return;
}
templateTools.splice(props.index, 1);
setTemplateTools(structuredClone(templateTools));
}}
>
Delete
</Button>
</div>
);
};
export default (props: {}) => {
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
const {
templates,
setTemplates,
templateAPIs,
setTemplateAPIs,
templateAPIsWhisper,
setTemplateAPIsWhisper,
templateAPIsTTS,
setTemplateAPIsTTS,
templateAPIsImageGen,
setTemplateAPIsImageGen,
templateTools,
setTemplateTools,
selectedChatIndex,
} = useContext(AppContext);
let link =
location.protocol +
"//" +
location.host +
location.pathname +
`?key=${encodeURIComponent(chatStore.apiKey)}&api=${encodeURIComponent(
chatStore.apiEndpoint
)}&mode=${chatStore.streamMode ? "stream" : "fetch"}&model=${
chatStore.model
}&sys=${encodeURIComponent(chatStore.systemMessageContent)}`;
if (chatStore.develop_mode) {
link = link + `&dev=true`;
}
const importFileRef = useRef<any>(null);
const [totalCost, setTotalCost] = useState(getTotalCost());
// @ts-ignore
const { langCode, setLangCode } = useContext(langCodeContext);
const [open, setOpen] = useState<boolean>(false);
useEffect(() => {
themeChange(false);
const handleKeyPress = (event: any) => {
if (event.keyCode === 27) {
// keyCode for ESC key is 27
setOpen(false);
}
};
document.addEventListener("keydown", handleKeyPress);
return () => {
document.removeEventListener("keydown", handleKeyPress);
};
}, []); // The empty dependency array ensures that the effect runs only once
return (
<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger asChild>
<Button variant="outline" className="flex-grow">
{Tr("Settings")}
{(!chatStore.apiKey || !chatStore.apiEndpoint) && (
<TriangleAlertIcon className="w-4 h-4 ml-1 text-yellow-500" />
)}
</Button>
</SheetTrigger>
<SheetContent className="flex flex-col overflow-scroll">
<NonOverflowScrollArea>
<SheetHeader>
<SheetTitle>{Tr("Settings")}</SheetTitle>
<SheetDescription>
You can customize the settings here.
</SheetDescription>
</SheetHeader>
<Accordion type="multiple" className="w-full">
<AccordionItem value="session">
<AccordionTrigger>Session</AccordionTrigger>
<AccordionContent>
<Card>
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
<CardTitle>Session Cost</CardTitle>
<CardDescription>
Cost of the current session.
</CardDescription>
</div>
<div className="flex">
<div className="flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-8 sm:py-6">
<span className="text-xs text-muted-foreground">
$ USD
</span>
<span className="text-lg font-bold leading-none sm:text-3xl">
{chatStore.cost?.toFixed(4)}
</span>
</div>
</div>
</CardHeader>
</Card>
<LongInput
label="System Prompt"
field="systemMessageContent"
help="系统消息用于指示ChatGPT的角色和一些前置条件例如“你是一个有帮助的人工智能助理”或者“你是一个专业英语翻译把我的话全部翻译成英语”详情参考 OPEAN AI API 文档"
{...props}
/>
<LongInput
label="Tools String"
field="toolsString"
help="function call tools, should be valid json format in list"
{...props}
/>
<span className="pt-1">
JSON Check:{" "}
{isVailedJSON(chatStore.toolsString) ? (
<CheckIcon className="inline w-3 h-3" />
) : (
<BanIcon className="inline w-3 h-3" />
)}
</span>
<div className="box">
<div className="flex justify-evenly flex-wrap">
{chatStore.toolsString.trim() && (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">{Tr(`Save Tools`)}</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Save the tool as Template</DialogTitle>
<DialogDescription>
Once saved, you can easily access your tools from
the dropdown menu.
</DialogDescription>
</DialogHeader>
<div className="flex items-center space-x-2">
<div className="grid flex-1 gap-2">
<Label htmlFor="toolsName" className="sr-only">
Name
</Label>
<Input
id="toolsName"
placeholder="Type Something..."
/>
<Label
id="toolsNameError"
className="text-red-600"
></Label>
</div>
</div>
<DialogFooter className="sm:justify-start">
<DialogClose asChild>
<Button
type="submit"
size="sm"
className="px-3"
onClick={() => {
const name = document.getElementById(
"toolsName" as string
) as HTMLInputElement;
if (!name.value) {
const errorLabel = document.getElementById(
"toolsNameError" as string
) as HTMLLabelElement;
if (errorLabel) {
errorLabel.textContent =
"Tool name is required.";
}
return;
}
const newToolsTmp: TemplateTools = {
name: name.value,
toolsString: chatStore.toolsString,
};
templateTools.push(newToolsTmp);
setTemplateTools([...templateTools]);
}}
>
<SaveIcon className="w-4 h-4" /> Save
<span className="sr-only">Save</span>
</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="system">
<AccordionTrigger>System</AccordionTrigger>
<AccordionContent>
<>
<Card>
<CardHeader className="flex flex-col items-stretch space-y-0 border-b p-0 sm:flex-row">
<div className="flex flex-1 flex-col justify-center gap-1 px-6 py-5 sm:py-6">
<CardTitle>Accumulated Cost</CardTitle>
<CardDescription>in all sessions</CardDescription>
</div>
<div className="flex">
<div className="flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l data-[active=true]:bg-muted/50 sm:border-l sm:border-t-0 sm:px-8 sm:py-6">
<span className="text-xs text-muted-foreground">
$ USD
</span>
<span className="text-lg font-bold leading-none sm:text-3xl">
{totalCost.toFixed(4)}
</span>
</div>
</div>
</CardHeader>
</Card>
<div className="flex justify-end mt-2">
<Button
size="sm"
variant="destructive"
onClick={() => {
clearTotalCost();
setTotalCost(getTotalCost());
}}
>
Reset Total Cost
</Button>
</div>
<Choice
field="develop_mode"
help="开发者模式,开启后会显示更多选项及功能"
{...props}
/>
<div className="space-y-4">
<div className="space-y-2">
<Label>Language</Label>
<Select value={langCode} onValueChange={setLangCode}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select language" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Languages</SelectLabel>
{Object.keys(LANG_OPTIONS).map((opt) => (
<SelectItem key={opt} value={opt}>
{LANG_OPTIONS[opt].name}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label>Quick Actions</Label>
<div className="space-y-2">
<Button
variant="outline"
className="w-full"
onClick={() => {
navigator.clipboard.writeText(link);
toast({
title: tr(`Copied link:`, langCode),
description: `${link}`,
});
}}
>
{Tr("Copy Setting Link")}
</Button>
<Dialog>
<DialogTrigger asChild>
<Button variant="destructive" className="w-full">
{Tr("Clear History")}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>
Are you absolutely sure?
</DialogTitle>
<DialogDescription>
This action cannot be undone. This will
permanently delete all chat history.
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="destructive"
onClick={() => {
chatStore.history = chatStore.history.filter(
(msg) => msg.example && !msg.hide
);
chatStore.postBeginIndex = 0;
setChatStore({ ...chatStore });
}}
>
Yes, clear all history
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Button
variant="outline"
className="w-full"
onClick={() => {
let dataStr =
"data:text/json;charset=utf-8," +
encodeURIComponent(
JSON.stringify(chatStore, null, "\t")
);
let downloadAnchorNode =
document.createElement("a");
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute(
"download",
`chatgpt-api-web-${selectedChatIndex}.json`
);
document.body.appendChild(downloadAnchorNode);
downloadAnchorNode.click();
downloadAnchorNode.remove();
}}
>
{Tr("Export")}
</Button>
<Button
variant="outline"
className="w-full"
onClick={() => {
const name = prompt(
tr("Give this template a name:", langCode)
);
if (!name) {
alert(tr("No template name specified", langCode));
return;
}
const tmp: ChatStore = structuredClone(chatStore);
tmp.history = tmp.history.filter((h) => h.example);
// @ts-ignore
tmp.name = name;
templates.push(tmp as TemplateChatStore);
setTemplates([...templates]);
}}
>
{Tr("As template")}
</Button>
<Button
variant="outline"
className="w-full"
onClick={() => {
if (
!confirm(
tr(
"This will OVERWRITE the current chat history! Continue?",
langCode
)
)
)
return;
console.log("importFileRef", importFileRef);
importFileRef.current.click();
}}
>
Import
</Button>
<input
className="hidden"
ref={importFileRef}
type="file"
onChange={() => {
const file = importFileRef.current.files[0];
if (!file || file.type !== "application/json") {
alert(tr("Please select a json file", langCode));
return;
}
const reader = new FileReader();
reader.onload = () => {
if (!reader) {
alert(tr("Empty file", langCode));
return;
}
try {
const newChatStore: ChatStore = JSON.parse(
reader.result as string
);
if (!newChatStore.chatgpt_api_web_version) {
throw tr(
"This is not an exported chatgpt-api-web chatstore file. The key 'chatgpt_api_web_version' is missing!",
langCode
);
}
setChatStore({ ...newChatStore });
} catch (e) {
alert(
tr(
`Import error on parsing json:`,
langCode
) + `${e}`
);
}
};
reader.readAsText(file);
}}
/>
</div>
</div>
</div>
</>
</AccordionContent>
</AccordionItem>
<AccordionItem value="chat">
<AccordionTrigger>Chat</AccordionTrigger>
<AccordionContent>
<Card>
<CardHeader>
<CardTitle>Chat API</CardTitle>
<CardDescription>
Configure the LLM API settings
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<InputField
field="apiKey"
help="OPEN AI API 密钥,请勿泄漏此密钥"
{...props}
/>
<InputField
field="apiEndpoint"
help="API 端点,方便在不支持的地区使用反向代理服务,默认为 https://api.openai.com/v1/chat/completions"
{...props}
/>
<SetAPIsTemplate
label="Chat API"
endpoint={chatStore.apiEndpoint}
APIkey={chatStore.apiKey}
temps={templateAPIs}
setTemps={setTemplateAPIs}
/>
</CardContent>
</Card>
<Separator className="my-3" />
<SelectModel
help="模型,默认 3.5。不同模型性能和定价也不同,请参考 API 文档。"
{...props}
/>
<Slicer
field="temperature"
min={0}
max={2}
help="温度,数值越大模型生成文字的随机性越高。"
{...props}
/>
<Choice
field="streamMode"
help="流模式,使用 stream mode 将可以动态看到生成内容,但无法准确计算 token 数量,在 token 数量过多时可能会裁切过多或过少历史消息"
{...props}
/>
<Choice
field="logprobs"
help="返回每个Token的概率"
{...props}
/>
<Number
field="maxTokens"
help="最大上下文 token 数量。此值会根据选择的模型自动设置。"
readOnly={false}
{...props}
/>
<Number
field="maxGenTokens"
help="最大生成 Tokens 数量,可选值。"
readOnly={false}
{...props}
/>
<Number
field="tokenMargin"
help="当 totalTokens > maxTokens - tokenMargin 时会触发历史消息裁切chatgpt会“忘记”一部分对话中的消息但所有历史消息仍然保存在本地"
readOnly={false}
{...props}
/>
<Choice field="json_mode" help="JSON Mode" {...props} />
<Number
field="postBeginIndex"
help="指示发送 API 请求时要”忘记“多少历史消息"
readOnly={true}
{...props}
/>
<Number
field="totalTokens"
help="token总数每次对话都会更新此参数stream模式下该参数为估计值"
readOnly={true}
{...props}
/>
<Slicer
field="top_p"
min={0}
max={1}
help="Top P 采样方法。建议与温度采样方法二选一,不要同时开启。"
{...props}
/>
<Number
field="presence_penalty"
help="存在惩罚度"
readOnly={false}
{...props}
/>
<Number
field="frequency_penalty"
help="频率惩罚度"
readOnly={false}
{...props}
/>
</AccordionContent>
</AccordionItem>
<AccordionItem value="speech">
<AccordionTrigger>Speech Recognition</AccordionTrigger>
<AccordionContent>
<div className="space-y-4">
<Card>
<CardHeader>
<CardTitle>Whisper API</CardTitle>
<CardDescription>
Configure speech recognition settings
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<InputField
field="whisper_key"
help="Used for Whisper service. Defaults to the OpenAI key above, but can be configured separately here"
{...props}
/>
<InputField
field="whisper_api"
help="Whisper speech-to-text service. Service is enabled when this is set. Default: https://api.openai.com/v1/audio/transriptions"
{...props}
/>
<SetAPIsTemplate
label="Whisper API"
endpoint={chatStore.whisper_api}
APIkey={chatStore.whisper_key}
temps={templateAPIsWhisper}
setTemps={setTemplateAPIsWhisper}
/>
</CardContent>
</Card>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="tts">
<AccordionTrigger>TTS</AccordionTrigger>
<AccordionContent>
<Card>
<CardHeader>
<CardTitle>TTS API</CardTitle>
<CardDescription>
Configure text-to-speech settings
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<InputField
field="tts_key"
help="Text-to-speech service API key. Defaults to the OpenAI key above, but can be configured separately here"
{...props}
/>
<InputField
field="tts_api"
help="TTS API endpoint. Service is enabled when this is set. Default: https://api.openai.com/v1/audio/speech"
{...props}
/>
<SetAPIsTemplate
label="TTS API"
endpoint={chatStore.tts_api}
APIkey={chatStore.tts_key}
temps={templateAPIsTTS}
setTemps={setTemplateAPIsTTS}
/>
</CardContent>
</Card>
<div className="space-y-4">
<div className="space-y-2">
<Label className="flex items-center gap-2">
TTS Voice
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" size="icon">
<InfoIcon className="w-4 h-4" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>TTS Voice</DialogTitle>
<DialogDescription>
Select the voice style for text-to-speech
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</Label>
<Select
value={chatStore.tts_voice}
onValueChange={(value) => {
chatStore.tts_voice = value;
setChatStore({ ...chatStore });
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a voice" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Voices</SelectLabel>
{TTS_VOICES.map((opt) => (
<SelectItem key={opt} value={opt}>
{opt}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
<Slicer
min={0.25}
max={4.0}
field="tts_speed"
help="Adjust the playback speed of text-to-speech"
{...props}
/>
<div className="space-y-2">
<Label className="flex items-center gap-2">
TTS Format
<Dialog>
<DialogTrigger asChild>
<Button variant="ghost" size="icon">
<InfoIcon className="w-4 h-4" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>TTS Format</DialogTitle>
<DialogDescription>
Select the audio format for text-to-speech output
</DialogDescription>
</DialogHeader>
</DialogContent>
</Dialog>
</Label>
<Select
value={chatStore.tts_format}
onValueChange={(value) => {
chatStore.tts_format = value;
setChatStore({ ...chatStore });
}}
>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a format" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
<SelectLabel>Formats</SelectLabel>
{TTS_FORMAT.map((opt) => (
<SelectItem key={opt} value={opt}>
{opt}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</div>
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="image_gen">
<AccordionTrigger>Image Generation</AccordionTrigger>
<AccordionContent>
<Card>
<CardHeader>
<CardTitle>Image Generation API</CardTitle>
<CardDescription>
Configure image generation settings
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<InputField
field="image_gen_key"
help="Image generation service API key. Defaults to the OpenAI key above, but can be configured separately here"
{...props}
/>
<InputField
field="image_gen_api"
help="Image generation API endpoint. Service is enabled when this is set. Default: https://api.openai.com/v1/images/generations"
{...props}
/>
<SetAPIsTemplate
label="Image Gen API"
endpoint={chatStore.image_gen_api}
APIkey={chatStore.image_gen_key}
temps={templateAPIsImageGen}
setTemps={setTemplateAPIsImageGen}
/>
</CardContent>
</Card>
</AccordionContent>
</AccordionItem>
<AccordionItem value="templates">
<AccordionTrigger>Saved Template</AccordionTrigger>
<AccordionContent>
{templateAPIs.map((template, index) => (
<div key={index}>
<APIShowBlock
index={index}
label={template.name}
type="Chat"
apiField={template.endpoint}
keyField={template.key}
/>
</div>
))}
{templateAPIsWhisper.map((template, index) => (
<div key={index}>
<APIShowBlock
index={index}
label={template.name}
type="Whisper"
apiField={template.endpoint}
keyField={template.key}
/>
</div>
))}
{templateAPIsTTS.map((template, index) => (
<div key={index}>
<APIShowBlock
index={index}
label={template.name}
type="TTS"
apiField={template.endpoint}
keyField={template.key}
/>
</div>
))}
{templateAPIsImageGen.map((template, index) => (
<div key={index}>
<APIShowBlock
index={index}
label={template.name}
type="ImgGen"
apiField={template.endpoint}
keyField={template.key}
/>
</div>
))}
{templateTools.map((template, index) => (
<div key={index}>
<ToolsShowBlock
index={index}
label={template.name}
content={template.toolsString}
/>
</div>
))}
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="pt-4 space-y-2">
<p className="text-sm text-muted-foreground text-center">
chatgpt-api-web ChatStore {Tr("Version")}{" "}
{chatStore.chatgpt_api_web_version}
</p>
<p className="text-sm text-muted-foreground text-center">
{Tr("Documents and source code are avaliable here")}:{" "}
<a
className="underline hover:text-primary transition-colors"
href="https://github.com/heimoshuiyu/chatgpt-api-web"
target="_blank"
rel="noopener noreferrer"
>
github.com/heimoshuiyu/chatgpt-api-web
</a>
</p>
</div>
</NonOverflowScrollArea>
</SheetContent>
</Sheet>
);
};