1009 lines
35 KiB
TypeScript
1009 lines
35 KiB
TypeScript
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>
|
||
);
|
||
};
|