import { createRef } from "preact"; import { useEffect, useState } from "preact/hooks"; import type { ChatStore } from "./app"; import ChatGPT, { ChunkMessage, FetchResponse } from "./chatgpt"; import Message from "./message"; import Settings from "./settings"; export default function ChatBOX(props: { chatStore: ChatStore; setChatStore: (cs: ChatStore) => void; }) { const { chatStore, setChatStore } = props; // prevent error if (chatStore === undefined) return
; const [inputMsg, setInputMsg] = useState(""); const [showGenerating, setShowGenerating] = useState(false); const [generatingMessage, setGeneratingMessage] = useState(""); const [showRetry, setShowRetry] = useState(false); const messagesEndRef = createRef(); useEffect(() => { console.log("ref", messagesEndRef); messagesEndRef.current.scrollIntoView({ behavior: "smooth" }); }, [showRetry, showGenerating, generatingMessage]); const client = new ChatGPT(chatStore.apiKey); const _completeWithStreamMode = async (response: Response) => { // call api, return reponse text console.log("response", response); const reader = response.body?.getReader(); const allChunkMessage: string[] = []; new ReadableStream({ async start() { 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); setShowGenerating(true); setGeneratingMessage(allChunkMessage.join("")); if (responseDone) break; } setShowGenerating(false); // 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 (response: Response) => { const data = (await response.json()) as FetchResponse; const content = client.processFetchResponse(data); chatStore.history.push({ role: "assistant", content }); 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); const response = await client._fetch(chatStore.streamMode); const contentType = response.headers.get("content-type"); if (contentType === "text/event-stream") { await _completeWithStreamMode(response); } else if (contentType === "application/json") { await _completeWithFetchMode(response); } else { throw `unknown response content type ${contentType}`; } // 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) { setShowRetry(true); alert(error); } finally { setShowGenerating(false); } }; // when user click the "send" button or ctrl+Enter in the textarea const send = async (msg = "") => { const inputMsg = msg; if (!inputMsg) { console.log("empty message"); return; } chatStore.history.push({ role: "user", content: inputMsg.trim() }); setChatStore({ ...chatStore }); setInputMsg(""); await complete(); }; const [showSettings, setShowSettings] = useState(false); return (setShowSettings(true)} >
请先在上方设置 (OPENAI) API KEY
)} {!chatStore.apiEndpoint && (请先在上方设置 API Endpoint
)} {chatStore.history.length === 0 && (暂无历史对话记录
)} {chatStore.history.map((_, messageIndex) => ({generatingMessage ? generatingMessage.split("\n").map((line) =>
{line}
) : "生成中,请保持网络稳定"} ... )} {showRetry && ()}