Files
chatgpt-api-web/src/settings.tsx

1009 lines
35 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 {
InformationCircleIcon,
CheckIcon,
NoSymbolIcon,
CogIcon,
KeyIcon,
EyeIcon,
EllipsisHorizontalCircleIcon,
HandRaisedIcon,
AdjustmentsHorizontalIcon,
Cog6ToothIcon,
ListBulletIcon,
} from "@heroicons/react/24/outline";
import { createRef } from "preact";
import {
StateUpdater,
useContext,
useEffect,
useState,
Dispatch,
} from "preact/hooks";
import { clearTotalCost, getTotalCost } from "@/app";
import {
ChatStore,
TemplateChatStore,
TemplateAPI,
TemplateTools,
} from "@/types/chatstore";
import { models } from "@/types/models";
import { tr, Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
import { isVailedJSON } from "@/message";
import { SetAPIsTemplate } from "@/setAPIsTemplate";
import { autoHeight } from "@/textarea";
import { getDefaultParams } from "@/utils/getDefaultParam";
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: {
chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void;
help: string;
}) => {
let shouldIUseCustomModel: boolean = true;
for (const model in models) {
if (props.chatStore.model === model) {
shouldIUseCustomModel = false;
}
}
const [useCustomModel, setUseCustomModel] = useState(shouldIUseCustomModel);
return (
<Help help={props.help} field="">
<div className="label">
<span className="flex gap-2 items-center">
<ListBulletIcon className="w-4 h-4" />
Model
</span>{" "}
<div className="flex gap-3">
<span className="label-text">
<span className="label-text flex gap-2 items-center">
<Cog6ToothIcon className="w-4 h-4" />
{Tr("Custom")}
</span>
</span>
<span className="label-text-alt">
<input
type="checkbox"
checked={useCustomModel}
className="checkbox"
onClick={() => {
setUseCustomModel(!useCustomModel);
}}
/>
</span>
</div>
{/* <span className="label-text-alt">Top Right label</span> */}
</div>
<label className="form-control w-full">
{useCustomModel ? (
<input
className="input input-bordered"
value={props.chatStore.model}
onChange={(event: any) => {
const model = event.target.value as string;
props.chatStore.model = model;
props.setChatStore({ ...props.chatStore });
}}
/>
) : (
<select
className="select select-bordered"
value={props.chatStore.model}
onChange={(event: any) => {
const model = event.target.value as string;
props.chatStore.model = model;
props.chatStore.maxTokens = getDefaultParams(
"max",
models[model].maxToken,
);
props.setChatStore({ ...props.chatStore });
}}
>
{Object.keys(models).map((opt) => (
<option value={opt}>{opt}</option>
))}
</select>
)}
</label>
<div className="pb-5"></div>
</Help>
);
};
const LongInput = (props: {
chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void;
field: "systemMessageContent" | "toolsString";
label: string;
help: string;
}) => {
return (
<label className="form-control">
<div className="label">
<span className="label-text">{props.label}</span>
<span className="label-text-alt">
<button
onClick={() => {
alert(props.help);
}}
>
<InformationCircleIcon className="w-4 h-4" />
</button>
</span>
</div>
<textarea
className="textarea textarea-bordered h-24 w-full"
value={props.chatStore[props.field]}
onChange={(event: any) => {
props.chatStore[props.field] = event.target.value;
props.setChatStore({ ...props.chatStore });
autoHeight(event.target);
}}
onKeyPress={(event: any) => {
autoHeight(event.target);
}}
></textarea>
</label>
);
};
const Input = (props: {
chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void;
field:
| "apiKey"
| "apiEndpoint"
| "whisper_api"
| "whisper_key"
| "tts_api"
| "tts_key"
| "image_gen_api"
| "image_gen_key";
help: string;
}) => {
const [hideInput, setHideInput] = useState(true);
return (
<Help field={props.field} help={props.help}>
<div className="label">
<span className="label-text">{props.field}</span>
<span className="label-text-alt">
<button
onClick={() => {
alert(props.help);
}}
>
<InformationCircleIcon className="w-4 h-4" />
</button>
</span>
</div>
<label className="input input-bordered flex items-center gap-2 grow">
{hideInput ? (
<EyeIcon
className="w-4 h-4"
onClick={() => {
setHideInput(!hideInput);
console.log("clicked", hideInput);
}}
/>
) : (
<KeyIcon
className="w-4 h-4"
onClick={() => {
setHideInput(!hideInput);
console.log("clicked", hideInput);
}}
/>
)}
<input
type={hideInput ? "password" : "text"}
className="grow"
value={props.chatStore[props.field]}
onChange={(event: any) => {
props.chatStore[props.field] = event.target.value;
props.setChatStore({ ...props.chatStore });
}}
/>
</label>
</Help>
);
};
const Slicer = (props: {
chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void;
field: "temperature" | "top_p" | "tts_speed";
help: string;
min: number;
max: number;
}) => {
const enable_filed_name: "temperature_enabled" | "top_p_enabled" =
`${props.field}_enabled` as any;
const enabled = props.chatStore[enable_filed_name];
if (enabled === null || enabled === undefined) {
if (props.field === "temperature") {
props.chatStore[enable_filed_name] = true;
}
if (props.field === "top_p") {
props.chatStore[enable_filed_name] = false;
}
}
const setEnabled = (state: boolean) => {
props.chatStore[enable_filed_name] = state;
props.setChatStore({ ...props.chatStore });
};
return (
<Help help={props.help} field={props.field}>
<span className="py-3">
<div className="form-control">
<label className="flex gap-2">
<span className="label-text flex items-center gap-2">
<AdjustmentsHorizontalIcon className="w-4 h-4" />
{props.field}{" "}
<button
onClick={() => {
alert(props.help);
}}
>
<InformationCircleIcon className="w-4 h-4" />
</button>
</span>
<input
type="checkbox"
checked={props.chatStore[enable_filed_name]}
className="checkbox"
onClick={() => {
setEnabled(!enabled);
}}
/>
</label>
</div>
</span>
{enabled ? (
<div className="flex items-center">
<input
disabled={!enabled}
className="range"
type="range"
min={props.min}
max={props.max}
step="0.01"
value={props.chatStore[props.field]}
onChange={(event: any) => {
const value = parseFloat(event.target.value);
props.chatStore[props.field] = value;
props.setChatStore({ ...props.chatStore });
}}
/>
<input
disabled={!enabled}
className="m-2 p-2 border rounded focus w-28"
type="number"
value={props.chatStore[props.field]}
onChange={(event: any) => {
const value = parseFloat(event.target.value);
props.chatStore[props.field] = value;
props.setChatStore({ ...props.chatStore });
}}
/>
</div>
) : (
""
)}
</Help>
);
};
const Number = (props: {
chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void;
field:
| "totalTokens"
| "maxTokens"
| "maxGenTokens"
| "tokenMargin"
| "postBeginIndex"
| "presence_penalty"
| "frequency_penalty";
readOnly: boolean;
help: string;
}) => {
return (
<Help help={props.help} field="">
<span>
<label className="py-2 flex items-center gap-1">
<EllipsisHorizontalCircleIcon className="h-4 w-4" /> {props.field}{" "}
<button
onClick={() => {
alert(props.help);
}}
>
<InformationCircleIcon className="w-4 h-4" />
</button>
{props.field === "maxGenTokens" && (
<div className="form-control">
<label className="label grow">
<input
type="checkbox"
checked={props.chatStore.maxGenTokens_enabled}
onChange={() => {
const newChatStore = { ...props.chatStore };
newChatStore.maxGenTokens_enabled =
!newChatStore.maxGenTokens_enabled;
props.setChatStore({ ...newChatStore });
}}
className="checkbox"
/>
</label>
</div>
)}
</label>
</span>
<input
readOnly={props.readOnly}
disabled={
props.field === "maxGenTokens" &&
!props.chatStore.maxGenTokens_enabled
}
type="number"
className="input input-bordered input-sm w-full"
value={props.chatStore[props.field]}
onChange={(event: any) => {
console.log("type", typeof event.target.value);
let newNumber = parseFloat(event.target.value);
if (newNumber < 0) newNumber = 0;
props.chatStore[props.field] = newNumber;
props.setChatStore({ ...props.chatStore });
}}
></input>
</Help>
);
};
const Choice = (props: {
chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void;
field: "streamMode" | "develop_mode" | "json_mode" | "logprobs";
help: string;
}) => {
return (
<Help help={props.help} field={props.field}>
<div className="form-control">
<label className="py-2 flex items-center gap-1">
<CheckIcon className="h-4 w-4" />
<span className="label-text">{props.field}</span>
<button
onClick={() => {
alert(props.help);
}}
>
<InformationCircleIcon className="w-4 h-4" />
</button>
<input
type="checkbox"
checked={props.chatStore[props.field]}
className="checkbox"
onChange={(event: any) => {
props.chatStore[props.field] = event.target.checked;
props.setChatStore({ ...props.chatStore });
}}
/>
</label>
</div>
</Help>
);
};
export default (props: {
chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void;
setShow: Dispatch<StateUpdater<boolean>>;
selectedChatStoreIndex: number;
templates: TemplateChatStore[];
setTemplates: (templates: TemplateChatStore[]) => void;
templateAPIs: TemplateAPI[];
setTemplateAPIs: (templateAPIs: TemplateAPI[]) => void;
templateAPIsWhisper: TemplateAPI[];
setTemplateAPIsWhisper: (templateAPIs: TemplateAPI[]) => void;
templateAPIsTTS: TemplateAPI[];
setTemplateAPIsTTS: (templateAPIs: TemplateAPI[]) => void;
templateAPIsImageGen: TemplateAPI[];
setTemplateAPIsImageGen: (templateAPIs: TemplateAPI[]) => void;
templateTools: TemplateTools[];
setTemplateTools: (templateTools: TemplateTools[]) => void;
}) => {
let link =
location.protocol +
"//" +
location.host +
location.pathname +
`?key=${encodeURIComponent(
props.chatStore.apiKey,
)}&api=${encodeURIComponent(props.chatStore.apiEndpoint)}&mode=${
props.chatStore.streamMode ? "stream" : "fetch"
}&model=${props.chatStore.model}&sys=${encodeURIComponent(
props.chatStore.systemMessageContent,
)}`;
if (props.chatStore.develop_mode) {
link = link + `&dev=true`;
}
const importFileRef = createRef();
const [totalCost, setTotalCost] = useState(getTotalCost());
// @ts-ignore
const { langCode, setLangCode } = useContext(langCodeContext);
useEffect(() => {
themeChange(false);
const handleKeyPress = (event: any) => {
if (event.keyCode === 27) {
// keyCode for ESC key is 27
props.setShow(false);
}
};
document.addEventListener("keydown", handleKeyPress);
return () => {
document.removeEventListener("keydown", handleKeyPress);
};
}, []); // The empty dependency array ensures that the effect runs only once
return (
<div
onClick={() => props.setShow(false)}
className="left-0 top-0 overflow-scroll flex justify-center absolute w-full h-full z-10"
>
<div
onClick={(event: any) => {
event.stopPropagation();
}}
className="modal-box"
>
<div className="flex justify-between items-center mb-2">
<h3 className="text-lg font-bold">{Tr("Settings")}</h3>
<button
className="btn btn-secondary btn-sm"
onClick={() => {
props.setShow(false);
}}
>
{Tr("Close")}
</button>
</div>
<div className="join join-vertical w-full">
<div className="join-item collapse collapse-plus bg-base-200">
<input type="radio" name="setting-accordion" />
<div className="collapse-title text-xl font-medium">Session</div>
<div className="collapse-content">
<div className="stats shadow">
<div className="stat">
<div className="stat-title">Session cost</div>
<div className="stat-value">
{props.chatStore.cost.toFixed(4)} $
</div>
</div>
</div>
<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(props.chatStore.toolsString) ? (
<CheckIcon className="inline w-4 h-4" />
) : (
<NoSymbolIcon className="inline w-4 h-4" />
)}
</span>
<div className="box">
<div className="flex justify-evenly flex-wrap">
{props.chatStore.toolsString.trim() && (
<button
className="btn"
onClick={() => {
const name = prompt(
`Give this **Tools** template a name:`,
);
if (!name) {
alert("No template name specified");
return;
}
const newToolsTmp: TemplateTools = {
name,
toolsString: props.chatStore.toolsString,
};
props.templateTools.push(newToolsTmp);
props.setTemplateTools([...props.templateTools]);
}}
>
{Tr(`Save Tools`)}
</button>
)}
</div>
</div>
</div>
</div>
<div className="join-item collapse collapse-plus bg-base-200">
<input type="radio" name="setting-accordion" />
<div className="collapse-title text-xl font-medium">System</div>
<div className="collapse-content">
<div className="stats shadow">
<div className="stat">
<div className="stat-title">Accumulated cost</div>
<div className="stat-value">{totalCost.toFixed(4)} $</div>
<div className="stat-desc">
in all sessions -{" "}
<a
className="btn btn-xs btn-primary"
onClick={() => {
clearTotalCost();
setTotalCost(getTotalCost());
}}
>
Reset
</a>
</div>
</div>
</div>
<Choice
field="develop_mode"
help="开发者模式,开启后会显示更多选项及功能"
{...props}
/>
<label className="form-control w-full">
<div className="label">
<span className="label-text">Theme Switch</span>
</div>
<select data-choose-theme className="select select-bordered">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="cupcake">Cupcake</option>
<option value="halloween">Halloween</option>
<option value="wireframe">Wireframe</option>
<option value="sunset">Sunset</option>
<option value="lemonade">Lemonade</option>
<option value="luxury">Luxury</option>
<option value="cyberpunk">Cyberpunk</option>
<option value="black">Black</option>
</select>
</label>
<label className="form-control w-full">
<div className="label">
<span className="label-text">Language</span>
</div>
<select className="select select-bordered">
{Object.keys(LANG_OPTIONS).map((opt) => (
<option
value={opt}
selected={opt === (langCodeContext as any).langCode}
onClick={(event: any) => {
console.log("set lang code", event.target.value);
setLangCode(event.target.value);
}}
>
{LANG_OPTIONS[opt].name}
</option>
))}
</select>
</label>
<label className="form-control w-full">
<div className="label">
<span className="label-text">Quick Actions</span>
</div>
<div className="flex flex-col gap-2">
<button
className="btn btn-sm btn-outline btn-neural"
onClick={() => {
navigator.clipboard.writeText(link);
alert(tr(`Copied link:`, langCode) + `${link}`);
}}
>
{Tr("Copy Setting Link")}
</button>
<button
className="btn btn-sm btn-outline btn-neural"
onClick={() => {
if (
!confirm(
tr("Are you sure to clear all history?", langCode),
)
)
return;
props.chatStore.history = props.chatStore.history.filter(
(msg) => msg.example && !msg.hide,
);
props.chatStore.postBeginIndex = 0;
props.setChatStore({ ...props.chatStore });
}}
>
{Tr("Clear History")}
</button>
<button
className="btn btn-sm btn-outline btn-neural"
onClick={() => {
let dataStr =
"data:text/json;charset=utf-8," +
encodeURIComponent(
JSON.stringify(props.chatStore, null, "\t"),
);
let downloadAnchorNode = document.createElement("a");
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute(
"download",
`chatgpt-api-web-${props.selectedChatStoreIndex}.json`,
);
document.body.appendChild(downloadAnchorNode); // required for firefox
downloadAnchorNode.click();
downloadAnchorNode.remove();
}}
>
{Tr("Export")}
</button>
<button
className="btn btn-sm btn-outline btn-neural"
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(props.chatStore);
tmp.history = tmp.history.filter((h) => h.example);
// clear api because it is stored in the API template
tmp.apiEndpoint = "";
tmp.apiKey = "";
tmp.whisper_api = "";
tmp.whisper_key = "";
tmp.tts_api = "";
tmp.tts_key = "";
tmp.image_gen_api = "";
tmp.image_gen_key = "";
// @ts-ignore
tmp.name = name;
props.templates.push(tmp as TemplateChatStore);
props.setTemplates([...props.templates]);
}}
>
{Tr("As template")}
</button>
<button
className="btn btn-sm btn-outline btn-neural"
onClick={() => {
if (
!confirm(
tr(
"This will OVERWRITE the current chat history! Continue?",
langCode,
),
)
)
return;
console.log("importFileRef", importFileRef);
importFileRef.current.click();
}}
>
Import
</button>
</div>
<input
className="hidden"
ref={importFileRef}
type="file"
onChange={() => {
const file = importFileRef.current.files[0];
console.log("file to import", file);
if (!file || file.type !== "application/json") {
alert(tr("Please select a json file", langCode));
return;
}
const reader = new FileReader();
reader.onload = () => {
console.log("import content", reader.result);
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,
);
}
props.setChatStore({ ...newChatStore });
} catch (e) {
alert(
tr(`Import error on parsing json:`, langCode) +
`${e}`,
);
}
};
reader.readAsText(file);
}}
/>
</label>
</div>
</div>
<div className="join-item collapse collapse-plus bg-base-200">
<input type="radio" name="setting-accordion" />
<div className="collapse-title text-xl font-medium">Chat</div>
<div className="collapse-content">
<div>
<h2 className="card-title">Chat API</h2>
<p>
<Input
field="apiKey"
help="OPEN AI API 密钥,请勿泄漏此密钥"
{...props}
/>
<Input
field="apiEndpoint"
help="API 端点,方便在不支持的地区使用反向代理服务,默认为 https://api.openai.com/v1/chat/completions"
{...props}
/>
</p>
<SetAPIsTemplate
label="Chat API"
endpoint={props.chatStore.apiEndpoint}
APIkey={props.chatStore.apiKey}
tmps={props.templateAPIs}
setTmps={props.setTemplateAPIs}
/>
<div className="divider" />
</div>
<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}
/>
</div>
</div>
<div className="join-item collapse collapse-plus bg-base-200">
<input type="radio" name="setting-accordion" />
<div className="collapse-title text-xl font-medium">
Speech Recognition
</div>
<div className="collapse-content">
<div>
<h2 className="card-title">Whisper API</h2>
<p>
<Input
field="whisper_key"
help="用于 Whisper 服务的 key默认为 上方使用的OPENAI key可在此单独配置专用key"
{...props}
/>
<Input
field="whisper_api"
help="Whisper 语言转文字服务填入此api才会开启默认为 https://api.openai.com/v1/audio/transriptions"
{...props}
/>
</p>
<SetAPIsTemplate
label="Whisper API"
endpoint={props.chatStore.whisper_api}
APIkey={props.chatStore.whisper_key}
tmps={props.templateAPIsWhisper}
setTmps={props.setTemplateAPIsWhisper}
/>
<div className="divider" />
</div>
</div>
</div>
<div className="join-item collapse collapse-plus bg-base-200">
<input type="radio" name="setting-accordion" />
<div className="collapse-title text-xl font-medium">TTS</div>
<div className="collapse-content">
<div>
<h2 className="card-title">TTS API</h2>
<p>
<Input
field="tts_key"
help="tts service api key"
{...props}
/>
<Input
field="tts_api"
help="tts api, eg. https://api.openai.com/v1/audio/speech"
{...props}
/>
</p>
<SetAPIsTemplate
label="TTS API"
endpoint={props.chatStore.tts_api}
APIkey={props.chatStore.tts_key}
tmps={props.templateAPIsTTS}
setTmps={props.setTemplateAPIsTTS}
/>
<div className="divider" />
</div>
<Help help="tts voice style" field="AAAAA">
<label className="">TTS Voice</label>
<select
className="select select-bordered"
value={props.chatStore.tts_voice}
onChange={(event: any) => {
const voice = event.target.value as string;
props.chatStore.tts_voice = voice;
props.setChatStore({ ...props.chatStore });
}}
>
{TTS_VOICES.map((opt) => (
<option value={opt}>{opt}</option>
))}
</select>
</Help>
<Slicer
min={0.25}
max={4.0}
field="tts_speed"
help={"TTS Speed"}
{...props}
/>
<Help help="tts response format" field="AAAAA">
<label className="">TTS Format</label>
<select
className="select select-bordered"
value={props.chatStore.tts_format}
onChange={(event: any) => {
const format = event.target.value as string;
props.chatStore.tts_format = format;
props.setChatStore({ ...props.chatStore });
}}
>
{TTS_FORMAT.map((opt) => (
<option value={opt}>{opt}</option>
))}
</select>
</Help>
</div>
</div>
<div className="join-item collapse collapse-plus bg-base-200">
<input type="radio" name="setting-accordion" />
<div className="collapse-title text-xl font-medium">
Image Generation
</div>
<div className="collapse-content">
<div>
<h2 className="card-title">Image Gen API</h2>
<p>
<Input
field="image_gen_key"
help="image generation service api key"
{...props}
/>
<Input
field="image_gen_api"
help="DALL image gen key, eg. https://api.openai.com/v1/images/generations"
{...props}
/>
</p>
<SetAPIsTemplate
label="Image Gen API"
endpoint={props.chatStore.image_gen_api}
APIkey={props.chatStore.image_gen_key}
tmps={props.templateAPIsImageGen}
setTmps={props.setTemplateAPIsImageGen}
/>
<div className="divider" />
</div>
</div>
</div>
</div>
<div className="pt-4 pb-2">
<p className="text-center">
chatgpt-api-web ChatStore {Tr("Version")}{" "}
{props.chatStore.chatgpt_api_web_version}
</p>
<p className="text-center">
{Tr("Documents and source code are avaliable here")}:{" "}
<a
className="underline"
href="https://github.com/heimoshuiyu/chatgpt-api-web"
target="_blank"
>
github.com/heimoshuiyu/chatgpt-api-web
</a>
</p>
</div>
</div>
</div>
);
};