import { useEffect, useState } from "preact/hooks"; import "./global.css"; import ChatGPT, { Message, ChunkMessage } from "./chatgpt"; import { createRef } from "preact"; export interface ChatStore { systemMessageContent: string; history: Message[]; postBeginIndex: number; tokenMargin: number; totalTokens: number; maxTokens: number; apiKey: string; apiEndpoint: string; 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"; export const newChatStore = ( apiKey = "", systemMessageContent = "你是一个猫娘,你要模仿猫娘的语气说话", apiEndpoint = _defaultAPIEndpoint, streamMode = true ): ChatStore => { return { systemMessageContent: defaultSysMessage() || systemMessageContent, history: [], postBeginIndex: 0, tokenMargin: 1024, totalTokens: 0, maxTokens: 4096, apiKey: defaultAPIKEY() || apiKey, apiEndpoint: defaultAPIEndpoint() || apiEndpoint, streamMode: defauleMode() ?? streamMode, }; }; const STORAGE_NAME = "chatgpt-api-web"; const STORAGE_NAME_SELECTED = `${STORAGE_NAME}-selected`; export function App() { // init all chat store const initAllChatStore: ChatStore[] = JSON.parse( localStorage.getItem(STORAGE_NAME) || "[]" ); if (initAllChatStore.length === 0) { initAllChatStore.push(newChatStore()); localStorage.setItem(STORAGE_NAME, JSON.stringify(initAllChatStore)); } const [allChatStore, setAllChatStore] = useState(initAllChatStore); const [selectedChatIndex, setSelectedChatIndex] = useState( parseInt(localStorage.getItem(STORAGE_NAME_SELECTED) ?? "0") ); useEffect(() => { localStorage.setItem(STORAGE_NAME_SELECTED, `${selectedChatIndex}`); }, [selectedChatIndex]); const chatStore = allChatStore[selectedChatIndex]; const setChatStore = (cs: ChatStore) => { allChatStore[selectedChatIndex] = cs; setAllChatStore([...allChatStore]); }; useEffect(() => { console.log("saved", allChatStore); localStorage.setItem(STORAGE_NAME, JSON.stringify(allChatStore)); }, [allChatStore]); const [inputMsg, setInputMsg] = useState(""); const [showGenerating, setShowGenerating] = useState(false); const [generatingMessage, setGeneratingMessage] = useState(""); const client = new ChatGPT(chatStore.apiKey); const _completeWithStreamMode = async () => { // call api, return reponse text const response = await client.completeWithSteam(); console.log("response", response); const reader = response.body?.getReader(); const allChunkMessage: string[] = []; await new ReadableStream({ async start(controller) { while (true) { let responseDone = false; let state = await reader?.read(); let done = state?.done; let value = state?.value; if (done) break; let text = new TextDecoder().decode(value); // console.log("text:", text); const lines = text .trim() .split("\n") .map((line) => line.trim()) .filter((i) => { if (!i) return false; if (i === "data: [DONE]") { responseDone = true; return false; } return true; }); console.log("lines", lines); const jsons: ChunkMessage[] = lines .map((line) => { return JSON.parse(line.trim().slice("data: ".length)); }) .filter((i) => i); // console.log("jsons", jsons); const chunkText = jsons .map((j) => j.choices[0].delta.content ?? "") .join(""); // console.log("chunk text", chunkText); allChunkMessage.push(chunkText); setGeneratingMessage(allChunkMessage.join("")); if (responseDone) break; } // console.log("push to history", allChunkMessage); chatStore.history.push({ role: "assistant", content: allChunkMessage.join(""), }); // manually copy status from client to chatStore chatStore.maxTokens = client.max_tokens; chatStore.tokenMargin = client.tokens_margin; chatStore.totalTokens = client.total_tokens + 39 + client.calculate_token_length(allChunkMessage.join("")); setChatStore({ ...chatStore }); setGeneratingMessage(""); setShowGenerating(false); }, }); }; const _completeWithFetchMode = async () => { // call api, return reponse text const response = await client.complete(); chatStore.history.push({ role: "assistant", content: response }); setShowGenerating(false); }; // wrap the actuall complete api const complete = async () => { // manually copy status from chatStore to client client.apiEndpoint = chatStore.apiEndpoint; client.sysMessageContent = chatStore.systemMessageContent; client.messages = chatStore.history.slice(chatStore.postBeginIndex); try { setShowGenerating(true); if (chatStore.streamMode) { await _completeWithStreamMode(); } else { await _completeWithFetchMode(); } // manually copy status from client to chatStore chatStore.maxTokens = client.max_tokens; chatStore.tokenMargin = client.tokens_margin; chatStore.totalTokens = client.total_tokens; // when total token > max token - margin token: // ChatGPT will "forgot" some historical message // so client.message.length will be less than chatStore.history.length chatStore.postBeginIndex = chatStore.history.length - client.messages.length; console.log("postBeginIndex", chatStore.postBeginIndex); setChatStore({ ...chatStore }); } catch (error) { alert(error); } }; // when user click the "send" button or ctrl+Enter in the textarea const send = async () => { if (!inputMsg) { console.log("empty message"); return; } chatStore.history.push({ role: "user", content: inputMsg.trim() }); setChatStore({ ...chatStore }); setInputMsg(""); await complete(); setChatStore({ ...chatStore }); }; // change api key const changAPIKEY = () => { const newAPIKEY = prompt(`Current API KEY: ${chatStore.apiKey}`); if (!newAPIKEY) return; chatStore.apiKey = newAPIKEY; setChatStore({ ...chatStore }); }; return (
喵喵,请先在上方设置 (OPENAI) API KEY
)} {!chatStore.apiEndpoint && (喵喵,请先在上方设置 API Endpoint
)} {chatStore.history.length === 0 && (这里什么都没有哦 QwQ
)} {chatStore.history.map((chat, i) => { const pClassName = chat.role === "assistant" ? "p-2 rounded relative bg-white my-2 text-left" : "p-2 rounded relative bg-green-400 my-2 text-right"; const iconClassName = chat.role === "user" ? "absolute bottom-0 left-0" : "absolute bottom-0 right-0"; const DeleteIcon = () => ( ); return ({chat.content .split("\n") .filter((line) => line) .map((line) => (
{line}
))}{generatingMessage ? generatingMessage.split("\n").map((line) =>
{line}
) : "生成中,保持网络稳定喵"} ... )}