Compare commits
24 Commits
71cdba59ee
...
markdown
| Author | SHA1 | Date | |
|---|---|---|---|
|
e1ef16015d
|
|||
|
6b4cbd62ac
|
|||
|
61c4f548f1
|
|||
|
bb84045481
|
|||
|
7a15792c39
|
|||
|
2ab9a0a46c
|
|||
|
99d3c69647
|
|||
|
0148465e34
|
|||
|
9b00cd0fbc
|
|||
|
7149ed0310
|
|||
|
46b842500d
|
|||
|
f278e5b1fc
|
|||
|
c7f731c2fa
|
|||
|
371adfb1d4
|
|||
|
2ed7f9d05a
|
|||
|
e0b50ced12
|
|||
|
80508f9c6c
|
|||
|
14457cbb5f
|
|||
|
f59b63884d
|
|||
|
9b5730760a
|
|||
|
1372121e32
|
|||
|
09e9d18e71
|
|||
|
383d6de1d6
|
|||
|
e56389b4c2
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
/dist.zip
|
||||||
# Logs
|
# Logs
|
||||||
logs
|
logs
|
||||||
*.log
|
*.log
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
~~让喵娘统治世界吧((发病.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
|
||||||
|
|
||||||
以上参数应用于单个对话,随时可在顶部更改
|
以上参数应用于单个对话,随时可在顶部更改
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
},
|
},
|
||||||
|
|||||||
23
src/app.tsx
23
src/app.tsx
@@ -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);
|
||||||
|
|
||||||
|
|||||||
134
src/chatbox.tsx
134
src/chatbox.tsx
@@ -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) => {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
72
src/message.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
18
yarn.lock
18
yarn.lock
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user