24 Commits

Author SHA1 Message Date
e1ef16015d markdown 2023-03-25 16:59:05 +08:00
6b4cbd62ac auto textarea height 2023-03-25 12:38:48 +08:00
61c4f548f1 fit message width 2023-03-25 02:08:25 +08:00
bb84045481 scroll to bottom of messages generating messages 2023-03-25 01:02:38 +08:00
7a15792c39 scroll to bottom of messages 2023-03-25 01:01:25 +08:00
2ab9a0a46c show correct message.length 2023-03-24 15:51:28 +08:00
99d3c69647 use stream mode depend on content-type 2023-03-24 13:14:34 +08:00
0148465e34 select last chat after delete 2023-03-24 10:51:19 +08:00
9b00cd0fbc Love from ChatGPT: How to make <p> show all space like <pre> 2023-03-22 20:28:30 +08:00
7149ed0310 更好的文档 2023-03-22 15:50:03 +08:00
46b842500d 更好的设置说明 2023-03-22 15:43:53 +08:00
f278e5b1fc split message.tsx 2023-03-22 15:39:25 +08:00
c7f731c2fa change p-4 to p-2 2023-03-22 15:30:02 +08:00
371adfb1d4 retry button 2023-03-22 14:42:27 +08:00
2ed7f9d05a 爱来自chatgpt: how to hide scrollbar in tailwind css? 2023-03-19 14:26:51 +08:00
e0b50ced12 change url parms order 2023-03-18 02:27:19 +08:00
80508f9c6c fix stream mode showGeneratingText 2023-03-17 10:53:27 +08:00
14457cbb5f fix chatstore state 2023-03-17 10:42:16 +08:00
f59b63884d support dark mode 2023-03-17 10:31:25 +08:00
9b5730760a clear history reset postBeginIndex totalTokens 2023-03-17 09:31:07 +08:00
1372121e32 fix delete and save new chat 2023-03-17 01:16:21 +08:00
09e9d18e71 fix key link 2023-03-17 00:41:53 +08:00
383d6de1d6 你正常点 2023-03-17 00:29:13 +08:00
e56389b4c2 git ignore /dist.zip 2023-03-17 00:29:13 +08:00
10 changed files with 244 additions and 116 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
/dist.zip
# Logs # Logs
logs logs
*.log *.log

View File

@@ -10,9 +10,10 @@
与官方 ChatGPT 相比: 与官方 ChatGPT 相比:
- 对话记录使用浏览器的 localStorage 保存在本地 - API 调用速度更快更稳定
- 对话记录、API 密钥等使用浏览器的 localStorage 保存在本地
- 可删除对话消息 - 可删除对话消息
- 可以设置 system message (如:"你是一个喵娘",参见官方 [API 文档](https://platform.openai.com/docs/guides/chat)) - 可以设置 system message (如:"你是一个猫娘" 或 "你是一个有用的助理" 或 "将我的话翻译成英语",参见官方 [API 文档](https://platform.openai.com/docs/guides/chat))
- 可以为不同对话设置不同 APIKEY - 可以为不同对话设置不同 APIKEY
- 小(整个网页 30k 左右) - 小(整个网页 30k 左右)
- 可以设置不同的 API Endpoint方便墙内人士使用反向代理转发 API 请求) - 可以设置不同的 API Endpoint方便墙内人士使用反向代理转发 API 请求)
@@ -21,8 +22,6 @@
![screenshot](./screenshot.webp) ![screenshot](./screenshot.webp)
~~让喵娘统治世界吧((发病.webp~~
## 使用 ## 使用
以下任意方式都可: 以下任意方式都可:
@@ -38,7 +37,7 @@
- `api`: API Endpoint 默认为 `https://api.openai.com/v1/chat/completions` - `api`: API Endpoint 默认为 `https://api.openai.com/v1/chat/completions`
- `mode`: `fetch``stream` 模式stream 模式下可以动态看到 api 返回的数据,但无法得知 token 数量,只能进行估算,在 token 数量过多时可能会裁切过多或过少历史消息 - `mode`: `fetch``stream` 模式stream 模式下可以动态看到 api 返回的数据,但无法得知 token 数量,只能进行估算,在 token 数量过多时可能会裁切过多或过少历史消息
例如 `http://localhost:1234/?key=xxxx` 那么新创建的会话将会使用该默认 API 例如 `http://localhost:1234/?key=xxxx&api=xxxx` 那么 **新创建** 的会话将会使用该默认 API 和 API Endpoint
以上参数应用于单个对话,随时可在顶部更改 以上参数应用于单个对话,随时可在顶部更改

View File

@@ -12,6 +12,7 @@
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"postcss": "^8.4.21", "postcss": "^8.4.21",
"preact": "^10.11.3", "preact": "^10.11.3",
"preact-markdown": "^2.1.0",
"sakura.css": "^1.4.1", "sakura.css": "^1.4.1",
"tailwindcss": "^3.2.7" "tailwindcss": "^3.2.7"
}, },

