init
This commit is contained in:
24
.gitignore
vendored
Normal file
24
.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
43
README.md
Normal file
43
README.md
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
# ChatGPT API WEB
|
||||||
|
|
||||||
|
一个简单的网页,调用 OPENAI ChatGPT 进行对话。
|
||||||
|
|
||||||
|
与官方 ChatGPT 相比:
|
||||||
|
|
||||||
|
- 对话记录使用浏览器的 localStorage 保存在本地
|
||||||
|
- 可删除对话消息
|
||||||
|
- 可以设置 system message (如:"你是一个喵娘",参见官方 [API 文档](https://platform.openai.com/docs/guides/chat))
|
||||||
|
- 可以为不同对话设置不同 APIKEY
|
||||||
|
- 小(整个网页 30k 左右)
|
||||||
|
- 可以设置不同的 API Endpoint(方便墙内人士使用反向代理转发 API 请求)
|
||||||
|
|
||||||
|
## 屏幕截图
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
~~发病.webp~~
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
以下任意方式都可:
|
||||||
|
|
||||||
|
- 访问 github pages 部署
|
||||||
|
- 从 release 下载网页文件然后双击打开
|
||||||
|
- 自行编译构建网页
|
||||||
|
|
||||||
|
### 更改默认参数
|
||||||
|
|
||||||
|
- `key`: OPENAI API KEY 默认为空
|
||||||
|
- `sys`: system message 默认为 "你是一个猫娘,你要模仿猫娘的语气说话"
|
||||||
|
- `api`: API Endpoint 默认为 `https://api.openai.com/v1/chat/completions`
|
||||||
|
|
||||||
|
例如 `http://localhost:1234/?key=xxxx` 那么新创建的会话将会使用该默认 API
|
||||||
|
|
||||||
|
以上三个参数应用于单个对话,随时可在顶部更改
|
||||||
|
|
||||||
|
## 自行编译构建网页
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn install
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
12
index.html
Normal file
12
index.html
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>ChatGPT API Web</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
23
package.json
Normal file
23
package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "chatgpt-api-web",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"autoprefixer": "^10.4.14",
|
||||||
|
"postcss": "^8.4.21",
|
||||||
|
"preact": "^10.11.3",
|
||||||
|
"sakura.css": "^1.4.1",
|
||||||
|
"tailwindcss": "^3.2.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@preact/preset-vite": "^2.5.0",
|
||||||
|
"typescript": "^4.9.3",
|
||||||
|
"vite": "^4.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.cjs
Normal file
6
postcss.config.cjs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
BIN
screenshot.webp
Normal file
BIN
screenshot.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 52 KiB |
311
src/app.tsx
Normal file
311
src/app.tsx
Normal file
@@ -0,0 +1,311 @@
|
|||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import "./global.css";
|
||||||
|
|
||||||
|
import ChatGPT, { Message } from "./chatgpt";
|
||||||
|
import { createRef } from "preact";
|
||||||
|
|
||||||
|
export interface ChatStore {
|
||||||
|
systemMessageContent: string;
|
||||||
|
history: Message[];
|
||||||
|
postBeginIndex: number;
|
||||||
|
tokenMargin: number;
|
||||||
|
totalTokens: number;
|
||||||
|
maxTokens: number;
|
||||||
|
apiKey: string;
|
||||||
|
apiEndpoint: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultAPIKEY = () => {
|
||||||
|
const queryParameters = new URLSearchParams(window.location.search);
|
||||||
|
const key = queryParameters.get("key");
|
||||||
|
return key;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultSysMessage = () => {
|
||||||
|
const queryParameters = new URLSearchParams(window.location.search);
|
||||||
|
const sys = queryParameters.get("sys");
|
||||||
|
return sys;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultAPIEndpoint = () => {
|
||||||
|
const queryParameters = new URLSearchParams(window.location.search);
|
||||||
|
const sys = queryParameters.get("api");
|
||||||
|
return sys;
|
||||||
|
};
|
||||||
|
|
||||||
|
const _defaultAPIEndpoint = "https://api.openai.com/v1/chat/completions";
|
||||||
|
export const newChatStore = (
|
||||||
|
apiKey = "",
|
||||||
|
systemMessageContent = "你是一个猫娘,你要模仿猫娘的语气说话",
|
||||||
|
apiEndpoint = _defaultAPIEndpoint
|
||||||
|
): ChatStore => {
|
||||||
|
return {
|
||||||
|
systemMessageContent: defaultSysMessage() || systemMessageContent,
|
||||||
|
history: [],
|
||||||
|
postBeginIndex: 0,
|
||||||
|
tokenMargin: 1024,
|
||||||
|
totalTokens: 0,
|
||||||
|
maxTokens: 4096,
|
||||||
|
apiKey: defaultAPIKEY() || apiKey,
|
||||||
|
apiEndpoint: defaultAPIEndpoint() || apiEndpoint,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const STORAGE_NAME = "chatgpt-api-web";
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const initAllChatStore: ChatStore[] = JSON.parse(
|
||||||
|
localStorage.getItem(STORAGE_NAME) || "[]"
|
||||||
|
);
|
||||||
|
if (initAllChatStore.length === 0) {
|
||||||
|
initAllChatStore.push(newChatStore());
|
||||||
|
localStorage.setItem(STORAGE_NAME, JSON.stringify(initAllChatStore));
|
||||||
|
}
|
||||||
|
const [allChatStore, setAllChatStore] = useState(initAllChatStore);
|
||||||
|
const [selectedChatIndex, setSelectedChatIndex] = useState(0);
|
||||||
|
const chatStore = allChatStore[selectedChatIndex];
|
||||||
|
const setChatStore = (cs: ChatStore) => {
|
||||||
|
allChatStore[selectedChatIndex] = cs;
|
||||||
|
setAllChatStore([...allChatStore]);
|
||||||
|
};
|
||||||
|
useEffect(() => {
|
||||||
|
console.log("saved", allChatStore);
|
||||||
|
localStorage.setItem(STORAGE_NAME, JSON.stringify(allChatStore));
|
||||||
|
}, [allChatStore]);
|
||||||
|
|
||||||
|
const [inputMsg, setInputMsg] = useState("");
|
||||||
|
const [showGenerating, setShowGenerating] = useState(false);
|
||||||
|
|
||||||
|
const client = new ChatGPT(chatStore.apiKey);
|
||||||
|
|
||||||
|
const _complete = async () => {
|
||||||
|
client.apiEndpoint = chatStore.apiEndpoint;
|
||||||
|
client.sysMessageContent = chatStore.systemMessageContent;
|
||||||
|
client.messages = chatStore.history.slice(chatStore.postBeginIndex);
|
||||||
|
const response = await client.complete();
|
||||||
|
chatStore.history.push({ role: "assistant", content: response });
|
||||||
|
chatStore.maxTokens = client.max_tokens;
|
||||||
|
chatStore.tokenMargin = client.tokens_margin;
|
||||||
|
chatStore.totalTokens = client.total_tokens;
|
||||||
|
chatStore.postBeginIndex =
|
||||||
|
chatStore.history.length - client.messages.length;
|
||||||
|
console.log("postBeginIndex", chatStore.postBeginIndex);
|
||||||
|
setChatStore({ ...chatStore });
|
||||||
|
};
|
||||||
|
|
||||||
|
const complete = async () => {
|
||||||
|
try {
|
||||||
|
setShowGenerating(true);
|
||||||
|
await _complete();
|
||||||
|
} catch (error) {
|
||||||
|
alert(error);
|
||||||
|
} finally {
|
||||||
|
setShowGenerating(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const send = async () => {
|
||||||
|
if (!inputMsg) {
|
||||||
|
console.log("empty message");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
chatStore.history.push({ role: "user", content: inputMsg.trim() });
|
||||||
|
setChatStore({ ...chatStore });
|
||||||
|
setInputMsg("");
|
||||||
|
await complete();
|
||||||
|
setChatStore({ ...chatStore });
|
||||||
|
};
|
||||||
|
|
||||||
|
const changAPIKEY = () => {
|
||||||
|
const newAPIKEY = prompt(`Current API KEY: ${chatStore.apiKey}`);
|
||||||
|
if (!newAPIKEY) return;
|
||||||
|
chatStore.apiKey = newAPIKEY;
|
||||||
|
setChatStore({ ...chatStore });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex text-sm h-screen bg-slate-200">
|
||||||
|
<div className="flex flex-col h-full p-4 border-r-indigo-500 border-2">
|
||||||
|
<div className="grow overflow-scroll">
|
||||||
|
<button
|
||||||
|
className="bg-violet-300 p-1 rounded hover:bg-violet-400"
|
||||||
|
onClick={() => {
|
||||||
|
allChatStore.push(
|
||||||
|
newChatStore(
|
||||||
|
allChatStore[selectedChatIndex].apiKey,
|
||||||
|
allChatStore[selectedChatIndex].systemMessageContent,
|
||||||
|
allChatStore[selectedChatIndex].apiEndpoint
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setAllChatStore([...allChatStore]);
|
||||||
|
setSelectedChatIndex(allChatStore.length - 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
NEW
|
||||||
|
</button>
|
||||||
|
<ul>
|
||||||
|
{allChatStore.map((cs, i) => (
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
className={`w-full my-1 p-1 rounded hover:bg-blue-300 ${
|
||||||
|
i === selectedChatIndex ? "bg-blue-500" : "bg-blue-200"
|
||||||
|
}`}
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedChatIndex(i);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{i}
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="rounded bg-rose-400 p-1 my-1 w-full"
|
||||||
|
onClick={() => {
|
||||||
|
if (!confirm("Are you sure you want to delete this chat history?"))
|
||||||
|
return;
|
||||||
|
const oldAPIkey = allChatStore[selectedChatIndex].apiKey;
|
||||||
|
allChatStore.splice(selectedChatIndex, 1);
|
||||||
|
if (allChatStore.length === 0) {
|
||||||
|
allChatStore.push(
|
||||||
|
newChatStore(
|
||||||
|
oldAPIkey,
|
||||||
|
allChatStore[selectedChatIndex].systemMessageContent,
|
||||||
|
allChatStore[selectedChatIndex].apiEndpoint
|
||||||
|
)
|
||||||
|
);
|
||||||
|
setSelectedChatIndex(0);
|
||||||
|
} else {
|
||||||
|
setSelectedChatIndex(selectedChatIndex - 1);
|
||||||
|
}
|
||||||
|
setAllChatStore([...allChatStore]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
DEL
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="grow flex flex-col p-4">
|
||||||
|
<p>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
className="underline"
|
||||||
|
onClick={() => {
|
||||||
|
const newSysMsgContent = prompt(
|
||||||
|
"Change system message content"
|
||||||
|
);
|
||||||
|
if (newSysMsgContent === null) return;
|
||||||
|
chatStore.systemMessageContent = newSysMsgContent;
|
||||||
|
setChatStore({ ...chatStore });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{chatStore.systemMessageContent}
|
||||||
|
</button>{" "}
|
||||||
|
<button className="underline" onClick={changAPIKEY}>
|
||||||
|
KEY
|
||||||
|
</button>{" "}
|
||||||
|
<button
|
||||||
|
className="underline"
|
||||||
|
onClick={() => {
|
||||||
|
const newEndpoint = prompt(
|
||||||
|
`Enter new API endpoint\n(current: ${chatStore.apiEndpoint})\n(default: ${_defaultAPIEndpoint})`
|
||||||
|
);
|
||||||
|
if (!newEndpoint) return;
|
||||||
|
chatStore.apiEndpoint = newEndpoint;
|
||||||
|
setChatStore({ ...chatStore });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
ENDPOINT
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs">
|
||||||
|
<span>Total: {chatStore.totalTokens}</span>{" "}
|
||||||
|
<span>Max: {chatStore.maxTokens}</span>{" "}
|
||||||
|
<span>Margin: {chatStore.tokenMargin}</span>{" "}
|
||||||
|
<span>
|
||||||
|
Message: {chatStore.history.length - chatStore.postBeginIndex}
|
||||||
|
</span>{" "}
|
||||||
|
<span>Cut: {chatStore.postBeginIndex}</span>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<div className="grow overflow-scroll">
|
||||||
|
{chatStore.history.length === 0 && (
|
||||||
|
<p className="opacity-60 p-6 rounded bg-white my-3 text-left">
|
||||||
|
喵喵,请先在上方设置 (OPENAI) API KEY
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{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 = () => (
|
||||||
|
<button
|
||||||
|
className={iconClassName}
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
confirm(
|
||||||
|
`Are you sure to delete this message?\n${chat.content.slice(
|
||||||
|
0,
|
||||||
|
39
|
||||||
|
)}...`
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
chatStore.history.splice(i, 1);
|
||||||
|
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 && (
|
||||||
|
<p className="animate-pulse">Generating... please wait...</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<textarea
|
||||||
|
value={inputMsg}
|
||||||
|
onChange={(event: any) => setInputMsg(event.target.value)}
|
||||||
|
onKeyPress={(event: any) => {
|
||||||
|
console.log(event);
|
||||||
|
if (event.ctrlKey && event.code === "Enter") {
|
||||||
|
send();
|
||||||
|
setInputMsg("");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setInputMsg(event.target.value);
|
||||||
|
}}
|
||||||
|
className="rounded grow m-1 p-1 border-2 border-gray-400 w-0"
|
||||||
|
placeholder="Type here..."
|
||||||
|
></textarea>
|
||||||
|
<button
|
||||||
|
className="rounded m-1 p-1 border-2 border-rose-400 bg-cyan-400 hover:bg-cyan-600"
|
||||||
|
disabled={showGenerating || !chatStore.apiKey}
|
||||||
|
onClick={() => {
|
||||||
|
send();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
148
src/chatgpt.ts
Normal file
148
src/chatgpt.ts
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
export interface Message {
|
||||||
|
role: "system" | "user" | "assistant";
|
||||||
|
content: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class Chat {
|
||||||
|
OPENAI_API_KEY: string;
|
||||||
|
messages: Message[];
|
||||||
|
sysMessageContent: string;
|
||||||
|
total_tokens: number;
|
||||||
|
max_tokens: number;
|
||||||
|
tokens_margin: number;
|
||||||
|
apiEndpoint: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
OPENAI_API_KEY: string | undefined,
|
||||||
|
{
|
||||||
|
systemMessage = "你是一个有用的人工智能助理",
|
||||||
|
max_tokens = 4096,
|
||||||
|
tokens_margin = 1024,
|
||||||
|
apiEndPoint = "https://api.openai.com/v1/chat/completions",
|
||||||
|
} = {}
|
||||||
|
) {
|
||||||
|
if (OPENAI_API_KEY === undefined) {
|
||||||
|
throw "OPENAI_API_KEY is undefined";
|
||||||
|
}
|
||||||
|
this.OPENAI_API_KEY = OPENAI_API_KEY;
|
||||||
|
this.messages = [];
|
||||||
|
this.total_tokens = 0;
|
||||||
|
this.max_tokens = max_tokens;
|
||||||
|
this.tokens_margin = tokens_margin;
|
||||||
|
this.sysMessageContent = systemMessage;
|
||||||
|
this.apiEndpoint = apiEndPoint;
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetch(): Promise<{
|
||||||
|
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 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
async say(content: string): Promise<string> {
|
||||||
|
this.messages.push({ role: "user", content });
|
||||||
|
await this.complete();
|
||||||
|
return this.messages.slice(-1)[0].content;
|
||||||
|
}
|
||||||
|
|
||||||
|
async complete(): Promise<string> {
|
||||||
|
const resp = await this.fetch();
|
||||||
|
this.total_tokens = resp?.usage?.total_tokens ?? 0;
|
||||||
|
if (resp?.choices[0]?.message) {
|
||||||
|
this.messages.push(resp?.choices[0]?.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resp.choices[0]?.finish_reason === "length") {
|
||||||
|
this.forceForgetSomeMessages();
|
||||||
|
} else {
|
||||||
|
this.forgetSomeMessages();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
resp?.choices[0]?.message?.content ?? `Error: ${JSON.stringify(resp)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
const chineseCount = content.match(/[\u00ff-\uffff]|\S+/g)?.length ?? 0;
|
||||||
|
const englishCount = totalCount - chineseCount;
|
||||||
|
const tokenLength = englishCount / 4 + (chineseCount * 4) / 3;
|
||||||
|
return ~~tokenLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
user(...messages: string[]) {
|
||||||
|
for (const msg of messages) {
|
||||||
|
this.messages.push({ role: "user", content: msg });
|
||||||
|
this.total_tokens += this.calculate_token_length(msg);
|
||||||
|
this.forgetSomeMessages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assistant(...messages: string[]) {
|
||||||
|
for (const msg of messages) {
|
||||||
|
this.messages.push({ role: "assistant", content: msg });
|
||||||
|
this.total_tokens += this.calculate_token_length(msg);
|
||||||
|
this.forgetSomeMessages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
forgetSomeMessages() {
|
||||||
|
// forget occur condition
|
||||||
|
if (this.total_tokens + this.tokens_margin >= this.max_tokens) {
|
||||||
|
this.forceForgetSomeMessages();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
forceForgetSomeMessages() {
|
||||||
|
this.messages = [
|
||||||
|
...this.messages.slice(Math.max(~~(this.messages.length / 4), 2)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
forgetAllMessage() {
|
||||||
|
this.messages = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
stats(): string {
|
||||||
|
return (
|
||||||
|
`total_tokens: ${this.total_tokens}` +
|
||||||
|
"\n" +
|
||||||
|
`max_tokens: ${this.max_tokens}` +
|
||||||
|
"\n" +
|
||||||
|
`tokens_margin: ${this.tokens_margin}` +
|
||||||
|
"\n" +
|
||||||
|
`messages.length: ${this.messages.length}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Chat;
|
||||||
3
src/global.css
Normal file
3
src/global.css
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
4
src/main.tsx
Normal file
4
src/main.tsx
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { render } from 'preact'
|
||||||
|
import { App } from './app'
|
||||||
|
|
||||||
|
render(<App />, document.getElementById('app') as HTMLElement)
|
||||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
8
tailwind.config.cjs
Normal file
8
tailwind.config.cjs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
22
tsconfig.json
Normal file
22
tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": false,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "preact"
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
9
tsconfig.node.json
Normal file
9
tsconfig.node.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
8
vite.config.ts
Normal file
8
vite.config.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import preact from '@preact/preset-vite'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [preact()],
|
||||||
|
base: './',
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user