diff --git a/src/app.tsx b/src/app.tsx index f75d866..1e10ba4 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "preact/hooks"; import "./global.css"; -import ChatGPT, { Message } from "./chatgpt"; +import ChatGPT, { Message, ChunkMessage } from "./chatgpt"; import { createRef } from "preact"; export interface ChatStore { @@ -79,6 +79,7 @@ export function App() { const [inputMsg, setInputMsg] = useState(""); const [showGenerating, setShowGenerating] = useState(false); + const [generatingMessage, setGeneratingMessage] = useState(""); const client = new ChatGPT(chatStore.apiKey); @@ -89,8 +90,65 @@ export function App() { client.messages = chatStore.history.slice(chatStore.postBeginIndex); // call api, return reponse text - const response = await client.complete(); - chatStore.history.push({ role: "assistant", content: response }); + 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); + }, + }); // manually copy status from client to chatStore chatStore.maxTokens = client.max_tokens; @@ -112,8 +170,6 @@ export function App() { await _complete(); } catch (error) { alert(error); - } finally { - setShowGenerating(false); } }; @@ -304,7 +360,10 @@ export function App() { ); })} {showGenerating && ( -

Generating... please wait...

+

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

)}
diff --git a/src/chatgpt.ts b/src/chatgpt.ts index 788711d..6b2a18e 100644 --- a/src/chatgpt.ts +++ b/src/chatgpt.ts @@ -3,6 +3,12 @@ export interface Message { content: string; } +export interface ChunkMessage { + choices: { + delta: { role: "assitant" | undefined; content: string | undefined }; + }[]; +} + class Chat { OPENAI_API_KEY: string; messages: Message[]; @@ -33,6 +39,24 @@ class Chat { this.apiEndpoint = apiEndPoint; } + _fetch() { + return fetch(this.apiEndpoint, { + method: "POST", + headers: { + Authorization: `Bearer ${this.OPENAI_API_KEY}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + model: "gpt-3.5-turbo", + messages: [ + { role: "system", content: this.sysMessageContent }, + ...this.messages, + ], + stream: true, + }), + }); + } + async fetch(): Promise<{ id: string; object: string; @@ -49,21 +73,8 @@ class Chat { index: number | undefined; }[]; }> { - const resp = await fetch(this.apiEndpoint, { - method: "POST", - headers: { - Authorization: `Bearer ${this.OPENAI_API_KEY}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ - model: "gpt-3.5-turbo", - messages: [ - { role: "system", content: this.sysMessageContent }, - ...this.messages, - ], - }), - }).then((resp) => resp.json()); - return resp; + const resp = await this._fetch(); + return await resp.json(); } async say(content: string): Promise { @@ -90,6 +101,14 @@ class Chat { ); } + completeWithSteam() { + this.total_tokens = + this.messages + .map((msg) => this.calculate_token_length(msg.content) + 20) + .reduce((a, v) => a + v); + return this._fetch(); + } + // https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them calculate_token_length(content: string): number { const totalCount = content.length;