View File

@@ -20,7 +20,7 @@ export interface ChatStore {
const _defaultAPIEndpoint = "https://api.openai.com/v1/chat/completions"; const _defaultAPIEndpoint = "https://api.openai.com/v1/chat/completions";
const newChatStore = ( const newChatStore = (
apiKey = "", apiKey = "",
systemMessageContent = "你是一个猫娘,你要模仿猫娘的语气说话", systemMessageContent = "你是一个有用的人工智能助理",
apiEndpoint = _defaultAPIEndpoint, apiEndpoint = _defaultAPIEndpoint,
streamMode = true streamMode = true
): ChatStore => { ): ChatStore => {
@@ -74,24 +74,31 @@ export function App() {
return JSON.parse(val) as ChatStore; return JSON.parse(val) as ChatStore;
}; };
const chatStore = getChatStoreByIndex(selectedChatIndex); const [chatStore, _setChatStore] = useState(
getChatStoreByIndex(selectedChatIndex)
);
const setChatStore = (cs: ChatStore) => { const setChatStore = (cs: ChatStore) => {
console.log("saved chat", selectedChatIndex, chatStore); console.log("saved chat", selectedChatIndex, chatStore);
localStorage.setItem( localStorage.setItem(
`${STORAGE_NAME}-${selectedChatIndex}`, `${STORAGE_NAME}-${selectedChatIndex}`,
JSON.stringify(chatStore) JSON.stringify(cs)
); );
_setChatStore(cs);
}; };
useEffect(() => {
_setChatStore(getChatStoreByIndex(selectedChatIndex));
}, [selectedChatIndex]);
return ( return (
<div className="flex text-sm h-screen bg-slate-200"> <div className="flex text-sm h-screen bg-slate-200 dark:bg-slate-800 dark:text-white">
<div className="flex flex-col h-full p-4 border-r-indigo-500 border-2"> <div className="flex flex-col h-full p-2 border-r-indigo-500 border-2 dark:border-slate-800 dark:border-r-indigo-500 dark:text-black">
<div className="grow overflow-scroll"> <div className="grow overflow-scroll">
<button <button
className="bg-violet-300 p-1 rounded hover:bg-violet-400" className="bg-violet-300 p-1 rounded hover:bg-violet-400"
onClick={() => { onClick={() => {
const max = Math.max(...allChatStoreIndexes); const max = Math.max(...allChatStoreIndexes);
const next = max + 1; const next = max + 1;
console.log("save next chat", next);
localStorage.setItem( localStorage.setItem(
`${STORAGE_NAME}-${next}`, `${STORAGE_NAME}-${next}`,
JSON.stringify( JSON.stringify(
@@ -138,6 +145,7 @@ export function App() {
onClick={() => { onClick={() => {
if (!confirm("Are you sure you want to delete this chat history?")) if (!confirm("Are you sure you want to delete this chat history?"))
return; return;
console.log("remove item", `${STORAGE_NAME}-${selectedChatIndex}`);
localStorage.removeItem(`${STORAGE_NAME}-${selectedChatIndex}`); localStorage.removeItem(`${STORAGE_NAME}-${selectedChatIndex}`);
const newAllChatStoreIndexes = [ const newAllChatStoreIndexes = [
...allChatStoreIndexes.filter((v) => v !== selectedChatIndex), ...allChatStoreIndexes.filter((v) => v !== selectedChatIndex),
@@ -145,7 +153,6 @@ export function App() {
if (newAllChatStoreIndexes.length === 0) { if (newAllChatStoreIndexes.length === 0) {
newAllChatStoreIndexes.push(0); newAllChatStoreIndexes.push(0);
}
setChatStore( setChatStore(
newChatStore( newChatStore(
chatStore.apiKey, chatStore.apiKey,
@@ -154,9 +161,11 @@ export function App() {
chatStore.streamMode chatStore.streamMode
) )
); );
}
// find nex selected chat index // find nex selected chat index
const next = newAllChatStoreIndexes[0]; const next =
newAllChatStoreIndexes[newAllChatStoreIndexes.length - 1];
console.log("next is", next); console.log("next is", next);
setSelectedChatIndex(next); setSelectedChatIndex(next);

View File

@@ -1,6 +1,8 @@
import { useState } from "preact/hooks"; import { createRef } from "preact";
import { useEffect, useState } from "preact/hooks";
import type { ChatStore } from "./app"; import type { ChatStore } from "./app";
import ChatGPT, { ChunkMessage } from "./chatgpt"; import ChatGPT, { ChunkMessage, FetchResponse } from "./chatgpt";
import Message from "./message";
import Settings from "./settings"; import Settings from "./settings";
export default function ChatBOX(props: { export default function ChatBOX(props: {
@@ -13,17 +15,23 @@ export default function ChatBOX(props: {
const [inputMsg, setInputMsg] = useState(""); const [inputMsg, setInputMsg] = useState("");
const [showGenerating, setShowGenerating] = useState(false); const [showGenerating, setShowGenerating] = useState(false);
const [generatingMessage, setGeneratingMessage] = useState(""); 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 client = new ChatGPT(chatStore.apiKey);
const _completeWithStreamMode = async () => { const _completeWithStreamMode = async (response: Response) => {
// call api, return reponse text // call api, return reponse text
const response = await client.completeWithSteam();
console.log("response", response); console.log("response", response);
const reader = response.body?.getReader(); const reader = response.body?.getReader();
const allChunkMessage: string[] = []; const allChunkMessage: string[] = [];
await new ReadableStream({ new ReadableStream({
async start(controller) { async start() {
while (true) { while (true) {
let responseDone = false; let responseDone = false;
let state = await reader?.read(); let state = await reader?.read();
@@ -56,9 +64,11 @@ export default function ChatBOX(props: {
.join(""); .join("");
// console.log("chunk text", chunkText); // console.log("chunk text", chunkText);
allChunkMessage.push(chunkText); allChunkMessage.push(chunkText);
setShowGenerating(true);
setGeneratingMessage(allChunkMessage.join("")); setGeneratingMessage(allChunkMessage.join(""));
if (responseDone) break; if (responseDone) break;
} }
setShowGenerating(false);
// console.log("push to history", allChunkMessage); // console.log("push to history", allChunkMessage);
chatStore.history.push({ chatStore.history.push({
@@ -79,10 +89,10 @@ export default function ChatBOX(props: {
}); });
}; };
const _completeWithFetchMode = async () => { const _completeWithFetchMode = async (response: Response) => {
// call api, return reponse text const data = (await response.json()) as FetchResponse;
const response = await client.complete(); const content = client.processFetchResponse(data);
chatStore.history.push({ role: "assistant", content: response }); chatStore.history.push({ role: "assistant", content });
setShowGenerating(false); setShowGenerating(false);
}; };
@@ -94,10 +104,14 @@ export default function ChatBOX(props: {
client.messages = chatStore.history.slice(chatStore.postBeginIndex); client.messages = chatStore.history.slice(chatStore.postBeginIndex);
try { try {
setShowGenerating(true); setShowGenerating(true);
if (chatStore.streamMode) { const response = await client._fetch(chatStore.streamMode);
await _completeWithStreamMode(); const contentType = response.headers.get("content-type");
if (contentType === "text/event-stream") {
await _completeWithStreamMode(response);
} else if (contentType === "application/json") {
await _completeWithFetchMode(response);
} else { } else {
await _completeWithFetchMode(); throw `unknown response content type ${contentType}`;
} }
// manually copy status from client to chatStore // manually copy status from client to chatStore
chatStore.maxTokens = client.max_tokens; chatStore.maxTokens = client.max_tokens;
@@ -111,6 +125,7 @@ export default function ChatBOX(props: {
console.log("postBeginIndex", chatStore.postBeginIndex); console.log("postBeginIndex", chatStore.postBeginIndex);
setChatStore({ ...chatStore }); setChatStore({ ...chatStore });
} catch (error) { } catch (error) {
setShowRetry(true);
alert(error); alert(error);
} finally { } finally {
setShowGenerating(false); setShowGenerating(false);
@@ -128,19 +143,21 @@ export default function ChatBOX(props: {
setChatStore({ ...chatStore }); setChatStore({ ...chatStore });
setInputMsg(""); setInputMsg("");
await complete(); await complete();
setChatStore({ ...chatStore });
}; };
const [showSettings, setShowSettings] = useState(false); const [showSettings, setShowSettings] = useState(false);
return ( return (
<div className="grow flex flex-col p-4"> <div className="grow flex flex-col p-2 dark:text-black">
<Settings <Settings
chatStore={chatStore} chatStore={chatStore}
setChatStore={setChatStore} setChatStore={setChatStore}
show={showSettings} show={showSettings}
setShow={setShowSettings} setShow={setShowSettings}
/> />
<p className="cursor-pointer" onClick={() => setShowSettings(true)}> <p
className="cursor-pointer dark:text-white"
onClick={() => setShowSettings(true)}
>
<div> <div>
<button className="underline"> <button className="underline">
{chatStore.systemMessageContent.length > 16 {chatStore.systemMessageContent.length > 16
@@ -155,84 +172,59 @@ export default function ChatBOX(props: {
<span>Total: {chatStore.totalTokens}</span>{" "} <span>Total: {chatStore.totalTokens}</span>{" "}
<span>Max: {chatStore.maxTokens}</span>{" "} <span>Max: {chatStore.maxTokens}</span>{" "}
<span>Margin: {chatStore.tokenMargin}</span>{" "} <span>Margin: {chatStore.tokenMargin}</span>{" "}
<span> <span>Message: {chatStore.history.length}</span>{" "}
Message: {chatStore.history.length - chatStore.postBeginIndex}
</span>{" "}
<span>Cut: {chatStore.postBeginIndex}</span> <span>Cut: {chatStore.postBeginIndex}</span>
</div> </div>
</p> </p>
<div className="grow overflow-scroll"> <div className="grow overflow-scroll">
{!chatStore.apiKey && ( {!chatStore.apiKey && (
<p className="opacity-60 p-6 rounded bg-white my-3 text-left"> <p className="opacity-60 p-6 rounded bg-white my-3 text-left dark:text-black">
(OPENAI) API KEY (OPENAI) API KEY
</p> </p>
)} )}
{!chatStore.apiEndpoint && ( {!chatStore.apiEndpoint && (
<p className="opacity-60 p-6 rounded bg-white my-3 text-left"> <p className="opacity-60 p-6 rounded bg-white my-3 text-left dark:text-black">
API Endpoint API Endpoint
</p> </p>
)} )}
{chatStore.history.length === 0 && ( {chatStore.history.length === 0 && (
<p className="opacity-60 p-6 rounded bg-white my-3 text-left"> <p className="opacity-60 p-6 rounded bg-white my-3 text-left dark:text-black">
QwQ
</p> </p>
)} )}
{chatStore.history.map((chat, i) => { {chatStore.history.map((_, messageIndex) => (
const pClassName = <Message
chat.role === "assistant" chatStore={chatStore}
? "p-2 rounded relative bg-white my-2 text-left" setChatStore={setChatStore}
: "p-2 rounded relative bg-green-400 my-2 text-right"; messageIndex={messageIndex}
const iconClassName = />
chat.role === "user"
? "absolute bottom-0 left-0"
: "absolute bottom-0 right-0";
const DeleteIcon = () => (
<button
className={iconClassName}
onClick={() => {
if (
confirm(
`Are you sure to delete this message?\n${chat.content.slice(
0,
39
)}...`
)
) {
chatStore.history.splice(i, 1);
chatStore.postBeginIndex = Math.max(
chatStore.postBeginIndex - 1,
0
);
setChatStore({ ...chatStore });
}
}}
>
🗑
</button>
);
return (
<p className={pClassName}>
{chat.content
.split("\n")
.filter((line) => line)
.map((line) => (
<p className="my-1">{line}</p>
))} ))}
<DeleteIcon />
</p>
);
})}
{showGenerating && ( {showGenerating && (
<p className="p-2 my-2 animate-pulse"> <p className="p-2 my-2 animate-pulse dark:text-white">
{generatingMessage {generatingMessage
? generatingMessage.split("\n").map((line) => <p>{line}</p>) ? generatingMessage.split("\n").map((line) => <p>{line}</p>)
: "生成中,保持网络稳定"} : "生成中,保持网络稳定"}
... ...
</p> </p>
)} )}
{showRetry && (
<p className="text-right p-2 my-2 dark:text-white">
<button
className="p-1 rounded bg-rose-500"
onClick={async () => {
setShowRetry(false);
await complete();
}}
>
Retry
</button>
</p>
)}
<div ref={messagesEndRef}></div>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<textarea <textarea
rows={Math.min(10, (inputMsg.match(/\n/g) || []).length + 2)}
value={inputMsg} value={inputMsg}
onChange={(event: any) => setInputMsg(event.target.value)} onChange={(event: any) => setInputMsg(event.target.value)}
onKeyPress={(event: any) => { onKeyPress={(event: any) => {

View File

@@ -9,6 +9,23 @@ export interface ChunkMessage {
}[]; }[];
} }
export interface FetchResponse {
id: string;
object: string;
created: number;
model: string;
usage: {
prompt_tokens: number | undefined;
completion_tokens: number | undefined;
total_tokens: number | undefined;
};
choices: {
message: Message | undefined;
finish_reason: "stop" | "length";
index: number | undefined;
}[];
}
class Chat { class Chat {
OPENAI_API_KEY: string; OPENAI_API_KEY: string;
messages: Message[]; messages: Message[];
@@ -57,22 +74,7 @@ class Chat {
}); });
} }
async fetch(): Promise<{ async fetch(): Promise<FetchResponse> {
id: string;
object: string;
created: number;
model: string;
usage: {
prompt_tokens: number | undefined;
completion_tokens: number | undefined;
total_tokens: number | undefined;
};
choices: {
message: Message | undefined;
finish_reason: "stop" | "length";
index: number | undefined;
}[];
}> {
const resp = await this._fetch(); const resp = await this._fetch();
return await resp.json(); return await resp.json();
} }
@@ -83,8 +85,7 @@ class Chat {
return this.messages.slice(-1)[0].content; return this.messages.slice(-1)[0].content;
} }
async complete(): Promise<string> { processFetchResponse(resp: FetchResponse): string {
const resp = await this.fetch();
this.total_tokens = resp?.usage?.total_tokens ?? 0; this.total_tokens = resp?.usage?.total_tokens ?? 0;
if (resp?.choices[0]?.message) { if (resp?.choices[0]?.message) {
this.messages.push(resp?.choices[0]?.message); this.messages.push(resp?.choices[0]?.message);
@@ -101,6 +102,11 @@ class Chat {
); );
} }
async complete(): Promise<string> {
const resp = await this.fetch();
return this.processFetchResponse(resp);
}
completeWithSteam() { completeWithSteam() {
this.total_tokens = this.messages this.total_tokens = this.messages
.map((msg) => this.calculate_token_length(msg.content) + 20) .map((msg) => this.calculate_token_length(msg.content) + 20)

View File

@@ -1,3 +1,27 @@
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
@tailwind utilities; @tailwind utilities;
/* Hide scrollbar for webkit based browsers */
::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for moz based browsers */
::-moz-scrollbar {
display: none;
}
/* Hide scrollbar for IE/Edge based browsers */
::-ms-scrollbar {
display: none;
}
/* Hide scrollbar for all based browsers */
body::-webkit-scrollbar {
display: none;
}
p.message-content {
white-space: pre-wrap;
}

72
src/message.tsx Normal file
View File

@@ -0,0 +1,72 @@
import Markdown from "preact-markdown";
import { ChatStore } from "./app";
const Pre: React.FC<any> = ({ children, props }) => (
<div class="rounded p-1 bg-black text-white" {...props}>{children}</div>
);
const Code: React.FC<any> = ({ children }) => <code className="overflow-scroll break-keep">{children}</code>;
interface Props {
messageIndex: number;
chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void;
}
export default function Message(props: Props) {
const { chatStore, messageIndex, setChatStore } = props;
const chat = chatStore.history[messageIndex];
const DeleteIcon = () => (
<button
className={`absolute bottom-0 ${
chat.role === "user" ? "left-0" : "right-0"
}`}
onClick={() => {
if (
confirm(
`Are you sure to delete this message?\n${chat.content.slice(
0,
39
)}...`
)
) {
chatStore.history.splice(messageIndex, 1);
chatStore.postBeginIndex = Math.max(chatStore.postBeginIndex - 1, 0);
setChatStore({ ...chatStore });
}
}}
>
🗑
</button>
);
const codeMatches = chat.content.match(/(```([\s\S]*?)```$)/);
const AnyMarkdown = Markdown as any;
console.log("codeMatches", codeMatches);
if (codeMatches) console.log("matches", codeMatches[0]);
return (
<div
className={`flex ${
chat.role === "assistant" ? "justify-start" : "justify-end"
}`}
>
<div
className={`relative w-fit p-2 rounded my-2 ${
chat.role === "assistant"
? "bg-white dark:bg-gray-700 dark:text-white"
: "bg-green-400"
}`}
>
<p className="message-content">
<AnyMarkdown
markdown={chat.content}
markupOpts={{
components: {
code: Code,
pre: Pre,
},
}}
/>
</p>
<DeleteIcon />
</div>
</div>
);
}

View File

@@ -97,11 +97,11 @@ export default (props: {
"//" + "//" +
location.host + location.host +
location.pathname + location.pathname +
`?sys=${encodeURIComponent( `?key=${encodeURIComponent(
props.chatStore.systemMessageContent props.chatStore.apiKey
)}&api=${encodeURIComponent(props.chatStore.apiEndpoint)}&mode=${ )}&api=${encodeURIComponent(props.chatStore.apiEndpoint)}&mode=${
props.chatStore.streamMode ? "stream" : "fetch" props.chatStore.streamMode ? "stream" : "fetch"
}`; }&sys=${encodeURIComponent(props.chatStore.systemMessageContent)}`;
return ( return (
<div className="left-0 top-0 overflow-scroll flex justify-center absolute w-screen h-screen bg-black bg-opacity-50 z-10"> <div className="left-0 top-0 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"> <div className="m-2 p-2 bg-white rounded-lg h-fit">
@@ -110,13 +110,17 @@ export default (props: {
<div className="box"> <div className="box">
<Input <Input
field="systemMessageContent" field="systemMessageContent"
help="系统消息用于指示ChatGPT的角色和一些前置条件" help="系统消息用于指示ChatGPT的角色和一些前置条件,例如“你是一个有帮助的人工智能助理”,或者“你是一个专业英语翻译,把我的话全部翻译成英语”,详情参考 OPEAN AI API 文档"
{...props}
/>
<Input
field="apiKey"
help="OPEN AI API 密钥,请勿泄漏此密钥"
{...props} {...props}
/> />
<Input field="apiKey" help="OPEN AI API 密钥" {...props} />
<Input <Input
field="apiEndpoint" field="apiEndpoint"
help="API 端点,方便在不支持的地区使用反向代理服务" help="API 端点,方便在不支持的地区使用反向代理服务,默认为 https://api.openai.com/v1/chat/completions"
{...props} {...props}
/> />
<Choice <Choice
@@ -170,6 +174,8 @@ export default (props: {
) )
return; return;
props.chatStore.history = []; props.chatStore.history = [];
props.chatStore.postBeginIndex = 0;
props.chatStore.totalTokens = 0;
props.setChatStore({ ...props.chatStore }); props.setChatStore({ ...props.chatStore });
}} }}
> >

View File

@@ -804,6 +804,11 @@ lru-cache@^5.1.1:
dependencies: dependencies:
yallist "^3.0.2" yallist "^3.0.2"
marked@^4.0.10:
version "4.3.0"
resolved "https://registry.npmmirror.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3"
integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==
merge2@^1.3.0: merge2@^1.3.0:
version "1.4.1" version "1.4.1"
resolved "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" resolved "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
@@ -925,6 +930,19 @@ postcss@^8.0.9, postcss@^8.4.21:
picocolors "^1.0.0" picocolors "^1.0.0"
source-map-js "^1.0.2" source-map-js "^1.0.2"
preact-markdown@^2.1.0:
version "2.1.0"
resolved "https://registry.npmmirror.com/preact-markdown/-/preact-markdown-2.1.0.tgz#c271cdd084b8854778f7d8e3640bbe9a7ea6ba4d"
integrity sha512-6c2hfarjLFkVDNa1hUKytXID6wl6yilZnGb2y83xKXnfk5SpXYAwhJc+JENgffAcNALWggqvX/ezlk8/8qJsuA==
dependencies:
marked "^4.0.10"
preact-markup "^2.1.1"
preact-markup@^2.1.1:
version "2.1.1"
resolved "https://registry.npmmirror.com/preact-markup/-/preact-markup-2.1.1.tgz#0451e7eed1dac732d7194c34a7f16ff45a2cfdd7"
integrity sha512-8JL2p36mzK8XkspOyhBxUSPjYwMxDM0L5BWBZWxsZMVW8WsGQrYQDgVuDKkRspt2hwrle+Cxr/053hpc9BJwfw==
preact@^10.11.3: preact@^10.11.3:
version "10.13.1" version "10.13.1"
resolved "https://registry.npmmirror.com/preact/-/preact-10.13.1.tgz#d220bd8771b8fa197680d4917f3cefc5eed88720" resolved "https://registry.npmmirror.com/preact/-/preact-10.13.1.tgz#d220bd8771b8fa197680d4917f3cefc5eed88720"