reconstitution ui with daisyui

This commit is contained in:
ecwu
2024-07-16 21:51:58 +08:00
parent 4079ec77f9
commit 0ae53ff954
9 changed files with 3134 additions and 766 deletions

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html data-theme="cupcake" lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta <meta

2268
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@heroicons/react": "^2.1.5",
"@types/ungap__structured-clone": "^0.3.1", "@types/ungap__structured-clone": "^0.3.1",
"@ungap/structured-clone": "^1.2.0", "@ungap/structured-clone": "^1.2.0",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
@@ -21,6 +22,8 @@
}, },
"devDependencies": { "devDependencies": {
"@preact/preset-vite": "^2.6.0", "@preact/preset-vite": "^2.6.0",
"daisyui": "^4.12.10",
"theme-change": "^2.5.0",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^4.5.0" "vite": "^4.5.0"
} }

View File

@@ -228,7 +228,9 @@ export function App() {
if (oldVersion < 11) { if (oldVersion < 11) {
if (oldVersion < 11 && oldVersion >= 1) { if (oldVersion < 11 && oldVersion >= 1) {
alert("Start upgrading storage, just a sec... (Click OK to continue)"); alert(
"Start upgrading storage, just a sec... (Click OK to continue)"
);
} }
if ( if (
transaction transaction
@@ -401,16 +403,16 @@ export function App() {
}, []); }, []);
return ( return (
<div className="flex text-sm h-full bg-slate-200 dark:bg-slate-800 dark:text-white"> <div className="flex text-sm h-full">
<div className="flex flex-col h-full p-2 border-r-indigo-500 border-2 dark:border-slate-800 dark:border-r-indigo-500 dark:text-black"> <div className="flex flex-col h-full p-2 bg-primary">
<div className="grow overflow-scroll"> <div className="grow overflow-scroll">
<button <button
className="w-full bg-violet-300 p-1 rounded hover:bg-violet-400" className="btn btn-sm btn-info p-1 my-1 w-full"
onClick={handleNewChatStore} onClick={handleNewChatStore}
> >
{Tr("NEW")} {Tr("NEW")}
</button> </button>
<ul> <ul class="pt-2">
{(allChatStoreIndexes as number[]) {(allChatStoreIndexes as number[])
.slice() .slice()
.reverse() .reverse()
@@ -419,8 +421,8 @@ export function App() {
return ( return (
<li> <li>
<button <button
className={`w-full my-1 p-1 rounded hover:bg-blue-500 ${ className={`w-full my-1 p-1 btn btn-sm ${
i === selectedChatIndex ? "bg-blue-500" : "bg-blue-200" i === selectedChatIndex ? "btn-accent" : "btn-secondary"
}`} }`}
onClick={() => { onClick={() => {
setSelectedChatIndex(i); setSelectedChatIndex(i);
@@ -433,12 +435,18 @@ export function App() {
})} })}
</ul> </ul>
</div> </div>
<div>
<button <button
className="rounded bg-rose-400 p-1 my-1 w-full" className="btn btn-warning btn-sm p-1 my-1 w-full"
onClick={async () => { onClick={async () => {
if (!confirm("Are you sure you want to delete this chat history?")) if (
!confirm("Are you sure you want to delete this chat history?")
)
return; return;
console.log("remove item", `${STORAGE_NAME}-${selectedChatIndex}`); console.log(
"remove item",
`${STORAGE_NAME}-${selectedChatIndex}`
);
(await db).delete(STORAGE_NAME, selectedChatIndex); (await db).delete(STORAGE_NAME, selectedChatIndex);
const newAllChatStoreIndexes = await ( const newAllChatStoreIndexes = await (
await db await db
@@ -461,7 +469,7 @@ export function App() {
</button> </button>
{chatStore.develop_mode && ( {chatStore.develop_mode && (
<button <button
className="rounded bg-rose-800 p-1 my-1 w-full text-white" className="btn btn-sm btn-warning p-1 my-1 w-full"
onClick={async () => { onClick={async () => {
if ( if (
!confirm( !confirm(
@@ -480,6 +488,7 @@ export function App() {
</button> </button>
)} )}
</div> </div>
</div>
<ChatBOX <ChatBOX
db={db} db={db}
chatStore={chatStore} chatStore={chatStore}

View File

@@ -34,6 +34,7 @@ import { ListToolsTempaltes } from "./listToolsTemplates";
import { autoHeight } from "./textarea"; import { autoHeight } from "./textarea";
import Search from "./search"; import Search from "./search";
import { IDBPDatabase } from "idb"; import { IDBPDatabase } from "idb";
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
export interface TemplateChatStore extends ChatStore { export interface TemplateChatStore extends ChatStore {
name: string; name: string;
@@ -434,7 +435,7 @@ export default function ChatBOX(props: {
const userInputRef = createRef(); const userInputRef = createRef();
return ( return (
<div className="grow flex flex-col p-2 dark:text-black"> <div className="grow flex flex-col p-2">
{showSettings && ( {showSettings && (
<Settings <Settings
chatStore={chatStore} chatStore={chatStore}
@@ -464,11 +465,11 @@ export default function ChatBOX(props: {
/> />
)} )}
<div <div
className="relative cursor-pointer rounded bg-cyan-300 dark:text-white p-1 dark:bg-cyan-800" className="relative cursor-pointer rounded p-2 bg-base-200"
onClick={() => setShowSettings(true)} onClick={() => setShowSettings(true)}
> >
<button <button
className="absolute right-1 bg-gray-300 rounded p-1 m-1" className="absolute right-1 rounded p-1 m-1"
onClick={(event) => { onClick={(event) => {
// stop propagation to parent // stop propagation to parent
event.stopPropagation(); event.stopPropagation();
@@ -476,7 +477,7 @@ export default function ChatBOX(props: {
setShowSearch(true); setShowSearch(true);
}} }}
> >
🔍 <MagnifyingGlassIcon class="w-5 h-5" />
</button> </button>
<div> <div>
<button className="underline"> <button className="underline">
@@ -514,12 +515,12 @@ export default function ChatBOX(props: {
</div> </div>
<div className="grow overflow-scroll"> <div className="grow overflow-scroll">
{!chatStore.apiKey && ( {!chatStore.apiKey && (
<p className="opacity-60 p-6 rounded bg-white my-3 text-left dark:text-black"> <p className="bg-base-200 p-6 rounded my-3 text-left">
{Tr("Please click above to set")} (OpenAI) API KEY {Tr("Please click above to set")} (OpenAI) API KEY
</p> </p>
)} )}
{!chatStore.apiEndpoint && ( {!chatStore.apiEndpoint && (
<p className="opacity-60 p-6 rounded bg-white my-3 text-left dark:text-black"> <p className="bg-base-200 p-6 rounded my-3 text-left">
{Tr("Please click above to set")} API Endpoint {Tr("Please click above to set")} API Endpoint
</p> </p>
)} )}
@@ -581,7 +582,7 @@ export default function ChatBOX(props: {
)} )}
{chatStore.history.filter((msg) => !msg.example).length == 0 && ( {chatStore.history.filter((msg) => !msg.example).length == 0 && (
<div className="break-all opacity-80 p-3 rounded bg-white my-3 text-left dark:text-black"> <div className="bg-base-200 break-all p-3 my-3 text-left">
<h2> <h2>
<span>{Tr("Saved prompt templates")}</span> <span>{Tr("Saved prompt templates")}</span>
<button <button
@@ -596,7 +597,7 @@ export default function ChatBOX(props: {
{Tr("Reset Current")} {Tr("Reset Current")}
</button> </button>
</h2> </h2>
<hr className="my-2" /> <div class="divider"></div>
<div className="flex flex-wrap"> <div className="flex flex-wrap">
{templates.map((t, index) => ( {templates.map((t, index) => (
<div <div
@@ -729,7 +730,7 @@ export default function ChatBOX(props: {
<p className="text-center"> <p className="text-center">
{chatStore.history.length > 0 && ( {chatStore.history.length > 0 && (
<button <button
className="disabled:line-through disabled:bg-slate-500 rounded m-2 p-2 border-2 bg-teal-500 hover:bg-teal-600" className="btn btn-warning disabled:line-through disabled:btn-neutral disabled:text-white m-2 p-2"
disabled={showGenerating} disabled={showGenerating}
onClick={async () => { onClick={async () => {
const messageIndex = chatStore.history.length - 1; const messageIndex = chatStore.history.length - 1;
@@ -749,7 +750,7 @@ export default function ChatBOX(props: {
)} )}
{chatStore.develop_mode && chatStore.history.length > 0 && ( {chatStore.develop_mode && chatStore.history.length > 0 && (
<button <button
className="disabled:line-through disabled:bg-slate-500 rounded m-2 p-2 border-2 bg-yellow-500 hover:bg-yellow-600" className="btn btn-warning disabled:line-through disabled:bg-neural m-2 p-2"
disabled={showGenerating} disabled={showGenerating}
onClick={async () => { onClick={async () => {
await complete(); await complete();
@@ -852,15 +853,16 @@ export default function ChatBOX(props: {
<input type="checkbox" checked={follow} /> <input type="checkbox" checked={follow} />
</span> </span>
)} )}
<div className="flex justify-between"> <div className="flex justify-between">
<button <button
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600" className="btn btn-primary disabled:line-through disabled:text-white disabled:bg-neutral m-1 p-1"
disabled={showGenerating || !chatStore.apiKey} disabled={showGenerating || !chatStore.apiKey}
onClick={() => { onClick={() => {
setShowAddImage(!showAddImage); setShowAddImage(!showAddImage);
}} }}
> >
Img Image
</button> </button>
{showAddImage && ( {showAddImage && (
<AddImage <AddImage
@@ -891,11 +893,11 @@ export default function ChatBOX(props: {
autoHeight(event.target); autoHeight(event.target);
setInputMsg(event.target.value); setInputMsg(event.target.value);
}} }}
className="rounded grow m-1 p-1 border-2 border-gray-400 w-0" className="textarea textarea-bordered textarea-xs grow m-1 p-1 w-0"
placeholder="Type here..." placeholder="Type here..."
></textarea> ></textarea>
<button <button
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600" className="btn btn-primary disabled:btn-neutral disabled:line-through m-1 p-1"
disabled={showGenerating} disabled={showGenerating}
onClick={() => { onClick={() => {
send(inputMsg, true); send(inputMsg, true);
@@ -909,10 +911,8 @@ export default function ChatBOX(props: {
chatStore.whisper_key && chatStore.whisper_key &&
(chatStore.whisper_key || chatStore.apiKey) && ( (chatStore.whisper_key || chatStore.apiKey) && (
<button <button
className={`disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 ${ className={`btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1 ${
isRecording === "Recording" isRecording === "Recording" ? "btn-error" : "btn-success"
? "bg-red-400 hover:bg-red-600"
: "bg-cyan-400 hover:bg-cyan-600"
} ${isRecording !== "Mic" ? "animate-pulse" : ""}`} } ${isRecording !== "Mic" ? "animate-pulse" : ""}`}
disabled={isRecording === "Transcribing"} disabled={isRecording === "Transcribing"}
ref={mediaRef} ref={mediaRef}
@@ -1027,7 +1027,7 @@ export default function ChatBOX(props: {
)} )}
{chatStore.develop_mode && ( {chatStore.develop_mode && (
<button <button
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600" className="btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1"
disabled={showGenerating || !chatStore.apiKey} disabled={showGenerating || !chatStore.apiKey}
onClick={() => { onClick={() => {
chatStore.history.push({ chatStore.history.push({
@@ -1051,7 +1051,7 @@ export default function ChatBOX(props: {
)} )}
{chatStore.develop_mode && ( {chatStore.develop_mode && (
<button <button
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600" className="btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1"
disabled={showGenerating || !chatStore.apiKey} disabled={showGenerating || !chatStore.apiKey}
onClick={() => { onClick={() => {
send(inputMsg, false); send(inputMsg, false);
@@ -1062,7 +1062,7 @@ export default function ChatBOX(props: {
)} )}
{chatStore.develop_mode && ( {chatStore.develop_mode && (
<button <button
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600" className="btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1"
disabled={showGenerating || !chatStore.apiKey} disabled={showGenerating || !chatStore.apiKey}
onClick={() => { onClick={() => {
setShowAddToolMsg(true); setShowAddToolMsg(true);

View File

@@ -3,25 +3,28 @@ import { App } from "./app";
import { useState, useEffect } from "preact/hooks"; import { useState, useEffect } from "preact/hooks";
import { Tr, langCodeContext, LANG_OPTIONS } from "./translate"; import { Tr, langCodeContext, LANG_OPTIONS } from "./translate";
import { themeChange } from "theme-change";
function Base() { function Base() {
const [langCode, _setLangCode] = useState("en-US"); const [langCode, _setLangCode] = useState("en-US");
const setLangCode = (langCode: string) => { const setLangCode = (langCode: string) => {
_setLangCode(langCode) _setLangCode(langCode);
if (!localStorage) return if (!localStorage) return;
localStorage.setItem('chatgpt-api-web-lang', langCode) localStorage.setItem("chatgpt-api-web-lang", langCode);
} };
// select language // select language
useEffect(() => { useEffect(() => {
themeChange(false);
// query localStorage // query localStorage
if (localStorage) { if (localStorage) {
const lang = localStorage.getItem('chatgpt-api-web-lang') const lang = localStorage.getItem("chatgpt-api-web-lang");
if (lang) { if (lang) {
console.log(`query langCode ${lang} from localStorage`) console.log(`query langCode ${lang} from localStorage`);
_setLangCode(lang) _setLangCode(lang);
return return;
} }
} }

View File

@@ -16,6 +16,14 @@ import { SetAPIsTemplate } from "./setAPIsTemplate";
import { autoHeight } from "./textarea"; import { autoHeight } from "./textarea";
import getDefaultParams from "./getDefaultParam"; import getDefaultParams from "./getDefaultParam";
import {
InformationCircleIcon,
CheckIcon,
NoSymbolIcon,
} from "@heroicons/react/24/outline";
import { themeChange } from "theme-change";
const TTS_VOICES: string[] = [ const TTS_VOICES: string[] = [
"alloy", "alloy",
"echo", "echo",
@@ -29,14 +37,6 @@ const TTS_FORMAT: string[] = ["mp3", "opus", "aac", "flac"];
const Help = (props: { children: any; help: string }) => { const Help = (props: { children: any; help: string }) => {
return ( return (
<div> <div>
<button
className="absolute"
onClick={() => {
alert(props.help);
}}
>
</button>
<p className="flex justify-between">{props.children}</p> <p className="flex justify-between">{props.children}</p>
</div> </div>
); );
@@ -103,12 +103,25 @@ const LongInput = (props: {
chatStore: ChatStore; chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void; setChatStore: (cs: ChatStore) => void;
field: "systemMessageContent" | "toolsString"; field: "systemMessageContent" | "toolsString";
label: string;
help: string; help: string;
}) => { }) => {
return ( return (
<Help help={props.help}> <label class="form-control">
<div class="label">
<span class="label-text">{props.label}</span>
<span class="label-text-alt">
<button
onClick={() => {
alert(props.help);
}}
>
<InformationCircleIcon className="w-4 h-4" />
</button>
</span>
</div>
<textarea <textarea
className="m-2 p-2 border rounded focus w-full" className="textarea textarea-bordered h-24 w-full"
value={props.chatStore[props.field]} value={props.chatStore[props.field]}
onChange={(event: any) => { onChange={(event: any) => {
props.chatStore[props.field] = event.target.value; props.chatStore[props.field] = event.target.value;
@@ -119,7 +132,7 @@ const LongInput = (props: {
autoHeight(event.target); autoHeight(event.target);
}} }}
></textarea> ></textarea>
</Help> </label>
); );
}; };
@@ -142,7 +155,6 @@ const Input = (props: {
<Help help={props.help}> <Help help={props.help}>
<label className="m-2 p-2">{props.field}</label> <label className="m-2 p-2">{props.field}</label>
<button <button
className="p-2"
onClick={() => { onClick={() => {
setHideInput(!hideInput); setHideInput(!hideInput);
console.log("clicked", hideInput); console.log("clicked", hideInput);
@@ -343,6 +355,7 @@ export default (props: {
const { langCode, setLangCode } = useContext(langCodeContext); const { langCode, setLangCode } = useContext(langCodeContext);
useEffect(() => { useEffect(() => {
themeChange(false);
const handleKeyPress = (event: any) => { const handleKeyPress = (event: any) => {
if (event.keyCode === 27) { if (event.keyCode === 27) {
// keyCode for ESC key is 27 // keyCode for ESC key is 27
@@ -360,17 +373,133 @@ export default (props: {
return ( return (
<div <div
onClick={() => props.setShow(false)} 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" className="left-0 top-0 overflow-scroll flex justify-center absolute mt-6 w-screen h-full z-10"
> >
<div <div
onClick={(event: any) => { onClick={(event: any) => {
event.stopPropagation(); event.stopPropagation();
}} }}
className="m-2 p-2 bg-white rounded-lg h-fit lg:w-2/3 z-20" className="px-6 pt-2 pb-4 rounded-lg h-fit lg:w-2/3 z-20 shadow-2xl bg-base-200"
> >
<h3 className="text-xl text-center flex justify-between"> <div className="flex justify-between items-center">
<h3 className="text-xl">
<span>{Tr("Settings")}</span> <span>{Tr("Settings")}</span>
<select> </h3>
<button
className="btn"
onClick={() => {
props.setShow(false);
}}
>
{Tr("Close")}
</button>
</div>
<hr className="pt-2" />
<div role="tablist" class="tabs tabs-bordered pt-2 w-full">
<input
type="radio"
name="setting_tab"
role="tab"
class="tab"
aria-label="Session"
/>
<div
role="tabpanel"
class="tab-content bg-base-100 border-base-300 rounded-box p-6"
>
<p>
{Tr("Total cost in this session")} $
{props.chatStore.cost.toFixed(4)}
</p>
<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>
<input
type="radio"
name="setting_tab"
role="tab"
class="tab"
aria-label="System"
/>
<div
role="tabpanel"
class="tab-content bg-base-100 border-base-300 rounded-box p-6"
>
<div className="flex justify-between">
<p>
{Tr("Accumulated cost in all sessions")} ${totalCost.toFixed(4)}
</p>
<button
onClick={() => {
clearTotalCost();
setTotalCost(getTotalCost());
}}
>
{Tr("Reset")}
</button>
</div>
<label class="form-control w-full max-w-xs">
<div class="label">
<span class="label-text">Theme Switch</span>
</div>
<select data-choose-theme class="select select-bordered">
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="cyberpunk">Cyberpunk</option>
<option value="black">Black</option>
</select>
</label>
<label class="form-control w-full max-w-xs">
<div class="label">
<span class="label-text">Language</span>
</div>
<select class="select select-bordered">
{Object.keys(LANG_OPTIONS).map((opt) => ( {Object.keys(LANG_OPTIONS).map((opt) => (
<option <option
value={opt} value={opt}
@@ -384,11 +513,10 @@ export default (props: {
</option> </option>
))} ))}
</select> </select>
</h3> </label>
<hr /> <div class="join pt-2">
<div className="flex justify-between">
<button <button
className="p-2 m-2 rounded bg-purple-600 text-white" class="btn join-item"
onClick={() => { onClick={() => {
navigator.clipboard.writeText(link); navigator.clipboard.writeText(link);
alert(tr(`Copied link:`, langCode) + `${link}`); alert(tr(`Copied link:`, langCode) + `${link}`);
@@ -397,9 +525,11 @@ export default (props: {
{Tr("Copy Setting Link")} {Tr("Copy Setting Link")}
</button> </button>
<button <button
className="p-2 m-2 rounded bg-rose-600 text-white" class="btn join-item"
onClick={() => { onClick={() => {
if (!confirm(tr("Are you sure to clear all history?", langCode))) if (
!confirm(tr("Are you sure to clear all history?", langCode))
)
return; return;
props.chatStore.history = props.chatStore.history.filter( props.chatStore.history = props.chatStore.history.filter(
(msg) => msg.example && !msg.hide (msg) => msg.example && !msg.hide
@@ -411,33 +541,125 @@ export default (props: {
{Tr("Clear History")} {Tr("Clear History")}
</button> </button>
<button <button
className="p-2 m-2 rounded bg-cyan-600 text-white" class="btn join-item"
onClick={() => { onClick={() => {
props.setShow(false); 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("Close")} {Tr("Export")}
</button> </button>
<button
class="btn join-item"
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
class="btn join-item"
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);
}}
/>
</div> </div>
<p className="m-2 p-2"> </div>
{Tr("Total cost in this session")} ${props.chatStore.cost.toFixed(4)}
</p> <input
<hr /> type="radio"
<div className="box"> name="setting_tab"
<LongInput role="tab"
field="systemMessageContent" class="tab"
help="系统消息用于指示ChatGPT的角色和一些前置条件例如“你是一个有帮助的人工智能助理”或者“你是一个专业英语翻译把我的话全部翻译成英语”详情参考 OPEAN AI API 文档" aria-label="Chat"
{...props}
/>
<span>
Valied JSON:{" "}
{isVailedJSON(props.chatStore.toolsString) ? "🆗" : "❌"}
</span>
<LongInput
field="toolsString"
help="function call tools, should be valied json format in list"
{...props}
/> />
<div
role="tabpanel"
class="tab-content bg-base-100 border-base-300 rounded-box p-6"
>
<div className="relative border-slate-300 border rounded"> <div className="relative border-slate-300 border rounded">
<div className="flex justify-between"> <div className="flex justify-between">
<strong className="p-1 m-1">Chat API</strong> <strong className="p-1 m-1">Chat API</strong>
@@ -533,7 +755,19 @@ export default (props: {
readOnly={false} readOnly={false}
{...props} {...props}
/> />
</div>
<input
type="radio"
name="setting_tab"
role="tab"
class="tab"
aria-label="TTS"
/>
<div
role="tabpanel"
class="tab-content bg-base-100 border-base-300 rounded-box p-6"
>
<div className="relative border-slate-300 border rounded"> <div className="relative border-slate-300 border rounded">
<div className="flex justify-between"> <div className="flex justify-between">
<strong className="p-1 m-1">Whisper API</strong> <strong className="p-1 m-1">Whisper API</strong>
@@ -616,7 +850,18 @@ export default (props: {
))} ))}
</select> </select>
</Help> </Help>
</div>
<input
type="radio"
name="setting_tab"
role="tab"
class="tab"
aria-label="Image Gen"
/>
<div
role="tabpanel"
class="tab-content bg-base-100 border-base-300 rounded-box p-6"
>
<div className="relative border-slate-300 border rounded"> <div className="relative border-slate-300 border rounded">
<div className="flex justify-between"> <div className="flex justify-between">
<strong className="p-1 m-1">Image Gen API</strong> <strong className="p-1 m-1">Image Gen API</strong>
@@ -640,155 +885,15 @@ export default (props: {
{...props} {...props}
/> />
</div> </div>
<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> </div>
<div className="flex justify-evenly flex-wrap">
{props.chatStore.toolsString.trim() && (
<button
className="p-2 m-2 rounded bg-blue-300"
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>
<p className="flex justify-evenly"> <div className="pt-4 pb-2">
<button <p className="text-center">
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 = "";
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="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")}{" "} chatgpt-api-web ChatStore {Tr("Version")}{" "}
{props.chatStore.chatgpt_api_web_version} {props.chatStore.chatgpt_api_web_version}
</p> </p>
<p> <p className="text-center">
{Tr("Documents and source code are avaliable here")}:{" "} {Tr("Documents and source code are avaliable here")}:{" "}
<a <a
className="underline" className="underline"
href="https://github.com/heimoshuiyu/chatgpt-api-web" href="https://github.com/heimoshuiyu/chatgpt-api-web"
@@ -798,7 +903,6 @@ export default (props: {
</a> </a>
</p> </p>
</div> </div>
<hr />
</div> </div>
</div> </div>
); );

View File

@@ -1,8 +1,42 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
daisyui: {
themes: ["light",
"dark",
"cupcake",
"bumblebee",
"emerald",
"corporate",
"synthwave",
"retro",
"cyberpunk",
"valentine",
"halloween",
"garden",
"forest",
"aqua",
"lofi",
"pastel",
"fantasy",
"wireframe",
"black",
"luxury",
"dracula",
"cmyk",
"autumn",
"business",
"acid",
"lemonade",
"night",
"coffee",
"winter",
"dim",
"nord",
"sunset",],
},
theme: { theme: {
extend: {}, extend: {},
}, },
plugins: [], plugins: [require('daisyui')],
}; };

511
yarn.lock

File diff suppressed because it is too large Load Diff