diff --git a/src/app.tsx b/src/app.tsx index 9f7b7a5..d436bda 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,10 +1,9 @@ import { useEffect, useState } from "preact/hooks"; import "./global.css"; -import ChatGPT, { Message, ChunkMessage } from "./chatgpt"; -import { createRef } from "preact"; -import Settings from "./settings"; +import { Message } from "./chatgpt"; import getDefaultParams from "./getDefaultParam"; +import ChatBOX from "./chatbox"; export interface ChatStore { systemMessageContent: string; @@ -70,135 +69,8 @@ export function App() { 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 (msg = "") => { - const inputMsg = msg; - if (!inputMsg) { - console.log("empty message"); - return; - } - chatStore.history.push({ role: "user", content: inputMsg.trim() }); - setChatStore({ ...chatStore }); - setInputMsg(""); - await complete(); - setChatStore({ ...chatStore }); - }; - - const [showSettings, setShowSettings] = useState(false); - return (
-
-
-

setShowSettings(true)}> -

- {" "} - -
-
- Total: {chatStore.totalTokens}{" "} - Max: {chatStore.maxTokens}{" "} - Margin: {chatStore.tokenMargin}{" "} - - Message: {chatStore.history.length - chatStore.postBeginIndex} - {" "} - Cut: {chatStore.postBeginIndex} -
-

-
- {!chatStore.apiKey && ( -

- 喵喵,请先在上方设置 (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}

- ))} - -

- ); - })} - {showGenerating && ( -

- {generatingMessage - ? generatingMessage.split("\n").map((line) =>

{line}

) - : "生成中,保持网络稳定喵"} - ... -

- )} -
-
- - -
-
+
); } diff --git a/src/chatbox.tsx b/src/chatbox.tsx new file mode 100644 index 0000000..1d4a4e3 --- /dev/null +++ b/src/chatbox.tsx @@ -0,0 +1,258 @@ +import { useState } from "preact/hooks"; +import type { ChatStore } from "./app"; +import ChatGPT, { ChunkMessage } from "./chatgpt"; +import Settings from "./settings"; + +export default function ChatBOX(props: { + chatStore: ChatStore; + setChatStore: (cs: ChatStore) => void; +}) { + const { chatStore, setChatStore } = props; + 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 (msg = "") => { + const inputMsg = msg; + if (!inputMsg) { + console.log("empty message"); + return; + } + chatStore.history.push({ role: "user", content: inputMsg.trim() }); + setChatStore({ ...chatStore }); + setInputMsg(""); + await complete(); + setChatStore({ ...chatStore }); + }; + + const [showSettings, setShowSettings] = useState(false); + return ( +
+ +

setShowSettings(true)}> +

+ {" "} + +
+
+ Total: {chatStore.totalTokens}{" "} + Max: {chatStore.maxTokens}{" "} + Margin: {chatStore.tokenMargin}{" "} + + Message: {chatStore.history.length - chatStore.postBeginIndex} + {" "} + Cut: {chatStore.postBeginIndex} +
+

+
+ {!chatStore.apiKey && ( +

+ 喵喵,请先在上方设置 (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}

+ ))} + +

+ ); + })} + {showGenerating && ( +

+ {generatingMessage + ? generatingMessage.split("\n").map((line) =>

{line}

) + : "生成中,保持网络稳定喵"} + ... +

+ )} +
+
+ + +
+
+ ); +}