Files
chatgpt-api-web/src/settings.tsx
2023-11-03 23:51:31 +08:00

555 lines
18 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 { createRef } from "preact";
import { StateUpdater, useContext, useEffect, useState } from "preact/hooks";
import { ChatStore, TemplateAPI, clearTotalCost, getTotalCost } from "./app";
import models from "./models";
import { TemplateChatStore } from "./chatbox";
import { tr, Tr, langCodeContext, LANG_OPTIONS } from "./translate";
import p from "preact-markdown";
const Help = (props: { children: any; help: string }) => {
return (
<div>
<button
className="absolute"
onClick={() => {
alert(props.help);
}}
>
</button>
<p className="flex justify-between">{props.children}</p>
</div>
);
};
const SelectModel = (props: {
chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void;
help: string;
}) => {
return (
<Help help={props.help}>
<label className="m-2 p-2">Model</label>
<select
className="m-2 p-2"
value={props.chatStore.model}
onChange={(event: any) => {
const model = event.target.value as string;
props.chatStore.model = model;
props.chatStore.maxTokens = models[model].maxToken;
props.setChatStore({ ...props.chatStore });
}}
>
{Object.keys(models).map((opt) => (
<option value={opt}>{opt}</option>
))}
</select>
</Help>
);
};
const LongInput = (props: {
chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void;
field: "systemMessageContent";
help: string;
}) => {
return (
<Help help={props.help}>
<textarea
className="m-2 p-2 border rounded focus w-full"
value={props.chatStore[props.field]}
onChange={(event: any) => {
props.chatStore[props.field] = event.target.value;
props.setChatStore({ ...props.chatStore });
}}
></textarea>
</Help>
);
};
const Input = (props: {
chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void;
field: "apiKey" | "apiEndpoint" | "whisper_api" | "whisper_key";
help: string;
}) => {
const [hideInput, setHideInput] = useState(true);
return (
<Help help={props.help}>
<label className="m-2 p-2">{props.field}</label>
<span
className="m-2 p-2"
onClick={(event: any) => setHideInput(!hideInput)}
>
<label>hide</label>
<input type="checkbox" checked={hideInput} />
</span>
<input
type={hideInput ? "password" : "text"}
className="m-2 p-2 border rounded focus w-32 md:w-fit"
value={props.chatStore[props.field]}
onChange={(event: any) => {
props.chatStore[props.field] = event.target.value;
props.setChatStore({ ...props.chatStore });
}}
></input>
</Help>
);
};
const Slicer = (props: {
chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void;
field: "temperature" | "top_p";
help: string;
}) => {
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}>
<span>
<label className="m-2 p-2">{props.field}</label>
<input
type="checkbox"
checked={props.chatStore[enable_filed_name]}
onClick={() => {
setEnabled(!enabled);
}}
/>
</span>
<span>
<input
disabled={!enabled}
className="m-2 p-2 border rounded focus w-28"
type="range"
min="0"
max="1"
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 });
}}
/>
</span>
</Help>
);
};
const Number = (props: {
chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void;
field:
| "totalTokens"
| "maxTokens"
| "tokenMargin"
| "postBeginIndex"
| "presence_penalty"
| "frequency_penalty";
readOnly: boolean;
help: string;
}) => {
return (
<Help help={props.help}>
<label className="m-2 p-2">{props.field}</label>
<input
readOnly={props.readOnly}
type="number"
className="m-2 p-2 border rounded focus w-28"
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";
help: string;
}) => {
return (
<Help help={props.help}>
<label className="m-2 p-2">{props.field}</label>
<input
type="checkbox"
className="m-2 p-2 border rounded focus"
checked={props.chatStore[props.field]}
onChange={(event: any) => {
props.chatStore[props.field] = event.target.checked;
props.setChatStore({ ...props.chatStore });
}}
></input>
</Help>
);
};
export default (props: {
chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void;
setShow: StateUpdater<boolean>;
selectedChatStoreIndex: number;
templates: TemplateChatStore[];
setTemplates: (templates: TemplateChatStore[]) => void;
templateAPIs: TemplateAPI[];
setTemplateAPIs: (templateAPIs: TemplateAPI[]) => 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(() => {
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-screen h-full bg-black bg-opacity-50 z-10"
>
<div
onClick={(event: any) => {
event.stopPropagation();
}}
className="m-2 p-2 bg-white rounded-lg h-fit lg:w-2/3 z-20"
>
<h3 className="text-xl text-center flex justify-between">
<span>{Tr("Settings")}</span>
<select>
{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>
</h3>
<hr />
<div className="flex justify-between">
<button
className="p-2 m-2 rounded bg-purple-600 text-white"
onClick={() => {
navigator.clipboard.writeText(link);
alert(tr(`Copied link:`, langCode) + `${link}`);
}}
>
{Tr("Copy Setting Link")}
</button>
<button
className="p-2 m-2 rounded bg-rose-600 text-white"
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="p-2 m-2 rounded bg-cyan-600 text-white"
onClick={() => {
props.setShow(false);
}}
>
{Tr("Close")}
</button>
</div>
<p className="m-2 p-2">
{Tr("Total cost in this session")} ${props.chatStore.cost.toFixed(4)}
</p>
<hr />
<div className="box">
<LongInput
field="systemMessageContent"
help="系统消息用于指示ChatGPT的角色和一些前置条件例如“你是一个有帮助的人工智能助理”或者“你是一个专业英语翻译把我的话全部翻译成英语”详情参考 OPEAN AI API 文档"
{...props}
/>
<Input
field="apiKey"
help="OPEN AI API 密钥,请勿泄漏此密钥"
{...props}
/>
<Input
field="apiEndpoint"
help="API 端点,方便在不支持的地区使用反向代理服务,默认为 https://api.openai.com/v1/chat/completions"
{...props}
/>
<Choice
field="streamMode"
help="流模式,使用 stream mode 将可以动态看到生成内容,但无法准确计算 token 数量,在 token 数量过多时可能会裁切过多或过少历史消息"
{...props}
/>
<Choice
field="develop_mode"
help="开发者模式,拥有更多选项及功能"
{...props}
/>
<SelectModel
help="模型,默认 3.5。不同模型性能和定价也不同,请参考 API 文档。GPT-4 模型处于内测阶段,需要向 OPENAI 申请, 请确保您有访问权限)"
{...props}
/>
<Number
field="maxTokens"
help="最大 token 数量。如果使用非gpt-3.5模型请手动修改上限。gpt-4 & gpt-4-0314: 8192。gpt-4-32k & gpt-4-32k-0314: 32768"
readOnly={false}
{...props}
/>
<Number
field="tokenMargin"
help="当 totalTokens > maxTokens - tokenMargin 时会触发历史消息裁切chatgpt会“忘记”一部分对话中的消息但所有历史消息仍然保存在本地"
readOnly={false}
{...props}
/>
<Number
field="postBeginIndex"
help="指示发送 API 请求时要”忘记“多少历史消息"
readOnly={false}
{...props}
/>
<Number
field="totalTokens"
help="token总数每次对话都会更新此参数stream模式下该参数为估计值"
readOnly={true}
{...props}
/>
<Slicer field="temperature" help="温度" {...props} />
<Slicer field="top_p" help="top_p" {...props} />
<Number
field="presence_penalty"
help="presence_penalty"
readOnly={false}
{...props}
/>
<Number
field="frequency_penalty"
help="frequency_penalty"
readOnly={false}
{...props}
/>
<Input
field="whisper_api"
help="Whisper 语言转文字服务填入此api才会开启默认为 https://api.openai.com/v1/audio/transriptions"
{...props}
/>
<Input
field="whisper_key"
help="用于 Whisper 服务的 key默认为 上方使用的OPENAI key可在此单独配置专用key"
{...props}
/>
<div className="flex justify-between">
<p className="m-2 p-2">
{Tr("Accumulated cost in all sessions")} ${totalCost.toFixed(4)}
</p>
<button
className="p-2 m-2 rounded bg-emerald-500"
onClick={() => {
clearTotalCost();
setTotalCost(getTotalCost());
}}
>
{Tr("Reset")}
</button>
</div>
<p className="flex justify-evenly">
<button
className="p-2 m-2 rounded bg-amber-500"
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="p-2 m-2 rounded bg-amber-500"
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 = "";
// @ts-ignore
tmp.name = name;
props.templates.push(tmp as TemplateChatStore);
props.setTemplates([...props.templates]);
}}
>
{Tr("As template")}
</button>
<button
className="p-2 m-2 rounded bg-amber-500"
onClick={() => {
const name = prompt("Give this **API** template a name:");
if (!name) {
alert("No template name specified");
return;
}
const tmp: TemplateAPI = {
name,
endpoint: props.chatStore.apiEndpoint,
key: props.chatStore.apiKey,
};
props.templateAPIs.push(tmp);
props.setTemplateAPIs([...props.templateAPIs]);
}}
>
{Tr("As API Template")}
</button>
<button
className="p-2 m-2 rounded bg-amber-500"
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];
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);
}}
/>
</p>
<p className="text-center m-2 p-2">
chatgpt-api-web ChatStore {Tr("Version")}{" "}
{props.chatStore.chatgpt_api_web_version}
</p>
</div>
<hr />
</div>
</div>
);
};