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 { 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 { TemplateAttributeDialog } from "@/components/TemplateAttributeDialog";
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 (
{props.children}
);
};
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 (
Model
Model Selection
{props.help}
Custom
setUseCustomModel(!useCustomModel)}
/>
{useCustomModel ? (
) => {
chatStore.model = e.target.value;
setChatStore({ ...chatStore });
}}
/>
) : (
{
chatStore.model = model;
chatStore.maxTokens = models[model].maxToken;
setChatStore({ ...chatStore });
}}
>
Models
{Object.keys(models).map((opt) => (
{opt}
))}
)}
);
};
const LongInput = React.memo(
(props: {
field: "systemMessageContent" | "toolsString";
label: string;
help: string;
}) => {
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
const textareaRef = useRef(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) => {
setLocalValue(event.target.value);
};
const handleBlur = () => {
if (localValue !== chatStore[props.field]) {
chatStore[props.field] = localValue;
setChatStore({ ...chatStore });
}
};
return (
{props.label}{" "}
{props.label} Help
{props.help}
);
}
);
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 (
<>
>
);
};
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 (
{props.field}
{props.field}
{props.help}
setEnabled(!!checked)}
/>
{!chatStore[enable_filed_name] && (
disabled
)}
{enabled && (
)}
);
};
const Number = (props: {
field:
| "totalTokens"
| "maxTokens"
| "maxGenTokens"
| "tokenMargin"
| "postBeginIndex"
| "presence_penalty"
| "frequency_penalty";
readOnly: boolean;
help: string;
}) => {
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
return (
{props.field}
{props.field}
{props.help}
{props.field === "maxGenTokens" && (
{
const newChatStore = { ...chatStore };
newChatStore.maxGenTokens_enabled =
!newChatStore.maxGenTokens_enabled;
setChatStore({ ...newChatStore });
}}
/>
)}
{props.field === "presence_penalty" && (
{
const newChatStore = { ...chatStore };
newChatStore.presence_penalty_enabled =
!newChatStore.presence_penalty_enabled;
setChatStore({ ...newChatStore });
}}
/>
)}
{props.field === "frequency_penalty" && (
{
const newChatStore = { ...chatStore };
newChatStore.frequency_penalty_enabled =
!newChatStore.frequency_penalty_enabled;
setChatStore({ ...newChatStore });
}}
/>
)}
) => {
let newNumber = parseFloat(event.target.value);
if (newNumber < 0) newNumber = 0;
chatStore[props.field] = newNumber;
setChatStore({ ...chatStore });
}}
/>
);
};
const DefaultRenderMDCheckbox = () => {
const { defaultRenderMD, setDefaultRenderMD } = useContext(AppContext);
return (
{
setDefaultRenderMD(checked);
}}
/>
Render Markdown by Default
);
};
const Choice = (props: {
field: "streamMode" | "develop_mode" | "json_mode" | "logprobs";
help: string;
}) => {
const { chatStore, setChatStore } = useContext(AppChatStoreContext);
return (
{
chatStore[props.field] = checked;
setChatStore({ ...chatStore });
}}
/>
{props.field}
{props.field} Help
{props.help}
);
};
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 (
{props.type} {props.label}
Key
{props.keyField ? (
) : (
empty
)}
{
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
{
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
);
};
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 (
Tool {props.label}
Content
{JSON.stringify(JSON.parse(props.content), null, 2)}
{
const name = prompt(`Give the tool ${props.label} a new name`);
if (!name) return;
templateTools[props.index].name = name;
setTemplateTools(structuredClone(templateTools));
}}
>
Edit
{
if (!confirm(`Are you sure to delete ${props.label} Tool?`)) {
return;
}
templateTools.splice(props.index, 1);
setTemplateTools(structuredClone(templateTools));
}}
>
Delete
);
};
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(null);
const [totalCost, setTotalCost] = useState(getTotalCost());
// @ts-ignore
const { langCode, setLangCode } = useContext(langCodeContext);
const [open, setOpen] = useState(false);
const [showTemplateDialog, setShowTemplateDialog] = useState(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 (
Settings
{(!chatStore.apiKey || !chatStore.apiEndpoint) && (
)}
Settings
You can customize all the settings here
Session
Session Cost
Cost of the current session.
$ USD
{chatStore.cost?.toFixed(4)}
JSON Check:{" "}
{isVailedJSON(chatStore.toolsString) ? (
) : (
)}
{chatStore.toolsString.trim() && (
Save Tools
Save the tool as Template
Once saved, you can easily access your tools from
the dropdown menu.
{
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]);
}}
>
Save
Save
)}
System
<>
Accumulated Cost
in all sessions
$ USD
{totalCost.toFixed(4)}
{
clearTotalCost();
setTotalCost(getTotalCost());
}}
>
Reset Total Cost
Language
Languages
{Object.keys(LANG_OPTIONS).map((opt) => (
{LANG_OPTIONS[opt].name}
))}
Quick Actions
{
navigator.clipboard.writeText(link);
toast({
title: tr(`Copied link:`, langCode),
description: `${link}`,
});
}}
>
Copy Setting Link
Clear History
Are you absolutely sure?
This action cannot be undone. This will
permanently delete all chat history.
{
chatStore.history = chatStore.history.filter(
(msg) => msg.example && !msg.hide
);
chatStore.postBeginIndex = 0;
setChatStore({ ...chatStore });
}}
>
Yes, clear all history
{
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();
}}
>
Export
setShowTemplateDialog(true)}
>
As template
{
if (
!confirm(
tr(
"This will OVERWRITE the current chat history! Continue?",
langCode
)
)
)
return;
console.log("importFileRef", importFileRef);
importFileRef.current.click();
}}
>
Import
{
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);
}}
/>
>
Chat
Chat API
Configure the LLM API settings
maxTokens - tokenMargin, the history message will be truncated, chatgpt will "forget" part of the messages in the conversation (but all history messages are still saved locally)',
langCode
)}
readOnly={false}
{...props}
/>
Speech Recognition
Whisper API
Configure speech recognition settings
TTS
TTS API
Configure text-to-speech settings
TTS Voice
TTS Voice
Select the voice style for text-to-speech
{
chatStore.tts_voice = value;
setChatStore({ ...chatStore });
}}
>
Voices
{TTS_VOICES.map((opt) => (
{opt}
))}
TTS Format
TTS Format
Select the audio format for text-to-speech
output
{
chatStore.tts_format = value;
setChatStore({ ...chatStore });
}}
>
Formats
{TTS_FORMAT.map((opt) => (
{opt}
))}
Image Generation
Image Generation API
Configure image generation settings
Saved Template
{templateAPIs.map((template, index) => (
))}
{templateAPIsWhisper.map((template, index) => (
))}
{templateAPIsTTS.map((template, index) => (
))}
{templateAPIsImageGen.map((template, index) => (
))}
{templateTools.map((template, index) => (
))}
setShowTemplateDialog(false)}
onSave={(name, selectedAttributes) => {
const tmp: ChatStore = {
...chatStore,
...selectedAttributes,
history: chatStore.history.filter((h) => h.example),
};
// @ts-ignore
tmp.name = name;
templates.push(tmp as TemplateChatStore);
setTemplates([...templates]);
setShowTemplateDialog(false);
}}
/>
);
};