settings window
This commit is contained in:
103
src/app.tsx
103
src/app.tsx
@@ -3,6 +3,8 @@ import "./global.css";
|
|||||||
|
|
||||||
import ChatGPT, { Message, ChunkMessage } from "./chatgpt";
|
import ChatGPT, { Message, ChunkMessage } from "./chatgpt";
|
||||||
import { createRef } from "preact";
|
import { createRef } from "preact";
|
||||||
|
import Settings from "./settings";
|
||||||
|
import getDefaultParams from "./getDefaultParam";
|
||||||
|
|
||||||
export interface ChatStore {
|
export interface ChatStore {
|
||||||
systemMessageContent: string;
|
systemMessageContent: string;
|
||||||
@@ -16,49 +18,23 @@ export interface ChatStore {
|
|||||||
streamMode: boolean;
|
streamMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultAPIKEY = () => {
|
|
||||||
const queryParameters = new URLSearchParams(window.location.search);
|
|
||||||
const key = queryParameters.get("key");
|
|
||||||
return key;
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultSysMessage = () => {
|
|
||||||
const queryParameters = new URLSearchParams(window.location.search);
|
|
||||||
const sys = queryParameters.get("sys");
|
|
||||||
return sys;
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultAPIEndpoint = () => {
|
|
||||||
const queryParameters = new URLSearchParams(window.location.search);
|
|
||||||
const sys = queryParameters.get("api");
|
|
||||||
return sys;
|
|
||||||
};
|
|
||||||
|
|
||||||
const defauleMode = () => {
|
|
||||||
const queryParameters = new URLSearchParams(window.location.search);
|
|
||||||
const sys = queryParameters.get("mode");
|
|
||||||
if (sys === "stream") return true;
|
|
||||||
if (sys === "fetch") return false;
|
|
||||||
return undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const _defaultAPIEndpoint = "https://api.openai.com/v1/chat/completions";
|
const _defaultAPIEndpoint = "https://api.openai.com/v1/chat/completions";
|
||||||
export const newChatStore = (
|
const newChatStore = (
|
||||||
apiKey = "",
|
apiKey = "",
|
||||||
systemMessageContent = "你是一个猫娘,你要模仿猫娘的语气说话",
|
systemMessageContent = "你是一个猫娘,你要模仿猫娘的语气说话",
|
||||||
apiEndpoint = _defaultAPIEndpoint,
|
apiEndpoint = _defaultAPIEndpoint,
|
||||||
streamMode = true
|
streamMode = true
|
||||||
): ChatStore => {
|
): ChatStore => {
|
||||||
return {
|
return {
|
||||||
systemMessageContent: defaultSysMessage() || systemMessageContent,
|
systemMessageContent: getDefaultParams("sys", systemMessageContent),
|
||||||
history: [],
|
history: [],
|
||||||
postBeginIndex: 0,
|
postBeginIndex: 0,
|
||||||
tokenMargin: 1024,
|
tokenMargin: 1024,
|
||||||
totalTokens: 0,
|
totalTokens: 0,
|
||||||
maxTokens: 4096,
|
maxTokens: 4096,
|
||||||
apiKey: defaultAPIKEY() || apiKey,
|
apiKey: getDefaultParams("key", apiKey),
|
||||||
apiEndpoint: defaultAPIEndpoint() || apiEndpoint,
|
apiEndpoint: getDefaultParams("api", apiEndpoint),
|
||||||
streamMode: defauleMode() ?? streamMode,
|
streamMode: getDefaultParams("mode", streamMode),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -213,16 +189,16 @@ export function App() {
|
|||||||
setChatStore({ ...chatStore });
|
setChatStore({ ...chatStore });
|
||||||
};
|
};
|
||||||
|
|
||||||
// change api key
|
const [showSettings, setShowSettings] = useState(false);
|
||||||
const changAPIKEY = () => {
|
|
||||||
const newAPIKEY = prompt(`Current API KEY: ${chatStore.apiKey}`);
|
|
||||||
if (!newAPIKEY) return;
|
|
||||||
chatStore.apiKey = newAPIKEY;
|
|
||||||
setChatStore({ ...chatStore });
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex text-sm h-screen bg-slate-200">
|
<div className="flex text-sm h-screen bg-slate-200">
|
||||||
|
<Settings
|
||||||
|
chatStore={chatStore}
|
||||||
|
setChatStore={setChatStore}
|
||||||
|
show={showSettings}
|
||||||
|
setShow={setShowSettings}
|
||||||
|
/>
|
||||||
<div className="flex flex-col h-full p-4 border-r-indigo-500 border-2">
|
<div className="flex flex-col h-full p-4 border-r-indigo-500 border-2">
|
||||||
<div className="grow overflow-scroll">
|
<div className="grow overflow-scroll">
|
||||||
<button
|
<button
|
||||||
@@ -273,10 +249,10 @@ export function App() {
|
|||||||
if (allChatStore.length === 0) {
|
if (allChatStore.length === 0) {
|
||||||
allChatStore.push(
|
allChatStore.push(
|
||||||
newChatStore(
|
newChatStore(
|
||||||
defaultAPIKEY() || oldAPIkey,
|
getDefaultParams("api", oldAPIkey),
|
||||||
defaultSysMessage() || oldSystemMessageContent,
|
getDefaultParams("sys", oldSystemMessageContent),
|
||||||
defaultAPIEndpoint() || oldAPIEndpoint,
|
getDefaultParams("api", oldAPIEndpoint),
|
||||||
defauleMode() || oldMode
|
getDefaultParams("mode", oldMode)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
setSelectedChatIndex(0);
|
setSelectedChatIndex(0);
|
||||||
@@ -290,49 +266,16 @@ export function App() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="grow flex flex-col p-4">
|
<div className="grow flex flex-col p-4">
|
||||||
<p>
|
<p className="cursor-pointer" onClick={() => setShowSettings(true)}>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button className="underline">
|
||||||
className="underline"
|
|
||||||
onClick={() => {
|
|
||||||
const newSysMsgContent = prompt(
|
|
||||||
"Change system message content"
|
|
||||||
);
|
|
||||||
if (newSysMsgContent === null) return;
|
|
||||||
chatStore.systemMessageContent = newSysMsgContent;
|
|
||||||
setChatStore({ ...chatStore });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{chatStore.systemMessageContent.length > 13
|
{chatStore.systemMessageContent.length > 13
|
||||||
? chatStore.systemMessageContent.slice(0, 10) + "..."
|
? chatStore.systemMessageContent.slice(0, 10) + "..."
|
||||||
: chatStore.systemMessageContent}
|
: chatStore.systemMessageContent}
|
||||||
</button>{" "}
|
</button>{" "}
|
||||||
<button className="underline" onClick={changAPIKEY}>
|
<button className="underline">KEY</button>{" "}
|
||||||
KEY
|
<button className="underline">ENDPOINT</button>{" "}
|
||||||
</button>{" "}
|
<button className="underline">
|
||||||
<button
|
|
||||||
className="underline"
|
|
||||||
onClick={() => {
|
|
||||||
const newEndpoint = prompt(
|
|
||||||
`Enter new API endpoint\n(current: ${chatStore.apiEndpoint})\n(default: ${_defaultAPIEndpoint})`
|
|
||||||
);
|
|
||||||
if (!newEndpoint) return;
|
|
||||||
chatStore.apiEndpoint = newEndpoint;
|
|
||||||
setChatStore({ ...chatStore });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
ENDPOINT
|
|
||||||
</button>{" "}
|
|
||||||
<button
|
|
||||||
className="underline"
|
|
||||||
onClick={() => {
|
|
||||||
const choice = confirm(
|
|
||||||
"FETCH 模式单次请求一段完整的对话文本,tokens 数量是准确的。\nSTREAM 模式下可以动态看到生成过程,但只能估算 tokens 数量,token 过多时可能裁剪过多或过少历史消息。\n需要使用 STREAM 模式吗?"
|
|
||||||
);
|
|
||||||
chatStore.streamMode = !!choice;
|
|
||||||
setChatStore({ ...chatStore });
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{chatStore.streamMode ? "STREAM" : "FETCH"}
|
{chatStore.streamMode ? "STREAM" : "FETCH"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
18
src/getDefaultParam.ts
Normal file
18
src/getDefaultParam.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
function getDefaultParams(param: string, val: string): string;
|
||||||
|
function getDefaultParams(param: string, val: number): number;
|
||||||
|
function getDefaultParams(param: string, val: boolean): boolean;
|
||||||
|
function getDefaultParams(param: any, val: any) {
|
||||||
|
const queryParameters = new URLSearchParams(window.location.search);
|
||||||
|
const get = queryParameters.get(param);
|
||||||
|
if (typeof val === "string") {
|
||||||
|
return get ?? val;
|
||||||
|
} else if (typeof val === "number") {
|
||||||
|
return parseInt(get ?? `${val}`);
|
||||||
|
} else if (typeof val === "boolean") {
|
||||||
|
if (get === "stream") return true;
|
||||||
|
if (get === "fetch") return false;
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default getDefaultParams;
|
||||||
156
src/settings.tsx
Normal file
156
src/settings.tsx
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import { StateUpdater } from "preact/hooks";
|
||||||
|
import { ChatStore } from "./app";
|
||||||
|
|
||||||
|
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 Input = (props: {
|
||||||
|
chatStore: ChatStore;
|
||||||
|
setChatStore: (cs: ChatStore) => void;
|
||||||
|
field: "apiKey" | "systemMessageContent" | "apiEndpoint";
|
||||||
|
help: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Help help={props.help}>
|
||||||
|
<label className="m-2 p-2">{props.field}</label>
|
||||||
|
<input
|
||||||
|
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 Number = (props: {
|
||||||
|
chatStore: ChatStore;
|
||||||
|
setChatStore: (cs: ChatStore) => void;
|
||||||
|
field: "totalTokens" | "maxTokens" | "tokenMargin" | "postBeginIndex";
|
||||||
|
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 = parseInt(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";
|
||||||
|
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;
|
||||||
|
show: boolean;
|
||||||
|
setShow: StateUpdater<boolean>;
|
||||||
|
}) => {
|
||||||
|
if (!props.show) return <div></div>;
|
||||||
|
return (
|
||||||
|
<div className="overflow-scroll flex justify-center absolute w-screen h-screen bg-black bg-opacity-50 z-10">
|
||||||
|
<div className="m-2 p-2 bg-white rounded-lg h-fit">
|
||||||
|
<h3 className="text-xl">Settings</h3>
|
||||||
|
<hr />
|
||||||
|
<div className="box">
|
||||||
|
<Input
|
||||||
|
field="systemMessageContent"
|
||||||
|
help="系统消息,用于指示ChatGPT的角色和一些前置条件"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<Input field="apiKey" help="OPEN AI API 密钥" {...props} />
|
||||||
|
<Input
|
||||||
|
field="apiEndpoint"
|
||||||
|
help="API 端点,方便在不支持的地区使用反向代理服务"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<Choice
|
||||||
|
field="streamMode"
|
||||||
|
help="流模式,使用 stream mode 将可以动态看到生成内容,但无法准确计算 token 数量,在 token 数量过多时可能会裁切过多或过少历史消息"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<Number
|
||||||
|
field="maxTokens"
|
||||||
|
help="最大 token 数量,这个详情参考 OPENAI API 文档"
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<hr />
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<button
|
||||||
|
className="p-2 m-2 rounded bg-cyan-600 text-white"
|
||||||
|
onClick={() => {
|
||||||
|
props.setShow(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user