Compare commits
85 Commits
wenker
...
626f406711
| Author | SHA1 | Date | |
|---|---|---|---|
|
626f406711
|
|||
|
9dd4d99e54
|
|||
|
5039bdfca8
|
|||
|
64f1e3d70e
|
|||
|
b98900873c
|
|||
|
400ebafc37
|
|||
|
e7c26560bb
|
|||
|
6aca74a7b4
|
|||
|
a763355420
|
|||
|
8122f6d8bf
|
|||
|
31c49ff888
|
|||
|
fd5f87f845
|
|||
|
587d0ba57d
|
|||
|
795cb16ed4
|
|||
|
47d96198e8
|
|||
|
eb8a6dc8ed
|
|||
|
32866d9a7f
|
|||
|
9cfb09a5ac
|
|||
|
7196799625
|
|||
|
915987cbfe
|
|||
|
9855027876
|
|||
|
2670183343
|
|||
|
ad291bd72e
|
|||
|
1fbd4ee87b
|
|||
|
af2ae82e74
|
|||
|
9e74e419c9
|
|||
|
ee9da49f70
|
|||
|
dccf4827c9
|
|||
|
04bac03fd7
|
|||
|
f0f040c42c
|
|||
|
1c3c94bae4
|
|||
|
f5d43ec4b9
|
|||
|
6df6ad031a
|
|||
|
3cc80fd8fe
|
|||
|
e09036860f
|
|||
|
49537a0d58
|
|||
|
243f1a5ea5
|
|||
|
b3a2988907
|
|||
|
4e2ac186d5
|
|||
|
41e3026ac5
|
|||
|
c123f9454a
|
|||
|
c473fd496e
|
|||
|
6a848580f6
|
|||
|
91f7043b7c
|
|||
|
2b430bd395
|
|||
|
46c8a87a06
|
|||
|
fb48723d34
|
|||
|
370a680d94
|
|||
|
9417b99ad4
|
|||
|
63b2f41b97
|
|||
|
44f5d28565
|
|||
|
b3d84ea454
|
|||
|
b9fdfb8905
|
|||
|
3328b3e94d
|
|||
| 603ec23d24 | |||
|
|
08a7670509 | ||
|
|
3ee5cd32bc | ||
|
|
9298839b4f | ||
|
|
9d0f93ecf6 | ||
|
|
c2c17e5956 | ||
|
|
415fb934ae | ||
|
|
148d912be5 | ||
|
|
52d8c3280e | ||
|
|
a45785c607 | ||
|
|
8c17b842b2 | ||
|
|
4bf3e02962 | ||
|
|
0ae53ff954 | ||
|
4079ec77f9
|
|||
|
6e647d9181
|
|||
|
4162866fd6
|
|||
|
09f6e5b490
|
|||
|
6247d75234
|
|||
|
4dd29af256
|
|||
|
245db574f8
|
|||
|
2386e6f2e9
|
|||
|
117fce390c
|
|||
|
8e1e82cf4b
|
|||
|
c0ec74638a
|
|||
|
d4da4c3e32
|
|||
|
2a21985a17
|
|||
|
f0c16a3cd1
|
|||
|
f54b192616
|
|||
|
b20de667a4
|
|||
|
a76cf224f6
|
|||
|
943cb5f392
|
27
.gitea/workflows/default.yml
Normal file
27
.gitea/workflows/default.yml
Normal file
@@ -0,0 +1,27 @@
|
||||
name: Build static content
|
||||
|
||||
on:
|
||||
# Runs on pushes targeting the default branch
|
||||
push:
|
||||
branches: ["master"]
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20.x
|
||||
cache: 'npm'
|
||||
- run: npm install
|
||||
- run: npm run build
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: dist-files
|
||||
path: './dist/'
|
||||
@@ -55,7 +55,7 @@ ChatGPT API WEB 是为 ChatGPT 的日常用户和 Prompt 工程师设计的项
|
||||
- `api`: API Endpoint 默认为 `https://api.openai.com/v1/chat/completions`
|
||||
- `mode`: `fetch` 或 `stream` 模式,stream 模式下可以动态看到 api 返回的数据,但无法得知 token 数量,只能进行估算,在 token 数量过多时可能会裁切过多或过少历史消息
|
||||
- `dev`: true / false 开发模式,这个模式下可以看到并调整更多参数
|
||||
- `temp`: 温度,默认 0.7
|
||||
- `temp`: 温度,默认 1
|
||||
- `whisper-api`: Whisper 语音转文字服务 API, 只有设置了此值后才会显示语音转文字按钮
|
||||
- `whisper-key`: 用于 Whisper 服务的 key,如果留空则默认使用上方的 OPENAI API KEY
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html data-theme="cupcake" lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
|
||||
21
package.json
21
package.json
@@ -9,19 +9,22 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/ungap__structured-clone": "^0.3.1",
|
||||
"@heroicons/react": "^2.1.5",
|
||||
"@types/ungap__structured-clone": "^1.2.0",
|
||||
"@ungap/structured-clone": "^1.2.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"idb": "^7.1.1",
|
||||
"postcss": "^8.4.31",
|
||||
"preact": "^10.18.1",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"idb": "^8.0.0",
|
||||
"postcss": "^8.4.47",
|
||||
"preact": "^10.24.3",
|
||||
"preact-markdown": "^2.1.0",
|
||||
"sakura.css": "^1.5.0",
|
||||
"tailwindcss": "^3.3.4"
|
||||
"tailwindcss": "^3.4.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@preact/preset-vite": "^2.6.0",
|
||||
"typescript": "^5.2.2",
|
||||
"vite": "^4.5.0"
|
||||
"@preact/preset-vite": "^2.9.1",
|
||||
"daisyui": "^4.12.13",
|
||||
"theme-change": "^2.5.0",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.8"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
const CHATGPT_API_WEB_VERSION = "v2.1.0";
|
||||
|
||||
export default CHATGPT_API_WEB_VERSION;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "preact/hooks";
|
||||
import { ChatStore } from "./app";
|
||||
import { MessageDetail } from "./chatgpt";
|
||||
import { Tr } from "./translate";
|
||||
import { ChatStore } from "@/types/chatstore";
|
||||
import { MessageDetail } from "@/chatgpt";
|
||||
import { Tr } from "@/translate";
|
||||
|
||||
interface Props {
|
||||
chatStore: ChatStore;
|
||||
@@ -41,15 +41,25 @@ export function AddImage({
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded p-2 z-20"
|
||||
className="bg-base-200 p-2 z-20"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<h2>Add Images</h2>
|
||||
<span>
|
||||
<div className="flex justify-between items-center p-1">
|
||||
<h3>Add Images</h3>
|
||||
<button
|
||||
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
|
||||
className="btn btn-sm btn-neutral"
|
||||
onClick={() => {
|
||||
setShowAddImage(false);
|
||||
}}
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
</div>
|
||||
<span className="">
|
||||
<button
|
||||
className="btn btn-secondary btn-sm disabled:btn-disabled"
|
||||
onClick={() => {
|
||||
const image_url = prompt("Image URL");
|
||||
if (!image_url) {
|
||||
@@ -70,7 +80,7 @@ export function AddImage({
|
||||
Add from URL
|
||||
</button>
|
||||
<button
|
||||
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
|
||||
className="btn btn-primary btn-sm disabled:btn-disabled"
|
||||
onClick={() => {
|
||||
// select file and load it to base64 image URL format
|
||||
const input = document.createElement("input");
|
||||
@@ -111,23 +121,24 @@ export function AddImage({
|
||||
<input type="checkbox" checked={enableHighResolution} />
|
||||
</span>
|
||||
</span>
|
||||
<div className="divider"></div>
|
||||
{chatStore.image_gen_api && chatStore.image_gen_key && (
|
||||
<div className="flex flex-col">
|
||||
<hr className="my-2" />
|
||||
<h3>Generate Image</h3>
|
||||
<span className="flex flex-row justify-between m-1 p-1">
|
||||
<span className="flex flex-col justify-between m-1 p-1">
|
||||
<label>Prompt: </label>
|
||||
<textarea
|
||||
className="border rounded border-gray-400"
|
||||
className="textarea textarea-sm textarea-bordered"
|
||||
value={imageGenPrompt}
|
||||
onChange={(e: any) => {
|
||||
setImageGenPrompt(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
<span className="flex flex-row justify-between m-1 p-1">
|
||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||
<label>Model: </label>
|
||||
<select
|
||||
className="select select-sm select-bordered"
|
||||
value={imageGenModel}
|
||||
onChange={(e: any) => {
|
||||
setImageGenModel(e.target.value);
|
||||
@@ -137,9 +148,10 @@ export function AddImage({
|
||||
<option value="dall-e-2">DALL-E 2</option>
|
||||
</select>
|
||||
</span>
|
||||
<span className="flex flex-row justify-between m-1 p-1">
|
||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||
<label>n: </label>
|
||||
<input
|
||||
className="input input-sm input-bordered"
|
||||
value={imageGenN}
|
||||
type="number"
|
||||
min={1}
|
||||
@@ -147,9 +159,10 @@ export function AddImage({
|
||||
onChange={(e: any) => setImageGenN(parseInt(e.target.value))}
|
||||
/>
|
||||
</span>
|
||||
<span className="flex flex-row justify-between m-1 p-1">
|
||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||
<label>Quality: </label>
|
||||
<select
|
||||
className="select select-sm select-bordered"
|
||||
value={imageGenQuality}
|
||||
onChange={(e: any) => setImageGEnQuality(e.target.value)}
|
||||
>
|
||||
@@ -157,9 +170,10 @@ export function AddImage({
|
||||
<option value="standard">Standard</option>
|
||||
</select>
|
||||
</span>
|
||||
<span className="flex flex-row justify-between m-1 p-1">
|
||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||
<label>Response Format: </label>
|
||||
<select
|
||||
className="select select-sm select-bordered"
|
||||
value={imageGenResponseFormat}
|
||||
onChange={(e: any) => setImageGenResponseFormat(e.target.value)}
|
||||
>
|
||||
@@ -167,9 +181,10 @@ export function AddImage({
|
||||
<option value="url">url</option>
|
||||
</select>
|
||||
</span>
|
||||
<span className="flex flex-row justify-between m-1 p-1">
|
||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||
<label>Size: </label>
|
||||
<select
|
||||
className="select select-sm select-bordered"
|
||||
value={imageGenSize}
|
||||
onChange={(e: any) => setImageGenSize(e.target.value)}
|
||||
>
|
||||
@@ -180,9 +195,10 @@ export function AddImage({
|
||||
<option value="1024x1792">1024x1792 (dall-e-3)</option>
|
||||
</select>
|
||||
</span>
|
||||
<span className="flex flex-row justify-between m-1 p-1">
|
||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||
<label>Style (only dall-e-3): </label>
|
||||
<select
|
||||
className="select select-sm select-bordered"
|
||||
value={imageGenStyle}
|
||||
onChange={(e: any) => setImageGenStyle(e.target.value)}
|
||||
>
|
||||
@@ -190,9 +206,9 @@ export function AddImage({
|
||||
<option value="natural">natural</option>
|
||||
</select>
|
||||
</span>
|
||||
<span className="flex flex-row justify-between m-1 p-1">
|
||||
<span className="flex flex-row justify-between items-center m-1 p-1">
|
||||
<button
|
||||
className="bg-sky-400 m-1 p-1 rounded disabled:bg-slate-500"
|
||||
className="btn btn-primary btn-sm"
|
||||
disabled={imageGenGenerating}
|
||||
onClick={async () => {
|
||||
try {
|
||||
@@ -247,6 +263,7 @@ export function AddImage({
|
||||
example: false,
|
||||
audio: null,
|
||||
logprobs: null,
|
||||
response_model_name: imageGenModel,
|
||||
});
|
||||
|
||||
setChatStore({ ...chatStore });
|
||||
|
||||
425
src/app.tsx
425
src/app.tsx
@@ -1,425 +0,0 @@
|
||||
import { IDBPDatabase, openDB } from "idb";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import "./global.css";
|
||||
|
||||
import { calculate_token_length, Logprobs, Message } from "./chatgpt";
|
||||
import getDefaultParams from "./getDefaultParam";
|
||||
import ChatBOX from "./chatbox";
|
||||
import models, { defaultModel } from "./models";
|
||||
import { Tr, langCodeContext, LANG_OPTIONS } from "./translate";
|
||||
|
||||
import CHATGPT_API_WEB_VERSION from "./CHATGPT_API_WEB_VERSION";
|
||||
|
||||
export interface ChatStoreMessage extends Message {
|
||||
hide: boolean;
|
||||
token: number;
|
||||
example: boolean;
|
||||
audio: Blob | null;
|
||||
logprobs: Logprobs | null;
|
||||
}
|
||||
|
||||
export interface TemplateAPI {
|
||||
name: string;
|
||||
key: string;
|
||||
endpoint: string;
|
||||
}
|
||||
|
||||
export interface TemplateTools {
|
||||
name: string;
|
||||
toolsString: string;
|
||||
}
|
||||
|
||||
export interface ChatStore {
|
||||
chatgpt_api_web_version: string;
|
||||
systemMessageContent: string;
|
||||
toolsString: string;
|
||||
history: ChatStoreMessage[];
|
||||
postBeginIndex: number;
|
||||
tokenMargin: number;
|
||||
totalTokens: number;
|
||||
maxTokens: number;
|
||||
maxGenTokens: number;
|
||||
maxGenTokens_enabled: boolean;
|
||||
apiKey: string;
|
||||
apiEndpoint: string;
|
||||
streamMode: boolean;
|
||||
model: string;
|
||||
responseModelName: string;
|
||||
cost: number;
|
||||
temperature: number;
|
||||
temperature_enabled: boolean;
|
||||
top_p: number;
|
||||
top_p_enabled: boolean;
|
||||
presence_penalty: number;
|
||||
frequency_penalty: number;
|
||||
develop_mode: boolean;
|
||||
whisper_api: string;
|
||||
whisper_key: string;
|
||||
tts_api: string;
|
||||
tts_key: string;
|
||||
tts_voice: string;
|
||||
tts_speed: number;
|
||||
tts_speed_enabled: boolean;
|
||||
tts_format: string;
|
||||
image_gen_api: string;
|
||||
image_gen_key: string;
|
||||
json_mode: boolean;
|
||||
logprobs: boolean;
|
||||
}
|
||||
|
||||
const _defaultAPIEndpoint = "/v1/chat/completions";
|
||||
export const newChatStore = (
|
||||
apiKey = "",
|
||||
systemMessageContent = "",
|
||||
apiEndpoint = _defaultAPIEndpoint,
|
||||
streamMode = true,
|
||||
model = defaultModel,
|
||||
temperature = 0.7,
|
||||
dev = false,
|
||||
whisper_api = "https://api.openai.com/v1/audio/transcriptions",
|
||||
whisper_key = "",
|
||||
tts_api = "https://api.openai.com/v1/audio/speech",
|
||||
tts_key = "",
|
||||
tts_speed = 1.0,
|
||||
tts_speed_enabled = false,
|
||||
tts_format = "mp3",
|
||||
toolsString = "",
|
||||
image_gen_api = "https://api.openai.com/v1/images/generations",
|
||||
image_gen_key = "",
|
||||
json_mode = false,
|
||||
logprobs = true
|
||||
): ChatStore => {
|
||||
return {
|
||||
chatgpt_api_web_version: CHATGPT_API_WEB_VERSION,
|
||||
systemMessageContent: getDefaultParams("sys", systemMessageContent),
|
||||
toolsString,
|
||||
history: [],
|
||||
postBeginIndex: 0,
|
||||
tokenMargin: 1024,
|
||||
totalTokens: 0,
|
||||
maxTokens: getDefaultParams(
|
||||
"max",
|
||||
models[getDefaultParams("model", model)]?.maxToken ?? 2048
|
||||
),
|
||||
maxGenTokens: 2048,
|
||||
maxGenTokens_enabled: true,
|
||||
apiKey: getDefaultParams("key", apiKey),
|
||||
apiEndpoint: getDefaultParams("api", apiEndpoint),
|
||||
streamMode: getDefaultParams("mode", streamMode),
|
||||
model: getDefaultParams("model", model),
|
||||
responseModelName: "",
|
||||
cost: 0,
|
||||
temperature: getDefaultParams("temp", temperature),
|
||||
temperature_enabled: true,
|
||||
top_p: 1,
|
||||
top_p_enabled: false,
|
||||
presence_penalty: 0,
|
||||
frequency_penalty: 0,
|
||||
develop_mode: getDefaultParams("dev", dev),
|
||||
whisper_api: getDefaultParams("whisper-api", whisper_api),
|
||||
whisper_key: getDefaultParams("whisper-key", whisper_key),
|
||||
tts_api: getDefaultParams("tts-api", tts_api),
|
||||
tts_key: getDefaultParams("tts-key", tts_key),
|
||||
tts_voice: "alloy",
|
||||
tts_speed: tts_speed,
|
||||
tts_speed_enabled: tts_speed_enabled,
|
||||
image_gen_api: image_gen_api,
|
||||
image_gen_key: image_gen_key,
|
||||
json_mode: json_mode,
|
||||
tts_format: tts_format,
|
||||
logprobs,
|
||||
};
|
||||
};
|
||||
|
||||
export const STORAGE_NAME = "chatgpt-api-web";
|
||||
const STORAGE_NAME_SELECTED = `${STORAGE_NAME}-selected`;
|
||||
const STORAGE_NAME_INDEXES = `${STORAGE_NAME}-indexes`;
|
||||
const STORAGE_NAME_TOTALCOST = `${STORAGE_NAME}-totalcost`;
|
||||
export const STORAGE_NAME_TEMPLATE = `${STORAGE_NAME}-template`;
|
||||
export const STORAGE_NAME_TEMPLATE_API = `${STORAGE_NAME_TEMPLATE}-api`;
|
||||
export const STORAGE_NAME_TEMPLATE_API_WHISPER = `${STORAGE_NAME_TEMPLATE}-api-whisper`;
|
||||
export const STORAGE_NAME_TEMPLATE_API_TTS = `${STORAGE_NAME_TEMPLATE}-api-tts`;
|
||||
export const STORAGE_NAME_TEMPLATE_API_IMAGE_GEN = `${STORAGE_NAME_TEMPLATE}-api-image-gen`;
|
||||
export const STORAGE_NAME_TEMPLATE_TOOLS = `${STORAGE_NAME_TEMPLATE}-tools`;
|
||||
|
||||
export function addTotalCost(cost: number) {
|
||||
let totalCost = getTotalCost();
|
||||
totalCost += cost;
|
||||
localStorage.setItem(STORAGE_NAME_TOTALCOST, `${totalCost}`);
|
||||
}
|
||||
|
||||
export function getTotalCost(): number {
|
||||
let totalCost = parseFloat(
|
||||
localStorage.getItem(STORAGE_NAME_TOTALCOST) ?? "0"
|
||||
);
|
||||
return totalCost;
|
||||
}
|
||||
|
||||
export function clearTotalCost() {
|
||||
localStorage.setItem(STORAGE_NAME_TOTALCOST, `0`);
|
||||
}
|
||||
|
||||
export function App() {
|
||||
// init selected index
|
||||
const [selectedChatIndex, setSelectedChatIndex] = useState(
|
||||
parseInt(localStorage.getItem(STORAGE_NAME_SELECTED) ?? "1")
|
||||
);
|
||||
console.log("selectedChatIndex", selectedChatIndex);
|
||||
useEffect(() => {
|
||||
console.log("set selected chat index", selectedChatIndex);
|
||||
localStorage.setItem(STORAGE_NAME_SELECTED, `${selectedChatIndex}`);
|
||||
}, [selectedChatIndex]);
|
||||
|
||||
const db = openDB<ChatStore>(STORAGE_NAME, 1, {
|
||||
upgrade(db) {
|
||||
const store = db.createObjectStore(STORAGE_NAME, {
|
||||
autoIncrement: true,
|
||||
});
|
||||
|
||||
// copy from localStorage to indexedDB
|
||||
const allChatStoreIndexes: number[] = JSON.parse(
|
||||
localStorage.getItem(STORAGE_NAME_INDEXES) ?? "[]"
|
||||
);
|
||||
let keyCount = 0;
|
||||
for (const i of allChatStoreIndexes) {
|
||||
console.log("importing chatStore from localStorage", i);
|
||||
const key = `${STORAGE_NAME}-${i}`;
|
||||
const val = localStorage.getItem(key);
|
||||
if (val === null) continue;
|
||||
store.add(JSON.parse(val));
|
||||
keyCount += 1;
|
||||
}
|
||||
setSelectedChatIndex(keyCount);
|
||||
if (keyCount > 0) {
|
||||
alert(
|
||||
"v2.0.0 Update: Imported chat history from localStorage to indexedDB. 🎉"
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const getChatStoreByIndex = async (index: number): Promise<ChatStore> => {
|
||||
const ret: ChatStore = await (await db).get(STORAGE_NAME, index);
|
||||
if (ret === null || ret === undefined) return newChatStore();
|
||||
// handle read from old version chatstore
|
||||
if (ret.maxGenTokens === undefined) ret.maxGenTokens = 2048;
|
||||
if (ret.maxGenTokens_enabled === undefined) ret.maxGenTokens_enabled = true;
|
||||
if (ret.model === undefined) ret.model = defaultModel;
|
||||
if (ret.responseModelName === undefined) ret.responseModelName = "";
|
||||
if (ret.toolsString === undefined) ret.toolsString = "";
|
||||
if (ret.chatgpt_api_web_version === undefined)
|
||||
// this is from old version becasue it is undefined,
|
||||
// so no higher than v1.3.0
|
||||
ret.chatgpt_api_web_version = "v1.2.2";
|
||||
for (const message of ret.history) {
|
||||
if (message.hide === undefined) message.hide = false;
|
||||
if (message.token === undefined)
|
||||
message.token = calculate_token_length(message.content);
|
||||
}
|
||||
if (ret.cost === undefined) ret.cost = 0;
|
||||
return ret;
|
||||
};
|
||||
|
||||
const [chatStore, _setChatStore] = useState(newChatStore());
|
||||
const setChatStore = async (chatStore: ChatStore) => {
|
||||
console.log("recalculate postBeginIndex");
|
||||
const max = chatStore.maxTokens - chatStore.tokenMargin;
|
||||
let sum = 0;
|
||||
chatStore.postBeginIndex = chatStore.history.filter(
|
||||
({ hide }) => !hide
|
||||
).length;
|
||||
for (const msg of chatStore.history
|
||||
.filter(({ hide }) => !hide)
|
||||
.slice()
|
||||
.reverse()) {
|
||||
if (sum + msg.token > max) break;
|
||||
sum += msg.token;
|
||||
chatStore.postBeginIndex -= 1;
|
||||
}
|
||||
chatStore.postBeginIndex =
|
||||
chatStore.postBeginIndex < 0 ? 0 : chatStore.postBeginIndex;
|
||||
|
||||
// manually estimate token
|
||||
chatStore.totalTokens = calculate_token_length(
|
||||
chatStore.systemMessageContent
|
||||
);
|
||||
for (const msg of chatStore.history
|
||||
.filter(({ hide }) => !hide)
|
||||
.slice(chatStore.postBeginIndex)) {
|
||||
chatStore.totalTokens += msg.token;
|
||||
}
|
||||
|
||||
console.log("saved chat", selectedChatIndex, chatStore);
|
||||
(await db).put(STORAGE_NAME, chatStore, selectedChatIndex);
|
||||
|
||||
_setChatStore(chatStore);
|
||||
};
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
_setChatStore(await getChatStoreByIndex(selectedChatIndex));
|
||||
};
|
||||
run();
|
||||
}, [selectedChatIndex]);
|
||||
|
||||
// all chat store indexes
|
||||
const [allChatStoreIndexes, setAllChatStoreIndexes] = useState<IDBValidKey>(
|
||||
[]
|
||||
);
|
||||
|
||||
const handleNewChatStoreWithOldOne = async (chatStore: ChatStore) => {
|
||||
const newKey = await (
|
||||
await db
|
||||
).add(
|
||||
STORAGE_NAME,
|
||||
newChatStore(
|
||||
chatStore.apiKey,
|
||||
chatStore.systemMessageContent,
|
||||
chatStore.apiEndpoint,
|
||||
chatStore.streamMode,
|
||||
chatStore.model,
|
||||
chatStore.temperature,
|
||||
!!chatStore.develop_mode,
|
||||
chatStore.whisper_api,
|
||||
chatStore.whisper_key,
|
||||
chatStore.tts_api,
|
||||
chatStore.tts_key,
|
||||
chatStore.tts_speed,
|
||||
chatStore.tts_speed_enabled,
|
||||
chatStore.tts_format,
|
||||
chatStore.toolsString,
|
||||
chatStore.image_gen_api,
|
||||
chatStore.image_gen_key,
|
||||
chatStore.json_mode,
|
||||
chatStore.logprobs
|
||||
)
|
||||
);
|
||||
setSelectedChatIndex(newKey as number);
|
||||
setAllChatStoreIndexes(await (await db).getAllKeys(STORAGE_NAME));
|
||||
};
|
||||
const handleNewChatStore = async () => {
|
||||
return handleNewChatStoreWithOldOne(chatStore);
|
||||
};
|
||||
|
||||
// if there are any params in URL, create a new chatStore
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
const chatStore = await getChatStoreByIndex(selectedChatIndex);
|
||||
const api = getDefaultParams("api", "");
|
||||
const key = getDefaultParams("key", "");
|
||||
const sys = getDefaultParams("sys", "");
|
||||
const mode = getDefaultParams("mode", "");
|
||||
const model = getDefaultParams("model", "");
|
||||
const max = getDefaultParams("max", 0);
|
||||
console.log("max is", max, "chatStore.max is", chatStore.maxTokens);
|
||||
// only create new chatStore if the params in URL are NOT
|
||||
// equal to the current selected chatStore
|
||||
if (
|
||||
(api && api !== chatStore.apiEndpoint) ||
|
||||
(key && key !== chatStore.apiKey) ||
|
||||
(sys && sys !== chatStore.systemMessageContent) ||
|
||||
(mode && mode !== (chatStore.streamMode ? "stream" : "fetch")) ||
|
||||
(model && model !== chatStore.model) ||
|
||||
(max !== 0 && max !== chatStore.maxTokens)
|
||||
) {
|
||||
console.log("create new chatStore because of params in URL");
|
||||
handleNewChatStoreWithOldOne(chatStore);
|
||||
}
|
||||
await db;
|
||||
const allidx = await (await db).getAllKeys(STORAGE_NAME);
|
||||
if (allidx.length === 0) {
|
||||
handleNewChatStore();
|
||||
}
|
||||
setAllChatStoreIndexes(await (await db).getAllKeys(STORAGE_NAME));
|
||||
};
|
||||
run();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex text-sm h-full bg-slate-200 dark:bg-slate-800 dark:text-white">
|
||||
<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">
|
||||
<button
|
||||
className="bg-violet-300 p-1 rounded hover:bg-violet-400"
|
||||
onClick={handleNewChatStore}
|
||||
>
|
||||
{Tr("NEW")}
|
||||
</button>
|
||||
<ul>
|
||||
{(allChatStoreIndexes as number[])
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((i) => {
|
||||
// reverse
|
||||
return (
|
||||
<li>
|
||||
<button
|
||||
className={`w-full my-1 p-1 rounded hover:bg-blue-500 ${
|
||||
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={async () => {
|
||||
if (!confirm("Are you sure you want to delete this chat history?"))
|
||||
return;
|
||||
console.log("remove item", `${STORAGE_NAME}-${selectedChatIndex}`);
|
||||
(await db).delete(STORAGE_NAME, selectedChatIndex);
|
||||
const newAllChatStoreIndexes = await (
|
||||
await db
|
||||
).getAllKeys(STORAGE_NAME);
|
||||
|
||||
if (newAllChatStoreIndexes.length === 0) {
|
||||
handleNewChatStore();
|
||||
return;
|
||||
}
|
||||
|
||||
// find nex selected chat index
|
||||
const next =
|
||||
newAllChatStoreIndexes[newAllChatStoreIndexes.length - 1];
|
||||
console.log("next is", next);
|
||||
setSelectedChatIndex(next as number);
|
||||
setAllChatStoreIndexes(newAllChatStoreIndexes);
|
||||
}}
|
||||
>
|
||||
{Tr("DEL")}
|
||||
</button>
|
||||
{chatStore.develop_mode && (
|
||||
<button
|
||||
className="rounded bg-rose-800 p-1 my-1 w-full text-white"
|
||||
onClick={async () => {
|
||||
if (
|
||||
!confirm(
|
||||
"Are you sure you want to delete **ALL** chat history?"
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
await (await db).clear(STORAGE_NAME);
|
||||
setAllChatStoreIndexes([]);
|
||||
setSelectedChatIndex(1);
|
||||
window.location.reload();
|
||||
}}
|
||||
>
|
||||
{Tr("CLS")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<ChatBOX
|
||||
chatStore={chatStore}
|
||||
setChatStore={setChatStore}
|
||||
selectedChatIndex={selectedChatIndex}
|
||||
setSelectedChatIndex={setSelectedChatIndex}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { DefaultModel } from "@/const";
|
||||
|
||||
export interface ImageURL {
|
||||
url: string;
|
||||
detail: "low" | "high";
|
||||
@@ -108,7 +110,7 @@ function calculate_token_length_from_text(text: string): number {
|
||||
}
|
||||
// https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them
|
||||
export function calculate_token_length(
|
||||
content: string | MessageDetail[]
|
||||
content: string | MessageDetail[],
|
||||
): number {
|
||||
if (typeof content === "string") {
|
||||
return calculate_token_length_from_text(content);
|
||||
@@ -155,15 +157,15 @@ class Chat {
|
||||
enable_max_gen_tokens = true,
|
||||
tokens_margin = 1024,
|
||||
apiEndPoint = "https://api.openai.com/v1/chat/completions",
|
||||
model = "gpt-3.5-turbo",
|
||||
temperature = 0.7,
|
||||
model = DefaultModel,
|
||||
temperature = 1,
|
||||
enable_temperature = true,
|
||||
top_p = 1,
|
||||
enable_top_p = false,
|
||||
presence_penalty = 0,
|
||||
frequency_penalty = 0,
|
||||
json_mode = false,
|
||||
} = {}
|
||||
} = {},
|
||||
) {
|
||||
this.OPENAI_API_KEY = OPENAI_API_KEY ?? "";
|
||||
this.messages = [];
|
||||
@@ -198,14 +200,14 @@ class Chat {
|
||||
}
|
||||
if (msg.role === "system") {
|
||||
console.log(
|
||||
"Warning: detected system message in the middle of history"
|
||||
"Warning: detected system message in the middle of history",
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const msg of this.messages) {
|
||||
if (msg.name && msg.role !== "system") {
|
||||
console.log(
|
||||
"Warning: detected message where name field set but role is system"
|
||||
"Warning: detected message where name field set but role is system",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
204
src/components/StatusBar.tsx
Normal file
204
src/components/StatusBar.tsx
Normal file
@@ -0,0 +1,204 @@
|
||||
import {
|
||||
CubeIcon,
|
||||
BanknotesIcon,
|
||||
ChatBubbleLeftEllipsisIcon,
|
||||
ScissorsIcon,
|
||||
SwatchIcon,
|
||||
SparklesIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
|
||||
import { ChatStore } from "@/types/chatstore";
|
||||
import { models } from "@/types/models";
|
||||
import { Tr } from "@/translate";
|
||||
import { getTotalCost } from "@/utils/totalCost";
|
||||
|
||||
const StatusBar = (props: {
|
||||
chatStore: ChatStore;
|
||||
setShowSettings: (show: boolean) => void;
|
||||
setShowSearch: (show: boolean) => void;
|
||||
}) => {
|
||||
const { chatStore, setShowSettings, setShowSearch } = props;
|
||||
return (
|
||||
<div className="navbar bg-base-100 p-0">
|
||||
<div className="navbar-start">
|
||||
<div className="dropdown lg:hidden">
|
||||
<div tabindex={0} role="button" className="btn btn-ghost btn-circle">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<ul
|
||||
tabindex={0}
|
||||
className="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow"
|
||||
>
|
||||
<li>
|
||||
<p>
|
||||
<ChatBubbleLeftEllipsisIcon className="h-4 w-4" />
|
||||
Tokens: {chatStore.totalTokens}/{chatStore.maxTokens}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<ScissorsIcon className="h-4 w-4" />
|
||||
Cut:
|
||||
{chatStore.postBeginIndex}/
|
||||
{chatStore.history.filter(({ hide }) => !hide).length}
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<p>
|
||||
<BanknotesIcon className="h-4 w-4" />
|
||||
Cost: ${chatStore.cost.toFixed(4)}
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="navbar-center cursor-pointer py-1"
|
||||
onClick={() => {
|
||||
setShowSettings(true);
|
||||
}}
|
||||
>
|
||||
{/* the long staus bar */}
|
||||
<div className="stats shadow hidden lg:inline-grid">
|
||||
<div className="stat">
|
||||
<div className="stat-figure text-secondary">
|
||||
<CubeIcon className="h-10 w-10" />
|
||||
</div>
|
||||
<div className="stat-title">Model</div>
|
||||
<div className="stat-value text-base">{chatStore.model}</div>
|
||||
<div className="stat-desc">
|
||||
{models[chatStore.model]?.price?.prompt * 1000 * 1000} $/M tokens
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="stat-figure text-secondary">
|
||||
<SwatchIcon className="h-10 w-10" />
|
||||
</div>
|
||||
<div className="stat-title">Mode</div>
|
||||
<div className="stat-value text-base">
|
||||
{chatStore.streamMode ? Tr("STREAM") : Tr("FETCH")}
|
||||
</div>
|
||||
<div className="stat-desc">STREAM/FETCH</div>
|
||||
</div>
|
||||
|
||||
<div className="stat">
|
||||
<div className="stat-figure text-secondary">
|
||||
<ChatBubbleLeftEllipsisIcon className="h-10 w-10" />
|
||||
</div>
|
||||
<div className="stat-title">Tokens</div>
|
||||
<div className="stat-value text-base">{chatStore.totalTokens}</div>
|
||||
<div className="stat-desc">Max: {chatStore.maxTokens}</div>
|
||||
</div>
|
||||
|
||||
<div className="stat">
|
||||
<div className="stat-figure text-secondary">
|
||||
<ScissorsIcon className="h-10 w-10" />
|
||||
</div>
|
||||
<div className="stat-title">Cut</div>
|
||||
<div className="stat-value text-base">
|
||||
{chatStore.postBeginIndex}
|
||||
</div>
|
||||
<div className="stat-desc">
|
||||
Max: {chatStore.history.filter(({ hide }) => !hide).length}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat">
|
||||
<div className="stat-figure text-secondary">
|
||||
<BanknotesIcon className="h-10 w-10" />
|
||||
</div>
|
||||
<div className="stat-title">Cost</div>
|
||||
<div className="stat-value text-base">
|
||||
${chatStore.cost.toFixed(4)}
|
||||
</div>
|
||||
<div className="stat-desc">
|
||||
Accumulated: ${getTotalCost().toFixed(2)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* the short status bar */}
|
||||
<div className="indicator lg:hidden">
|
||||
{chatStore.totalTokens !== 0 && (
|
||||
<span className="indicator-item badge badge-primary">
|
||||
Tokens: {chatStore.totalTokens}
|
||||
</span>
|
||||
)}
|
||||
<a className="btn btn-ghost text-base sm:text-xl p-0">
|
||||
<SparklesIcon className="h-4 w-4 hidden sm:block" />
|
||||
{chatStore.model}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="navbar-end">
|
||||
<button
|
||||
className="btn btn-ghost btn-circle"
|
||||
onClick={(event) => {
|
||||
// stop propagation to parent
|
||||
event.stopPropagation();
|
||||
|
||||
setShowSearch(true);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="h-6 w-6"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-ghost btn-circle hidden sm:block"
|
||||
onClick={() => setShowSettings(true)}
|
||||
>
|
||||
<div className="indicator">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="1.5"
|
||||
stroke="currentColor"
|
||||
className="h-6 w-6"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.325.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 0 1 1.37.49l1.296 2.247a1.125 1.125 0 0 1-.26 1.431l-1.003.827c-.293.241-.438.613-.43.992a7.723 7.723 0 0 1 0 .255c-.008.378.137.75.43.991l1.004.827c.424.35.534.955.26 1.43l-1.298 2.247a1.125 1.125 0 0 1-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.47 6.47 0 0 1-.22.128c-.331.183-.581.495-.644.869l-.213 1.281c-.09.543-.56.94-1.11.94h-2.594c-.55 0-1.019-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 0 1-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 0 1-1.369-.49l-1.297-2.247a1.125 1.125 0 0 1 .26-1.431l1.004-.827c.292-.24.437-.613.43-.991a6.932 6.932 0 0 1 0-.255c.007-.38-.138-.751-.43-.992l-1.004-.827a1.125 1.125 0 0 1-.26-1.43l1.297-2.247a1.125 1.125 0 0 1 1.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.086.22-.128.332-.183.582-.495.644-.869l.214-1.28Z"
|
||||
/>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span className="badge badge-xs badge-primary indicator-item"></span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default StatusBar;
|
||||
113
src/components/Templates.tsx
Normal file
113
src/components/Templates.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { TemplateChatStore } from "@/types/chatstore";
|
||||
import { ChatStore } from "@/types/chatstore";
|
||||
import { getDefaultParams } from "@/utils/getDefaultParam";
|
||||
|
||||
const Templates = (props: {
|
||||
templates: TemplateChatStore[];
|
||||
chatStore: ChatStore;
|
||||
setChatStore: (cs: ChatStore) => void;
|
||||
setTemplates: (templates: TemplateChatStore[]) => void;
|
||||
}) => {
|
||||
const { templates, chatStore, setChatStore, setTemplates } = props;
|
||||
return (
|
||||
<>
|
||||
{templates.map((t, index) => (
|
||||
<div
|
||||
className="cursor-pointer rounded bg-green-400 w-fit p-2 m-1 flex flex-col"
|
||||
onClick={() => {
|
||||
const newChatStore: ChatStore = structuredClone(t);
|
||||
// @ts-ignore
|
||||
delete newChatStore.name;
|
||||
if (!newChatStore.apiEndpoint) {
|
||||
newChatStore.apiEndpoint = getDefaultParams(
|
||||
"api",
|
||||
chatStore.apiEndpoint,
|
||||
);
|
||||
}
|
||||
if (!newChatStore.apiKey) {
|
||||
newChatStore.apiKey = getDefaultParams("key", chatStore.apiKey);
|
||||
}
|
||||
if (!newChatStore.whisper_api) {
|
||||
newChatStore.whisper_api = getDefaultParams(
|
||||
"whisper-api",
|
||||
chatStore.whisper_api,
|
||||
);
|
||||
}
|
||||
if (!newChatStore.whisper_key) {
|
||||
newChatStore.whisper_key = getDefaultParams(
|
||||
"whisper-key",
|
||||
chatStore.whisper_key,
|
||||
);
|
||||
}
|
||||
if (!newChatStore.tts_api) {
|
||||
newChatStore.tts_api = getDefaultParams(
|
||||
"tts-api",
|
||||
chatStore.tts_api,
|
||||
);
|
||||
}
|
||||
if (!newChatStore.tts_key) {
|
||||
newChatStore.tts_key = getDefaultParams(
|
||||
"tts-key",
|
||||
chatStore.tts_key,
|
||||
);
|
||||
}
|
||||
if (!newChatStore.image_gen_api) {
|
||||
newChatStore.image_gen_api = getDefaultParams(
|
||||
"image-gen-api",
|
||||
chatStore.image_gen_api,
|
||||
);
|
||||
}
|
||||
if (!newChatStore.image_gen_key) {
|
||||
newChatStore.image_gen_key = getDefaultParams(
|
||||
"image-gen-key",
|
||||
chatStore.image_gen_key,
|
||||
);
|
||||
}
|
||||
newChatStore.cost = 0;
|
||||
|
||||
// manage undefined value because of version update
|
||||
newChatStore.toolsString = newChatStore.toolsString || "";
|
||||
|
||||
setChatStore({ ...newChatStore });
|
||||
}}
|
||||
>
|
||||
<span className="w-full text-center">{t.name}</span>
|
||||
<hr className="mt-2" />
|
||||
<span className="flex justify-between">
|
||||
<button
|
||||
onClick={(event) => {
|
||||
// prevent triggert other event
|
||||
event.stopPropagation();
|
||||
|
||||
const name = prompt("Give template a name");
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
t.name = name;
|
||||
setTemplates(structuredClone(templates));
|
||||
}}
|
||||
>
|
||||
🖋
|
||||
</button>
|
||||
<button
|
||||
onClick={(event) => {
|
||||
// prevent triggert other event
|
||||
event.stopPropagation();
|
||||
|
||||
if (!confirm("Are you sure to delete this template?")) {
|
||||
return;
|
||||
}
|
||||
templates.splice(index, 1);
|
||||
setTemplates(structuredClone(templates));
|
||||
}}
|
||||
>
|
||||
❌
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Templates;
|
||||
49
src/components/VersionHint.tsx
Normal file
49
src/components/VersionHint.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ChatStore } from "@/types/chatstore";
|
||||
import { Tr } from "@/translate";
|
||||
|
||||
const VersionHint = (props: { chatStore: ChatStore }) => {
|
||||
const { chatStore } = props;
|
||||
return (
|
||||
<>
|
||||
{chatStore.chatgpt_api_web_version < "v1.3.0" && (
|
||||
<p className="p-2 my-2 text-center dark:text-white">
|
||||
<br />
|
||||
{Tr("Warning: current chatStore version")}:{" "}
|
||||
{chatStore.chatgpt_api_web_version} {"< v1.3.0"}
|
||||
<br />
|
||||
v1.3.0
|
||||
引入与旧版不兼容的消息裁切算法。继续使用旧版可能会导致消息裁切过多或过少(表现为失去上下文或输出不完整)。
|
||||
<br />
|
||||
请在左上角创建新会话:)
|
||||
</p>
|
||||
)}
|
||||
{chatStore.chatgpt_api_web_version < "v1.4.0" && (
|
||||
<p className="p-2 my-2 text-center dark:text-white">
|
||||
<br />
|
||||
{Tr("Warning: current chatStore version")}:{" "}
|
||||
{chatStore.chatgpt_api_web_version} {"< v1.4.0"}
|
||||
<br />
|
||||
v1.4.0 增加了更多参数,继续使用旧版可能因参数确实导致未定义的行为
|
||||
<br />
|
||||
请在左上角创建新会话:)
|
||||
</p>
|
||||
)}
|
||||
{chatStore.chatgpt_api_web_version < "v1.6.0" && (
|
||||
<p className="p-2 my-2 text-center dark:text-white">
|
||||
<br />
|
||||
提示:当前会话版本 {chatStore.chatgpt_api_web_version}
|
||||
{Tr("Warning: current chatStore version")}:{" "}
|
||||
{chatStore.chatgpt_api_web_version} {"< v1.6.0"}
|
||||
。
|
||||
<br />
|
||||
v1.6.0 开始保存会话模板时会将 apiKey 和 apiEndpoint
|
||||
设置为空,继续使用旧版可能在保存读取模板时出现问题
|
||||
<br />
|
||||
请在左上角创建新会话:)
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default VersionHint;
|
||||
132
src/components/WhisperButton.tsx
Normal file
132
src/components/WhisperButton.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { createRef } from "preact";
|
||||
|
||||
import { ChatStore } from "@/types/chatstore";
|
||||
import { StateUpdater, useEffect, useState, Dispatch } from "preact/hooks";
|
||||
|
||||
const WhisperButton = (props: {
|
||||
chatStore: ChatStore;
|
||||
inputMsg: string;
|
||||
setInputMsg: Dispatch<StateUpdater<string>>;
|
||||
}) => {
|
||||
const { chatStore, inputMsg, setInputMsg } = props;
|
||||
const mediaRef = createRef();
|
||||
const [isRecording, setIsRecording] = useState("Mic");
|
||||
return (
|
||||
<button
|
||||
className={`btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1 ${
|
||||
isRecording === "Recording" ? "btn-error" : "btn-success"
|
||||
} ${isRecording !== "Mic" ? "animate-pulse" : ""}`}
|
||||
disabled={isRecording === "Transcribing"}
|
||||
ref={mediaRef}
|
||||
onClick={async () => {
|
||||
if (isRecording === "Recording") {
|
||||
// @ts-ignore
|
||||
window.mediaRecorder.stop();
|
||||
setIsRecording("Transcribing");
|
||||
return;
|
||||
}
|
||||
|
||||
// build prompt
|
||||
const prompt = [chatStore.systemMessageContent]
|
||||
.concat(
|
||||
chatStore.history
|
||||
.filter(({ hide }) => !hide)
|
||||
.slice(chatStore.postBeginIndex)
|
||||
.map(({ content }) => {
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
} else {
|
||||
return content.map((c) => c?.text).join(" ");
|
||||
}
|
||||
}),
|
||||
)
|
||||
.concat([inputMsg])
|
||||
.join(" ");
|
||||
console.log({ prompt });
|
||||
|
||||
setIsRecording("Recording");
|
||||
console.log("start recording");
|
||||
|
||||
try {
|
||||
const mediaRecorder = new MediaRecorder(
|
||||
await navigator.mediaDevices.getUserMedia({
|
||||
audio: true,
|
||||
}),
|
||||
{ audioBitsPerSecond: 64 * 1000 },
|
||||
);
|
||||
|
||||
// mount mediaRecorder to ref
|
||||
// @ts-ignore
|
||||
window.mediaRecorder = mediaRecorder;
|
||||
|
||||
mediaRecorder.start();
|
||||
const audioChunks: Blob[] = [];
|
||||
mediaRecorder.addEventListener("dataavailable", (event) => {
|
||||
audioChunks.push(event.data);
|
||||
});
|
||||
mediaRecorder.addEventListener("stop", async () => {
|
||||
// Stop the MediaRecorder
|
||||
mediaRecorder.stop();
|
||||
// Stop the media stream
|
||||
mediaRecorder.stream.getTracks()[0].stop();
|
||||
|
||||
setIsRecording("Transcribing");
|
||||
const audioBlob = new Blob(audioChunks);
|
||||
const audioUrl = URL.createObjectURL(audioBlob);
|
||||
console.log({ audioUrl });
|
||||
const audio = new Audio(audioUrl);
|
||||
// audio.play();
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(audioBlob);
|
||||
|
||||
// file-like object with mimetype
|
||||
const blob = new Blob([audioBlob], {
|
||||
type: "application/octet-stream",
|
||||
});
|
||||
|
||||
reader.onloadend = async () => {
|
||||
try {
|
||||
const base64data = reader.result;
|
||||
|
||||
// post to openai whisper api
|
||||
const formData = new FormData();
|
||||
// append file
|
||||
formData.append("file", blob, "audio.ogg");
|
||||
formData.append("model", "whisper-1");
|
||||
formData.append("response_format", "text");
|
||||
formData.append("prompt", prompt);
|
||||
|
||||
const response = await fetch(chatStore.whisper_api, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${
|
||||
chatStore.whisper_key || chatStore.apiKey
|
||||
}`,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
|
||||
setInputMsg(inputMsg ? inputMsg + " " + text : text);
|
||||
} catch (error) {
|
||||
alert(error);
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsRecording("Mic");
|
||||
}
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
alert(error);
|
||||
console.log(error);
|
||||
setIsRecording("Mic");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isRecording}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default WhisperButton;
|
||||
14
src/const.ts
Normal file
14
src/const.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export const DefaultAPIEndpoint = "https://api.openai.com/v1/chat/completions";
|
||||
export const CHATGPT_API_WEB_VERSION = "v2.1.0";
|
||||
export const DefaultModel = "gpt-4o-mini";
|
||||
|
||||
export const STORAGE_NAME = "chatgpt-api-web";
|
||||
export const STORAGE_NAME_SELECTED = `${STORAGE_NAME}-selected`;
|
||||
export const STORAGE_NAME_INDEXES = `${STORAGE_NAME}-indexes`;
|
||||
export const STORAGE_NAME_TOTALCOST = `${STORAGE_NAME}-totalcost`;
|
||||
export const STORAGE_NAME_TEMPLATE = `${STORAGE_NAME}-template`;
|
||||
export const STORAGE_NAME_TEMPLATE_API = `${STORAGE_NAME_TEMPLATE}-api`;
|
||||
export const STORAGE_NAME_TEMPLATE_API_WHISPER = `${STORAGE_NAME_TEMPLATE}-api-whisper`;
|
||||
export const STORAGE_NAME_TEMPLATE_API_TTS = `${STORAGE_NAME_TEMPLATE}-api-tts`;
|
||||
export const STORAGE_NAME_TEMPLATE_API_IMAGE_GEN = `${STORAGE_NAME_TEMPLATE}-api-image-gen`;
|
||||
export const STORAGE_NAME_TEMPLATE_TOOLS = `${STORAGE_NAME_TEMPLATE}-tools`;
|
||||
@@ -1,15 +1,13 @@
|
||||
import { Tr, langCodeContext, LANG_OPTIONS, tr } from "./translate";
|
||||
import { useState, useEffect, StateUpdater } from "preact/hooks";
|
||||
import { ChatStore, ChatStoreMessage } from "./app";
|
||||
import { calculate_token_length, getMessageText } from "./chatgpt";
|
||||
import { isVailedJSON } from "./message";
|
||||
import { EditMessageString } from "./editMessageString";
|
||||
import { EditMessageDetail } from "./editMessageDetail";
|
||||
import { useState, useEffect, StateUpdater, Dispatch } from "preact/hooks";
|
||||
import { Tr, langCodeContext, LANG_OPTIONS, tr } from "@/translate";
|
||||
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
|
||||
import { EditMessageString } from "@/editMessageString";
|
||||
import { EditMessageDetail } from "@/editMessageDetail";
|
||||
|
||||
interface EditMessageProps {
|
||||
chat: ChatStoreMessage;
|
||||
chatStore: ChatStore;
|
||||
setShowEdit: StateUpdater<boolean>;
|
||||
setShowEdit: Dispatch<StateUpdater<boolean>>;
|
||||
setChatStore: (cs: ChatStore) => void;
|
||||
}
|
||||
export function EditMessage(props: EditMessageProps) {
|
||||
@@ -44,17 +42,29 @@ export function EditMessage(props: EditMessageProps) {
|
||||
/>
|
||||
)}
|
||||
<div className={"w-full flex justify-center"}>
|
||||
{chatStore.develop_mode && <button
|
||||
className="w-full m-2 p-1 rounded bg-red-500"
|
||||
onClick={() => {
|
||||
if (typeof chat.content === "string") {
|
||||
chat.content = []
|
||||
} else {
|
||||
chat.content = ''
|
||||
}
|
||||
setChatStore({ ...chatStore })
|
||||
}}
|
||||
>Switch to {typeof chat.content === 'string' ? "media message" : "string message"}</button>}
|
||||
{chatStore.develop_mode && (
|
||||
<button
|
||||
className="w-full m-2 p-1 rounded bg-red-500"
|
||||
onClick={() => {
|
||||
const confirm = window.confirm(
|
||||
"Change message type will clear the content, are you sure?",
|
||||
);
|
||||
if (!confirm) return;
|
||||
|
||||
if (typeof chat.content === "string") {
|
||||
chat.content = [];
|
||||
} else {
|
||||
chat.content = "";
|
||||
}
|
||||
setChatStore({ ...chatStore });
|
||||
}}
|
||||
>
|
||||
Switch to{" "}
|
||||
{typeof chat.content === "string"
|
||||
? "media message"
|
||||
: "string message"}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={"w-full m-2 p-1 rounded bg-purple-500"}
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChatStore, ChatStoreMessage } from "./app";
|
||||
import { calculate_token_length } from "./chatgpt";
|
||||
import { Tr } from "./translate";
|
||||
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
|
||||
import { calculate_token_length } from "@/chatgpt";
|
||||
import { Tr } from "@/translate";
|
||||
|
||||
interface Props {
|
||||
chat: ChatStoreMessage;
|
||||
@@ -22,10 +22,10 @@ export function EditMessageDetail({
|
||||
>
|
||||
{chat.content.map((mdt, index) => (
|
||||
<div className={"w-full p-2 px-4"}>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex justify-center">
|
||||
{mdt.type === "text" ? (
|
||||
<textarea
|
||||
className={"w-full"}
|
||||
className={"w-full border p-1 rounded"}
|
||||
value={mdt.text}
|
||||
onChange={(event: any) => {
|
||||
if (typeof chat.content === "string") return;
|
||||
@@ -41,16 +41,16 @@ export function EditMessageDetail({
|
||||
}}
|
||||
></textarea>
|
||||
) : (
|
||||
<>
|
||||
<div className="border p-1 rounded">
|
||||
<img
|
||||
className="max-h-32 max-w-xs cursor-pointer"
|
||||
className="max-h-32 max-w-xs cursor-pointer m-2"
|
||||
src={mdt.image_url?.url}
|
||||
onClick={() => {
|
||||
window.open(mdt.image_url?.url, "_blank");
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className="bg-blue-300 p-1 rounded"
|
||||
className="bg-blue-300 p-1 rounded m-1"
|
||||
onClick={() => {
|
||||
const image_url = prompt("image url", mdt.image_url?.url);
|
||||
if (image_url) {
|
||||
@@ -65,7 +65,7 @@ export function EditMessageDetail({
|
||||
{Tr("Edit URL")}
|
||||
</button>
|
||||
<button
|
||||
className="bg-blue-300 p-1 rounded"
|
||||
className="bg-blue-300 p-1 rounded m-1"
|
||||
onClick={() => {
|
||||
// select file and load it to base64 image URL format
|
||||
const input = document.createElement("input");
|
||||
@@ -95,7 +95,7 @@ export function EditMessageDetail({
|
||||
{Tr("Upload")}
|
||||
</button>
|
||||
<span
|
||||
className="bg-blue-300 p-1 rounded"
|
||||
className="bg-blue-300 p-1 rounded m-1"
|
||||
onClick={() => {
|
||||
if (typeof chat.content === "string") return;
|
||||
const obj = chat.content[index].image_url;
|
||||
@@ -111,7 +111,7 @@ export function EditMessageDetail({
|
||||
checked={mdt.image_url?.detail === "high"}
|
||||
/>
|
||||
</span>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChatStore, ChatStoreMessage } from "./app";
|
||||
import { isVailedJSON } from "./message";
|
||||
import { calculate_token_length } from "./chatgpt";
|
||||
import { Tr } from "./translate";
|
||||
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
|
||||
import { isVailedJSON } from "@/message";
|
||||
import { calculate_token_length } from "@/chatgpt";
|
||||
import { Tr } from "@/translate";
|
||||
|
||||
interface Props {
|
||||
chat: ChatStoreMessage;
|
||||
@@ -69,7 +69,7 @@ export function EditMessageString({
|
||||
onClick={() => {
|
||||
if (!chat.tool_calls) return;
|
||||
chat.tool_calls = chat.tool_calls.filter(
|
||||
(tc) => tc.id !== tool_call.id
|
||||
(tc) => tc.id !== tool_call.id,
|
||||
);
|
||||
setChatStore({ ...chatStore });
|
||||
}}
|
||||
|
||||
@@ -30,6 +30,7 @@ body::-webkit-scrollbar {
|
||||
|
||||
.message-content {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: anywhere;
|
||||
}
|
||||
|
||||
.markup > h2 {
|
||||
@@ -78,8 +79,14 @@ body::-webkit-scrollbar {
|
||||
white-space: break-space;
|
||||
background-color: rgba(175, 184, 193, 0.2);
|
||||
border-radius: 6px;
|
||||
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
|
||||
Liberation Mono, monospace;
|
||||
font-family:
|
||||
ui-monospace,
|
||||
SFMono-Regular,
|
||||
SF Mono,
|
||||
Menlo,
|
||||
Consolas,
|
||||
Liberation Mono,
|
||||
monospace;
|
||||
}
|
||||
|
||||
.markup > pre {
|
||||
@@ -138,3 +145,7 @@ body::-webkit-scrollbar {
|
||||
background-color: #f5f5f5;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.stat {
|
||||
padding: 0.39rem;
|
||||
}
|
||||
|
||||
24
src/indexedDB/upgrade.ts
Normal file
24
src/indexedDB/upgrade.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { STORAGE_NAME } from "@/const";
|
||||
import { ChatStore } from "@/types/chatstore";
|
||||
import { IDBPDatabase, IDBPTransaction, StoreNames } from "idb";
|
||||
import { upgradeV1 } from "@/indexedDB/v1";
|
||||
import { upgradeV11 } from "./v11";
|
||||
|
||||
export async function upgrade(
|
||||
db: IDBPDatabase<ChatStore>,
|
||||
oldVersion: number,
|
||||
newVersion: number,
|
||||
transaction: IDBPTransaction<
|
||||
ChatStore,
|
||||
StoreNames<ChatStore>[],
|
||||
"versionchange"
|
||||
>,
|
||||
) {
|
||||
if (oldVersion < 1) {
|
||||
upgradeV1(db, oldVersion, newVersion, transaction);
|
||||
}
|
||||
|
||||
if (oldVersion < 11) {
|
||||
upgradeV11(db, oldVersion, newVersion, transaction);
|
||||
}
|
||||
}
|
||||
38
src/indexedDB/v1.ts
Normal file
38
src/indexedDB/v1.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { STORAGE_NAME, STORAGE_NAME_INDEXES } from "@/const";
|
||||
import { ChatStore } from "@/types/chatstore";
|
||||
import { IDBPDatabase, IDBPTransaction, StoreNames } from "idb";
|
||||
|
||||
export async function upgradeV1(
|
||||
db: IDBPDatabase<ChatStore>,
|
||||
oldVersion: number,
|
||||
newVersion: number,
|
||||
transaction: IDBPTransaction<
|
||||
ChatStore,
|
||||
StoreNames<ChatStore>[],
|
||||
"versionchange"
|
||||
>,
|
||||
) {
|
||||
const store = db.createObjectStore(STORAGE_NAME, {
|
||||
autoIncrement: true,
|
||||
});
|
||||
|
||||
// copy from localStorage to indexedDB
|
||||
const allChatStoreIndexes: number[] = JSON.parse(
|
||||
localStorage.getItem(STORAGE_NAME_INDEXES) ?? "[]",
|
||||
);
|
||||
let keyCount = 0;
|
||||
for (const i of allChatStoreIndexes) {
|
||||
console.log("importing chatStore from localStorage", i);
|
||||
const key = `${STORAGE_NAME}-${i}`;
|
||||
const val = localStorage.getItem(key);
|
||||
if (val === null) continue;
|
||||
store.add(JSON.parse(val));
|
||||
keyCount += 1;
|
||||
}
|
||||
// setSelectedChatIndex(keyCount);
|
||||
if (keyCount > 0) {
|
||||
alert(
|
||||
"v2.0.0 Update: Imported chat history from localStorage to indexedDB. 🎉",
|
||||
);
|
||||
}
|
||||
}
|
||||
25
src/indexedDB/v11.ts
Normal file
25
src/indexedDB/v11.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { ChatStore } from "@/types/chatstore";
|
||||
import { STORAGE_NAME } from "@/const";
|
||||
import { IDBPDatabase, IDBPTransaction, StoreNames } from "idb";
|
||||
|
||||
export async function upgradeV11(
|
||||
db: IDBPDatabase<ChatStore>,
|
||||
oldVersion: number,
|
||||
newVersion: number,
|
||||
transaction: IDBPTransaction<
|
||||
ChatStore,
|
||||
StoreNames<ChatStore>[],
|
||||
"versionchange"
|
||||
>,
|
||||
) {
|
||||
if (oldVersion < 11 && oldVersion >= 1) {
|
||||
alert("Start upgrading storage, just a sec... (Click OK to continue)");
|
||||
}
|
||||
if (
|
||||
transaction
|
||||
.objectStore(STORAGE_NAME)
|
||||
.indexNames.contains("contents_for_index")
|
||||
) {
|
||||
transaction.objectStore(STORAGE_NAME).deleteIndex("contents_for_index");
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChatStore, TemplateAPI } from "./app";
|
||||
import { Tr } from "./translate";
|
||||
import { ChatStore, TemplateAPI } from "@/types/chatstore";
|
||||
import { Tr } from "@/translate";
|
||||
|
||||
interface Props {
|
||||
chatStore: ChatStore;
|
||||
@@ -20,7 +20,7 @@ export function ListAPIs({
|
||||
keyField,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="break-all opacity-80 p-3 rounded bg-white my-3 text-left dark:text-black">
|
||||
<div className="break-all opacity-80 p-3 rounded base-200 my-3 text-left">
|
||||
<h2>{Tr(`Saved ${label} templates`)}</h2>
|
||||
<hr className="my-2" />
|
||||
<div className="flex flex-wrap">
|
||||
@@ -31,8 +31,8 @@ export function ListAPIs({
|
||||
chatStore[apiField] === t.endpoint &&
|
||||
// @ts-ignore
|
||||
chatStore[keyField] === t.key
|
||||
? "bg-red-600"
|
||||
: "bg-red-400"
|
||||
? "bg-info"
|
||||
: "bg-base-300"
|
||||
} w-fit p-2 m-1 flex flex-col`}
|
||||
onClick={() => {
|
||||
// @ts-ignore
|
||||
@@ -43,9 +43,9 @@ export function ListAPIs({
|
||||
}}
|
||||
>
|
||||
<span className="w-full text-center">{t.name}</span>
|
||||
<hr className="mt-2" />
|
||||
<span className="flex justify-between">
|
||||
<span className="flex justify-between gap-x-2">
|
||||
<button
|
||||
className="link"
|
||||
onClick={() => {
|
||||
const name = prompt(`Give **${label}** template a name`);
|
||||
if (!name) {
|
||||
@@ -55,13 +55,14 @@ export function ListAPIs({
|
||||
setTmps(structuredClone(tmps));
|
||||
}}
|
||||
>
|
||||
🖋
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
className="link"
|
||||
onClick={() => {
|
||||
if (
|
||||
!confirm(
|
||||
`Are you sure to delete this **${label}** template?`
|
||||
`Are you sure to delete this **${label}** template?`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
@@ -70,7 +71,7 @@ export function ListAPIs({
|
||||
setTmps(structuredClone(tmps));
|
||||
}}
|
||||
>
|
||||
❌
|
||||
Delete
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChatStore, TemplateTools } from "./app";
|
||||
import { Tr } from "./translate";
|
||||
import { ChatStore, TemplateTools } from "@/types/chatstore";
|
||||
import { Tr } from "@/translate";
|
||||
|
||||
interface Props {
|
||||
templateTools: TemplateTools[];
|
||||
@@ -33,8 +33,8 @@ export function ListToolsTempaltes({
|
||||
<div
|
||||
className={`cursor-pointer rounded ${
|
||||
chatStore.toolsString === t.toolsString
|
||||
? "bg-red-600"
|
||||
: "bg-red-400"
|
||||
? "bg-info"
|
||||
: "bg-base-300"
|
||||
} w-fit p-2 m-1 flex flex-col`}
|
||||
onClick={() => {
|
||||
chatStore.toolsString = t.toolsString;
|
||||
@@ -42,9 +42,9 @@ export function ListToolsTempaltes({
|
||||
}}
|
||||
>
|
||||
<span className="w-full text-center">{t.name}</span>
|
||||
<hr className="mt-2" />
|
||||
<span className="flex justify-between">
|
||||
<span className="flex justify-between gap-x-2">
|
||||
<button
|
||||
className="link"
|
||||
onClick={() => {
|
||||
const name = prompt(`Give **tools** template a name`);
|
||||
if (!name) {
|
||||
@@ -54,9 +54,10 @@ export function ListToolsTempaltes({
|
||||
setTemplateTools(structuredClone(templateTools));
|
||||
}}
|
||||
>
|
||||
🖋
|
||||
Edit
|
||||
</button>
|
||||
<button
|
||||
className="link"
|
||||
onClick={() => {
|
||||
if (
|
||||
!confirm(`Are you sure to delete this **tools** template?`)
|
||||
@@ -67,7 +68,7 @@ export function ListToolsTempaltes({
|
||||
setTemplateTools(structuredClone(templateTools));
|
||||
}}
|
||||
>
|
||||
❌
|
||||
Delete
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ const logprobToColor = (logprob: number) => {
|
||||
// 绿色的RGB值为(0, 255, 0),红色的RGB值为(255, 0, 0)
|
||||
const red = Math.round(255 * (1 - percent / 100));
|
||||
const green = Math.round(255 * (percent / 100));
|
||||
const color = `rgb(${red}, ${green}, 0)`;
|
||||
const color = `rgba(${red}, ${green}, 0, 0.5)`;
|
||||
|
||||
return color;
|
||||
};
|
||||
|
||||
22
src/main.tsx
22
src/main.tsx
@@ -1,27 +1,29 @@
|
||||
import { themeChange } from "theme-change";
|
||||
import { render } from "preact";
|
||||
import { App } from "./app";
|
||||
import { useState, useEffect } from "preact/hooks";
|
||||
import { Tr, langCodeContext, LANG_OPTIONS } from "./translate";
|
||||
import { App } from "@/pages/App";
|
||||
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
||||
|
||||
function Base() {
|
||||
const [langCode, _setLangCode] = useState("en-US");
|
||||
|
||||
const setLangCode = (langCode: string) => {
|
||||
_setLangCode(langCode)
|
||||
if (!localStorage) return
|
||||
_setLangCode(langCode);
|
||||
if (!localStorage) return;
|
||||
|
||||
localStorage.setItem('chatgpt-api-web-lang', langCode)
|
||||
}
|
||||
localStorage.setItem("chatgpt-api-web-lang", langCode);
|
||||
};
|
||||
|
||||
// select language
|
||||
useEffect(() => {
|
||||
themeChange(false);
|
||||
// query localStorage
|
||||
if (localStorage) {
|
||||
const lang = localStorage.getItem('chatgpt-api-web-lang')
|
||||
const lang = localStorage.getItem("chatgpt-api-web-lang");
|
||||
if (lang) {
|
||||
console.log(`query langCode ${lang} from localStorage`)
|
||||
_setLangCode(lang)
|
||||
return
|
||||
console.log(`query langCode ${lang} from localStorage`);
|
||||
_setLangCode(lang);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
184
src/message.tsx
184
src/message.tsx
@@ -1,15 +1,17 @@
|
||||
import { Tr, langCodeContext, LANG_OPTIONS } from "./translate";
|
||||
import { useState, useEffect, StateUpdater } from "preact/hooks";
|
||||
import { ChatStore, ChatStoreMessage } from "./app";
|
||||
import { calculate_token_length, getMessageText } from "./chatgpt";
|
||||
import { XMarkIcon } from "@heroicons/react/24/outline";
|
||||
import Markdown from "preact-markdown";
|
||||
import TTSButton, { TTSPlay } from "./tts";
|
||||
import { MessageHide } from "./messageHide";
|
||||
import { MessageDetail } from "./messageDetail";
|
||||
import { MessageToolCall } from "./messageToolCall";
|
||||
import { MessageToolResp } from "./messageToolResp";
|
||||
import { EditMessage } from "./editMessage";
|
||||
import logprobToColor from "./logprob";
|
||||
import { useState, useEffect, StateUpdater } from "preact/hooks";
|
||||
|
||||
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
||||
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
|
||||
import { calculate_token_length, getMessageText } from "@/chatgpt";
|
||||
import TTSButton, { TTSPlay } from "@/tts";
|
||||
import { MessageHide } from "@/messageHide";
|
||||
import { MessageDetail } from "@/messageDetail";
|
||||
import { MessageToolCall } from "@/messageToolCall";
|
||||
import { MessageToolResp } from "@/messageToolResp";
|
||||
import { EditMessage } from "@/editMessage";
|
||||
import logprobToColor from "@/logprob";
|
||||
|
||||
export const isVailedJSON = (str: string): boolean => {
|
||||
try {
|
||||
@@ -24,7 +26,6 @@ interface Props {
|
||||
messageIndex: number;
|
||||
chatStore: ChatStore;
|
||||
setChatStore: (cs: ChatStore) => void;
|
||||
update_total_tokens: () => void;
|
||||
}
|
||||
|
||||
export default function Message(props: Props) {
|
||||
@@ -51,17 +52,26 @@ export default function Message(props: Props) {
|
||||
setChatStore({ ...chatStore });
|
||||
}}
|
||||
>
|
||||
🗑️
|
||||
Delete
|
||||
</button>
|
||||
);
|
||||
const CopiedHint = () => (
|
||||
<span
|
||||
className={
|
||||
"bg-purple-400 p-1 rounded shadow-md absolute z-20 left-1/2 top-3/4 transform -translate-x-1/2 -translate-y-1/2"
|
||||
}
|
||||
>
|
||||
{Tr("Message copied to clipboard!")}
|
||||
</span>
|
||||
<div role="alert" className="alert">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
className="stroke-info h-6 w-6 shrink-0"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
></path>
|
||||
</svg>
|
||||
<span>{Tr("Message copied to clipboard!")}</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
@@ -78,7 +88,7 @@ export default function Message(props: Props) {
|
||||
copyToClipboard(textToCopy);
|
||||
}}
|
||||
>
|
||||
📋
|
||||
Copy
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
@@ -92,8 +102,8 @@ export default function Message(props: Props) {
|
||||
chatStore.history.slice(0, messageIndex).filter(({ hide }) => !hide)
|
||||
.length && (
|
||||
<div className="flex items-center relative justify-center">
|
||||
<hr className="w-full h-px my-4 border-0 bg-slate-800 dark:bg-white" />
|
||||
<span className="absolute px-3 bg-slate-800 text-white rounded p-1 dark:bg-white dark:text-black">
|
||||
<hr className="w-full h-px my-4 border-0" />
|
||||
<span className="absolute px-3 rounded p-1">
|
||||
Above messages are "forgotten"
|
||||
</span>
|
||||
</div>
|
||||
@@ -103,53 +113,66 @@ export default function Message(props: Props) {
|
||||
chat.role === "assistant" ? "justify-start" : "justify-end"
|
||||
}`}
|
||||
>
|
||||
<div>
|
||||
<div className={`w-full`}>
|
||||
<div
|
||||
className={`w-fit p-2 rounded my-2 ${
|
||||
chat.role === "assistant"
|
||||
? "bg-white dark:bg-gray-700 dark:text-white"
|
||||
: "bg-green-400"
|
||||
className={`chat min-w-16 p-2 my-2 ${
|
||||
chat.role === "assistant" ? "chat-start" : "chat-end"
|
||||
} ${chat.hide ? "opacity-50" : ""}`}
|
||||
>
|
||||
{chat.hide ? (
|
||||
<MessageHide chat={chat} />
|
||||
) : typeof chat.content !== "string" ? (
|
||||
<MessageDetail chat={chat} renderMarkdown={renderMarkdown} />
|
||||
) : chat.tool_calls ? (
|
||||
<MessageToolCall chat={chat} copyToClipboard={copyToClipboard} />
|
||||
) : chat.role === "tool" ? (
|
||||
<MessageToolResp chat={chat} copyToClipboard={copyToClipboard} />
|
||||
) : renderMarkdown ? (
|
||||
// @ts-ignore
|
||||
<Markdown markdown={getMessageText(chat)} />
|
||||
) : (
|
||||
<div className="message-content">
|
||||
{
|
||||
// only show when content is string or list of message
|
||||
// this check is used to avoid rendering tool call
|
||||
chat.content &&
|
||||
(chat.logprobs && renderColor
|
||||
? chat.logprobs.content
|
||||
.filter((c) => c.token)
|
||||
.map((c) => (
|
||||
<div
|
||||
style={{
|
||||
color: logprobToColor(c.logprob),
|
||||
display: "inline",
|
||||
}}
|
||||
>
|
||||
{c.token}
|
||||
</div>
|
||||
))
|
||||
: getMessageText(chat))
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
<hr className="mt-2" />
|
||||
<TTSPlay chat={chat} />
|
||||
<div className="w-full flex justify-between">
|
||||
<div
|
||||
className={`chat-bubble max-w-full ${
|
||||
chat.role === "assistant"
|
||||
? renderColor
|
||||
? "chat-bubble-neutral"
|
||||
: "chat-bubble-secondary"
|
||||
: "chat-bubble-primary"
|
||||
}`}
|
||||
>
|
||||
{chat.hide ? (
|
||||
<MessageHide chat={chat} />
|
||||
) : typeof chat.content !== "string" ? (
|
||||
<MessageDetail chat={chat} renderMarkdown={renderMarkdown} />
|
||||
) : chat.tool_calls ? (
|
||||
<MessageToolCall
|
||||
chat={chat}
|
||||
copyToClipboard={copyToClipboard}
|
||||
/>
|
||||
) : chat.role === "tool" ? (
|
||||
<MessageToolResp
|
||||
chat={chat}
|
||||
copyToClipboard={copyToClipboard}
|
||||
/>
|
||||
) : renderMarkdown ? (
|
||||
// @ts-ignore
|
||||
<Markdown markdown={getMessageText(chat)} />
|
||||
) : (
|
||||
<div className="message-content">
|
||||
{
|
||||
// only show when content is string or list of message
|
||||
// this check is used to avoid rendering tool call
|
||||
chat.content &&
|
||||
(chat.logprobs && renderColor
|
||||
? chat.logprobs.content
|
||||
.filter((c) => c.token)
|
||||
.map((c) => (
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: logprobToColor(c.logprob),
|
||||
display: "inline",
|
||||
}}
|
||||
>
|
||||
{c.token}
|
||||
</div>
|
||||
))
|
||||
: getMessageText(chat))
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="chat-footer opacity-50 flex gap-x-2">
|
||||
<DeleteIcon />
|
||||
<button onClick={() => setShowEdit(true)}>🖋</button>
|
||||
<button onClick={() => setShowEdit(true)}>Edit</button>
|
||||
<CopyIcon textToCopy={getMessageText(chat)} />
|
||||
{chatStore.tts_api && chatStore.tts_key && (
|
||||
<TTSButton
|
||||
chatStore={chatStore}
|
||||
@@ -157,7 +180,13 @@ export default function Message(props: Props) {
|
||||
setChatStore={setChatStore}
|
||||
/>
|
||||
)}
|
||||
<CopyIcon textToCopy={getMessageText(chat)} />
|
||||
<TTSPlay chat={chat} />
|
||||
{chat.response_model_name && (
|
||||
<>
|
||||
<span className="opacity-50">{chat.response_model_name}</span>
|
||||
<hr />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{showEdit && (
|
||||
@@ -170,14 +199,17 @@ export default function Message(props: Props) {
|
||||
)}
|
||||
{showCopiedHint && <CopiedHint />}
|
||||
{chatStore.develop_mode && (
|
||||
<div>
|
||||
<span className="dark:text-white">token</span>
|
||||
<div
|
||||
className={`gap-1 chat-end flex ${
|
||||
chat.role === "assistant" ? "justify-start" : "justify-end"
|
||||
}`}
|
||||
>
|
||||
<span className="">token</span>
|
||||
<input
|
||||
value={chat.token}
|
||||
className="w-20"
|
||||
className="input input-bordered input-xs w-16"
|
||||
onChange={(event: any) => {
|
||||
chat.token = parseInt(event.target.value);
|
||||
props.update_total_tokens();
|
||||
setChatStore({ ...chatStore });
|
||||
}}
|
||||
/>
|
||||
@@ -186,7 +218,7 @@ export default function Message(props: Props) {
|
||||
chatStore.history.splice(messageIndex, 1);
|
||||
chatStore.postBeginIndex = Math.max(
|
||||
chatStore.postBeginIndex - 1,
|
||||
0
|
||||
0,
|
||||
);
|
||||
//chatStore.totalTokens =
|
||||
chatStore.totalTokens = 0;
|
||||
@@ -199,7 +231,7 @@ export default function Message(props: Props) {
|
||||
setChatStore({ ...chatStore });
|
||||
}}
|
||||
>
|
||||
❌
|
||||
<XMarkIcon className="w-4 h-4" />
|
||||
</button>
|
||||
<span
|
||||
onClick={(event: any) => {
|
||||
@@ -207,17 +239,17 @@ export default function Message(props: Props) {
|
||||
setChatStore({ ...chatStore });
|
||||
}}
|
||||
>
|
||||
<label className="dark:text-white">{Tr("example")}</label>
|
||||
<label className="">{Tr("example")}</label>
|
||||
<input type="checkbox" checked={chat.example} />
|
||||
</span>
|
||||
<span
|
||||
onClick={(event: any) => setRenderWorkdown(!renderMarkdown)}
|
||||
>
|
||||
<label className="dark:text-white">{Tr("render")}</label>
|
||||
<label className="">{Tr("render")}</label>
|
||||
<input type="checkbox" checked={renderMarkdown} />
|
||||
</span>
|
||||
<span onClick={(event: any) => setRenderColor(!renderColor)}>
|
||||
<label className="dark:text-white">{Tr("color")}</label>
|
||||
<label className="">{Tr("color")}</label>
|
||||
<input type="checkbox" checked={renderColor} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChatStoreMessage } from "./app";
|
||||
import { ChatStoreMessage } from "@/types/chatstore";
|
||||
|
||||
interface Props {
|
||||
chat: ChatStoreMessage;
|
||||
@@ -13,7 +13,7 @@ export function MessageDetail({ chat, renderMarkdown }: Props) {
|
||||
{chat.content.map((mdt) =>
|
||||
mdt.type === "text" ? (
|
||||
chat.hide ? (
|
||||
mdt.text?.split("\n")[0].slice(0, 16) + "... (deleted)"
|
||||
mdt.text?.split("\n")[0].slice(0, 16) + " ..."
|
||||
) : renderMarkdown ? (
|
||||
// @ts-ignore
|
||||
<Markdown markdown={mdt.text} />
|
||||
@@ -22,13 +22,13 @@ export function MessageDetail({ chat, renderMarkdown }: Props) {
|
||||
)
|
||||
) : (
|
||||
<img
|
||||
className="cursor-pointer max-w-xs max-h-32 p-1"
|
||||
className="my-2 rounded-md max-w-64 max-h-64"
|
||||
src={mdt.image_url?.url}
|
||||
onClick={() => {
|
||||
window.open(mdt.image_url?.url, "_blank");
|
||||
}}
|
||||
/>
|
||||
)
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { ChatStoreMessage } from "./app";
|
||||
import { getMessageText } from "./chatgpt";
|
||||
import { ChatStoreMessage } from "@/types/chatstore";
|
||||
import { getMessageText } from "@/chatgpt";
|
||||
|
||||
interface Props {
|
||||
chat: ChatStoreMessage;
|
||||
}
|
||||
|
||||
export function MessageHide({ chat }: Props) {
|
||||
return (
|
||||
<div>{getMessageText(chat).split("\n")[0].slice(0, 18)} ... (deleted)</div>
|
||||
);
|
||||
return <div>{getMessageText(chat).split("\n")[0].slice(0, 18)} ...</div>;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChatStoreMessage } from "./app";
|
||||
import { ChatStoreMessage } from "@/types/chatstore";
|
||||
|
||||
interface Props {
|
||||
chat: ChatStoreMessage;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ChatStoreMessage } from "./app";
|
||||
import { ChatStoreMessage } from "@/types/chatstore";
|
||||
|
||||
interface Props {
|
||||
chat: ChatStoreMessage;
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
interface Model {
|
||||
maxToken: number;
|
||||
price: {
|
||||
prompt: number;
|
||||
completion: number;
|
||||
};
|
||||
}
|
||||
|
||||
const models: Record<string, Model> = {
|
||||
"gpt-3.5-turbo-0125": {
|
||||
maxToken: 16385,
|
||||
price: { prompt: 0.0005 / 1000, completion: 0.0015 / 1000 },
|
||||
},
|
||||
"gpt-3.5-turbo-1106": {
|
||||
maxToken: 16385,
|
||||
price: { prompt: 0.001 / 1000, completion: 0.002 / 1000 },
|
||||
},
|
||||
"gpt-4-0125-preview": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 0.01 / 1000, completion: 0.03 / 1000 },
|
||||
},
|
||||
};
|
||||
|
||||
export const defaultModel = "gpt-3.5-turbo-0125";
|
||||
|
||||
export default models;
|
||||
93
src/pages/AddToolMsg.tsx
Normal file
93
src/pages/AddToolMsg.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import { useState } from "preact/hooks";
|
||||
import { Dispatch, StateUpdater } from "preact/hooks";
|
||||
|
||||
import { Tr } from "@/translate";
|
||||
import { calculate_token_length } from "@/chatgpt";
|
||||
import { ChatStore } from "@/types/chatstore";
|
||||
|
||||
const AddToolMsg = (props: {
|
||||
setShowAddToolMsg: Dispatch<StateUpdater<boolean>>;
|
||||
chatStore: ChatStore;
|
||||
setChatStore: (cs: ChatStore) => void;
|
||||
}) => {
|
||||
const { setShowAddToolMsg, chatStore, setChatStore } = props;
|
||||
|
||||
const [newToolCallID, setNewToolCallID] = useState("");
|
||||
const [newToolContent, setNewToolContent] = useState("");
|
||||
return (
|
||||
<div
|
||||
className="absolute z-10 bg-black bg-opacity-50 w-full h-full flex justify-center items-center left-0 top-0 overflow-scroll"
|
||||
onClick={() => {
|
||||
setShowAddToolMsg(false);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded p-2 z-20 flex flex-col"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<h2>Add Tool Message</h2>
|
||||
<hr className="my-2" />
|
||||
<span>
|
||||
<label>tool_call_id</label>
|
||||
<input
|
||||
className="rounded m-1 p-1 border-2 border-gray-400"
|
||||
type="text"
|
||||
value={newToolCallID}
|
||||
onChange={(event: any) => setNewToolCallID(event.target.value)}
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<label>Content</label>
|
||||
<textarea
|
||||
className="rounded m-1 p-1 border-2 border-gray-400"
|
||||
rows={5}
|
||||
value={newToolContent}
|
||||
onChange={(event: any) => setNewToolContent(event.target.value)}
|
||||
></textarea>
|
||||
</span>
|
||||
<span className={`flex justify-between p-2`}>
|
||||
<button
|
||||
className="btn btn-info m-1 p-1"
|
||||
onClick={() => setShowAddToolMsg(false)}
|
||||
>
|
||||
{Tr("Cancle")}
|
||||
</button>
|
||||
<button
|
||||
className="rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
|
||||
onClick={() => {
|
||||
if (!newToolCallID.trim()) {
|
||||
alert("tool_call_id is empty");
|
||||
return;
|
||||
}
|
||||
if (!newToolContent.trim()) {
|
||||
alert("content is empty");
|
||||
return;
|
||||
}
|
||||
chatStore.history.push({
|
||||
role: "tool",
|
||||
tool_call_id: newToolCallID.trim(),
|
||||
content: newToolContent.trim(),
|
||||
token: calculate_token_length(newToolContent),
|
||||
hide: false,
|
||||
example: false,
|
||||
audio: null,
|
||||
logprobs: null,
|
||||
response_model_name: null,
|
||||
});
|
||||
setChatStore({ ...chatStore });
|
||||
setNewToolCallID("");
|
||||
setNewToolContent("");
|
||||
setShowAddToolMsg(false);
|
||||
}}
|
||||
>
|
||||
{Tr("Add")}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddToolMsg;
|
||||
235
src/pages/App.tsx
Normal file
235
src/pages/App.tsx
Normal file
@@ -0,0 +1,235 @@
|
||||
import { openDB } from "idb";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import "@/global.css";
|
||||
|
||||
import { calculate_token_length } from "@/chatgpt";
|
||||
import { getDefaultParams } from "@/utils/getDefaultParam";
|
||||
import ChatBOX from "@/pages/Chatbox";
|
||||
import { DefaultModel } from "@/const";
|
||||
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
||||
import { ChatStore } from "@/types/chatstore";
|
||||
import { newChatStore } from "@/types/newChatstore";
|
||||
import { STORAGE_NAME, STORAGE_NAME_SELECTED } from "@/const";
|
||||
import { upgrade } from "@/indexedDB/upgrade";
|
||||
|
||||
export function App() {
|
||||
// init selected index
|
||||
const [selectedChatIndex, setSelectedChatIndex] = useState(
|
||||
parseInt(localStorage.getItem(STORAGE_NAME_SELECTED) ?? "1"),
|
||||
);
|
||||
console.log("selectedChatIndex", selectedChatIndex);
|
||||
useEffect(() => {
|
||||
console.log("set selected chat index", selectedChatIndex);
|
||||
localStorage.setItem(STORAGE_NAME_SELECTED, `${selectedChatIndex}`);
|
||||
}, [selectedChatIndex]);
|
||||
|
||||
const db = openDB<ChatStore>(STORAGE_NAME, 11, {
|
||||
upgrade,
|
||||
});
|
||||
|
||||
const getChatStoreByIndex = async (index: number): Promise<ChatStore> => {
|
||||
const ret: ChatStore = await (await db).get(STORAGE_NAME, index);
|
||||
if (ret === null || ret === undefined) return newChatStore({});
|
||||
// handle read from old version chatstore
|
||||
if (ret.maxGenTokens === undefined) ret.maxGenTokens = 2048;
|
||||
if (ret.maxGenTokens_enabled === undefined) ret.maxGenTokens_enabled = true;
|
||||
if (ret.model === undefined) ret.model = DefaultModel;
|
||||
if (ret.toolsString === undefined) ret.toolsString = "";
|
||||
if (ret.chatgpt_api_web_version === undefined)
|
||||
// this is from old version becasue it is undefined,
|
||||
// so no higher than v1.3.0
|
||||
ret.chatgpt_api_web_version = "v1.2.2";
|
||||
for (const message of ret.history) {
|
||||
if (message.hide === undefined) message.hide = false;
|
||||
if (message.token === undefined)
|
||||
message.token = calculate_token_length(message.content);
|
||||
}
|
||||
if (ret.cost === undefined) ret.cost = 0;
|
||||
return ret;
|
||||
};
|
||||
|
||||
const [chatStore, _setChatStore] = useState(newChatStore({}));
|
||||
const setChatStore = async (chatStore: ChatStore) => {
|
||||
console.log("recalculate postBeginIndex");
|
||||
const max = chatStore.maxTokens - chatStore.tokenMargin;
|
||||
let sum = 0;
|
||||
chatStore.postBeginIndex = chatStore.history.filter(
|
||||
({ hide }) => !hide,
|
||||
).length;
|
||||
for (const msg of chatStore.history
|
||||
.filter(({ hide }) => !hide)
|
||||
.slice()
|
||||
.reverse()) {
|
||||
if (sum + msg.token > max) break;
|
||||
sum += msg.token;
|
||||
chatStore.postBeginIndex -= 1;
|
||||
}
|
||||
chatStore.postBeginIndex =
|
||||
chatStore.postBeginIndex < 0 ? 0 : chatStore.postBeginIndex;
|
||||
|
||||
// manually estimate token
|
||||
chatStore.totalTokens = calculate_token_length(
|
||||
chatStore.systemMessageContent,
|
||||
);
|
||||
for (const msg of chatStore.history
|
||||
.filter(({ hide }) => !hide)
|
||||
.slice(chatStore.postBeginIndex)) {
|
||||
chatStore.totalTokens += msg.token;
|
||||
}
|
||||
|
||||
console.log("saved chat", selectedChatIndex, chatStore);
|
||||
(await db).put(STORAGE_NAME, chatStore, selectedChatIndex);
|
||||
|
||||
// update total tokens
|
||||
chatStore.totalTokens = calculate_token_length(
|
||||
chatStore.systemMessageContent,
|
||||
);
|
||||
for (const msg of chatStore.history
|
||||
.filter(({ hide }) => !hide)
|
||||
.slice(chatStore.postBeginIndex)) {
|
||||
chatStore.totalTokens += msg.token;
|
||||
}
|
||||
|
||||
_setChatStore(chatStore);
|
||||
};
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
_setChatStore(await getChatStoreByIndex(selectedChatIndex));
|
||||
};
|
||||
run();
|
||||
}, [selectedChatIndex]);
|
||||
|
||||
// all chat store indexes
|
||||
const [allChatStoreIndexes, setAllChatStoreIndexes] = useState<IDBValidKey>(
|
||||
[],
|
||||
);
|
||||
|
||||
const handleNewChatStoreWithOldOne = async (chatStore: ChatStore) => {
|
||||
const newKey = await (await db).add(STORAGE_NAME, newChatStore(chatStore));
|
||||
setSelectedChatIndex(newKey as number);
|
||||
setAllChatStoreIndexes(await (await db).getAllKeys(STORAGE_NAME));
|
||||
};
|
||||
const handleNewChatStore = async () => {
|
||||
return handleNewChatStoreWithOldOne(chatStore);
|
||||
};
|
||||
|
||||
const handleDEL = async () => {
|
||||
if (!confirm("Are you sure you want to delete this chat history?")) return;
|
||||
console.log("remove item", `${STORAGE_NAME}-${selectedChatIndex}`);
|
||||
(await db).delete(STORAGE_NAME, selectedChatIndex);
|
||||
const newAllChatStoreIndexes = await (await db).getAllKeys(STORAGE_NAME);
|
||||
|
||||
if (newAllChatStoreIndexes.length === 0) {
|
||||
handleNewChatStore();
|
||||
return;
|
||||
}
|
||||
|
||||
// find nex selected chat index
|
||||
const next = newAllChatStoreIndexes[newAllChatStoreIndexes.length - 1];
|
||||
console.log("next is", next);
|
||||
setSelectedChatIndex(next as number);
|
||||
setAllChatStoreIndexes(newAllChatStoreIndexes);
|
||||
};
|
||||
|
||||
const handleCLS = async () => {
|
||||
if (!confirm("Are you sure you want to delete **ALL** chat history?"))
|
||||
return;
|
||||
|
||||
await (await db).clear(STORAGE_NAME);
|
||||
setAllChatStoreIndexes([]);
|
||||
setSelectedChatIndex(1);
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
// if there are any params in URL, create a new chatStore
|
||||
useEffect(() => {
|
||||
const run = async () => {
|
||||
const chatStore = await getChatStoreByIndex(selectedChatIndex);
|
||||
const api = getDefaultParams("api", "");
|
||||
const key = getDefaultParams("key", "");
|
||||
const sys = getDefaultParams("sys", "");
|
||||
const mode = getDefaultParams("mode", "");
|
||||
const model = getDefaultParams("model", "");
|
||||
const max = getDefaultParams("max", 0);
|
||||
console.log("max is", max, "chatStore.max is", chatStore.maxTokens);
|
||||
// only create new chatStore if the params in URL are NOT
|
||||
// equal to the current selected chatStore
|
||||
if (
|
||||
(api && api !== chatStore.apiEndpoint) ||
|
||||
(key && key !== chatStore.apiKey) ||
|
||||
(sys && sys !== chatStore.systemMessageContent) ||
|
||||
(mode && mode !== (chatStore.streamMode ? "stream" : "fetch")) ||
|
||||
(model && model !== chatStore.model) ||
|
||||
(max !== 0 && max !== chatStore.maxTokens)
|
||||
) {
|
||||
console.log("create new chatStore because of params in URL");
|
||||
handleNewChatStoreWithOldOne(chatStore);
|
||||
}
|
||||
await db;
|
||||
const allidx = await (await db).getAllKeys(STORAGE_NAME);
|
||||
if (allidx.length === 0) {
|
||||
handleNewChatStore();
|
||||
}
|
||||
setAllChatStoreIndexes(await (await db).getAllKeys(STORAGE_NAME));
|
||||
};
|
||||
run();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex text-sm h-full">
|
||||
<div className="flex flex-col h-full p-2 bg-primary">
|
||||
<div className="grow overflow-scroll">
|
||||
<button
|
||||
className="btn btn-sm btn-info p-1 my-1 w-full"
|
||||
onClick={handleNewChatStore}
|
||||
>
|
||||
{Tr("NEW")}
|
||||
</button>
|
||||
<ul className="pt-2">
|
||||
{(allChatStoreIndexes as number[])
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((i) => {
|
||||
// reverse
|
||||
return (
|
||||
<li>
|
||||
<button
|
||||
className={`w-full my-1 p-1 btn btn-sm ${
|
||||
i === selectedChatIndex ? "btn-accent" : "btn-secondary"
|
||||
}`}
|
||||
onClick={() => setSelectedChatIndex(i)}
|
||||
>
|
||||
{i}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
className="btn btn-warning btn-sm p-1 my-1 w-full"
|
||||
onClick={async () => handleDEL()}
|
||||
>
|
||||
{Tr("DEL")}
|
||||
</button>
|
||||
{chatStore.develop_mode && (
|
||||
<button
|
||||
className="btn btn-sm btn-warning p-1 my-1 w-full"
|
||||
onClick={async () => handleCLS()}
|
||||
>
|
||||
{Tr("CLS")}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<ChatBOX
|
||||
db={db}
|
||||
chatStore={chatStore}
|
||||
setChatStore={setChatStore}
|
||||
selectedChatIndex={selectedChatIndex}
|
||||
setSelectedChatIndex={setSelectedChatIndex}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +1,51 @@
|
||||
import { Tr, langCodeContext, LANG_OPTIONS } from "./translate";
|
||||
import structuredClone from "@ungap/structured-clone";
|
||||
import { IDBPDatabase } from "idb";
|
||||
import { createRef } from "preact";
|
||||
import { StateUpdater, useEffect, useState } from "preact/hooks";
|
||||
import { StateUpdater, useEffect, useState, Dispatch } from "preact/hooks";
|
||||
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
||||
import {
|
||||
ChatStore,
|
||||
ChatStoreMessage,
|
||||
STORAGE_NAME_TEMPLATE,
|
||||
STORAGE_NAME_TEMPLATE_API,
|
||||
STORAGE_NAME_TEMPLATE_API_IMAGE_GEN,
|
||||
STORAGE_NAME_TEMPLATE_API_TTS,
|
||||
STORAGE_NAME_TEMPLATE_API_WHISPER,
|
||||
STORAGE_NAME_TEMPLATE_TOOLS,
|
||||
TemplateAPI,
|
||||
TemplateTools,
|
||||
addTotalCost,
|
||||
} from "./app";
|
||||
} from "@/const";
|
||||
import { addTotalCost, getTotalCost } from "@/utils/totalCost";
|
||||
import ChatGPT, {
|
||||
calculate_token_length,
|
||||
ChunkMessage,
|
||||
FetchResponse,
|
||||
Message as MessageType,
|
||||
MessageDetail,
|
||||
ToolCall,
|
||||
Logprobs,
|
||||
} from "./chatgpt";
|
||||
import Message from "./message";
|
||||
import models from "./models";
|
||||
import Settings from "./settings";
|
||||
import getDefaultParams from "./getDefaultParam";
|
||||
import { AddImage } from "./addImage";
|
||||
import { ListAPIs } from "./listAPIs";
|
||||
import { ListToolsTempaltes } from "./listToolsTemplates";
|
||||
import { autoHeight } from "./textarea";
|
||||
|
||||
export interface TemplateChatStore extends ChatStore {
|
||||
name: string;
|
||||
}
|
||||
} from "@/chatgpt";
|
||||
import {
|
||||
ChatStore,
|
||||
ChatStoreMessage,
|
||||
TemplateChatStore,
|
||||
TemplateAPI,
|
||||
TemplateTools,
|
||||
} from "../types/chatstore";
|
||||
import Message from "@/message";
|
||||
import { models } from "@/types/models";
|
||||
import Settings from "@/settings";
|
||||
import { AddImage } from "@/addImage";
|
||||
import { ListAPIs } from "@/listAPIs";
|
||||
import { ListToolsTempaltes } from "@/listToolsTemplates";
|
||||
import { autoHeight } from "@/textarea";
|
||||
import Search from "@/search";
|
||||
import Templates from "@/components/Templates";
|
||||
import VersionHint from "@/components/VersionHint";
|
||||
import StatusBar from "@/components/StatusBar";
|
||||
import WhisperButton from "@/components/WhisperButton";
|
||||
import AddToolMsg from "./AddToolMsg";
|
||||
|
||||
export default function ChatBOX(props: {
|
||||
db: Promise<IDBPDatabase<ChatStore>>;
|
||||
chatStore: ChatStore;
|
||||
setChatStore: (cs: ChatStore) => void;
|
||||
selectedChatIndex: number;
|
||||
setSelectedChatIndex: StateUpdater<number>;
|
||||
setSelectedChatIndex: Dispatch<StateUpdater<number>>;
|
||||
}) {
|
||||
const { chatStore, setChatStore } = props;
|
||||
// prevent error
|
||||
@@ -52,32 +56,29 @@ export default function ChatBOX(props: {
|
||||
const [showGenerating, setShowGenerating] = useState(false);
|
||||
const [generatingMessage, setGeneratingMessage] = useState("");
|
||||
const [showRetry, setShowRetry] = useState(false);
|
||||
const [isRecording, setIsRecording] = useState("Mic");
|
||||
const [showAddToolMsg, setShowAddToolMsg] = useState(false);
|
||||
const [newToolCallID, setNewToolCallID] = useState("");
|
||||
const [newToolContent, setNewToolContent] = useState("");
|
||||
const mediaRef = createRef();
|
||||
const [showSearch, setShowSearch] = useState(false);
|
||||
let default_follow = localStorage.getItem("follow");
|
||||
if (default_follow === null) {
|
||||
default_follow = "true";
|
||||
}
|
||||
const [follow, _setFollow] = useState(default_follow === "true");
|
||||
|
||||
const setFollow = (follow: boolean) => {
|
||||
console.log("set follow", follow);
|
||||
localStorage.setItem("follow", follow.toString());
|
||||
_setFollow(follow);
|
||||
};
|
||||
|
||||
const messagesEndRef = createRef();
|
||||
useEffect(() => {
|
||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
if (follow) {
|
||||
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}, [showRetry, showGenerating, generatingMessage]);
|
||||
|
||||
const client = new ChatGPT(chatStore.apiKey);
|
||||
|
||||
const update_total_tokens = () => {
|
||||
// manually estimate token
|
||||
client.total_tokens = calculate_token_length(
|
||||
chatStore.systemMessageContent
|
||||
);
|
||||
for (const msg of chatStore.history
|
||||
.filter(({ hide }) => !hide)
|
||||
.slice(chatStore.postBeginIndex)) {
|
||||
client.total_tokens += msg.token;
|
||||
}
|
||||
chatStore.totalTokens = client.total_tokens;
|
||||
};
|
||||
|
||||
const _completeWithStreamMode = async (response: Response) => {
|
||||
let responseTokenCount = 0;
|
||||
const allChunkMessage: string[] = [];
|
||||
@@ -86,8 +87,9 @@ export default function ChatBOX(props: {
|
||||
const logprobs: Logprobs = {
|
||||
content: [],
|
||||
};
|
||||
let response_model_name: string | null = null;
|
||||
for await (const i of client.processStreamResponse(response)) {
|
||||
chatStore.responseModelName = i.model;
|
||||
response_model_name = i.model;
|
||||
responseTokenCount += 1;
|
||||
|
||||
const c = i.choices[0];
|
||||
@@ -98,14 +100,14 @@ export default function ChatBOX(props: {
|
||||
const logprob = c?.logprobs?.content[0]?.logprob;
|
||||
if (logprob !== undefined) {
|
||||
logprobs.content.push({
|
||||
token: c.delta.content ?? "",
|
||||
token: c?.delta?.content ?? "",
|
||||
logprob,
|
||||
});
|
||||
console.log(c.delta.content, logprob);
|
||||
console.log(c?.delta?.content, logprob);
|
||||
}
|
||||
|
||||
allChunkMessage.push(c.delta.content ?? "");
|
||||
const tool_calls = c.delta.tool_calls;
|
||||
allChunkMessage.push(c?.delta?.content ?? "");
|
||||
const tool_calls = c?.delta?.tool_calls;
|
||||
if (tool_calls) {
|
||||
for (const tool_call of tool_calls) {
|
||||
// init
|
||||
@@ -124,7 +126,7 @@ export default function ChatBOX(props: {
|
||||
|
||||
// update tool call arguments
|
||||
const tool = allChunkTool.find(
|
||||
(tool) => tool.index === tool_call.index
|
||||
(tool) => tool.index === tool_call.index,
|
||||
);
|
||||
|
||||
if (!tool) {
|
||||
@@ -139,7 +141,7 @@ export default function ChatBOX(props: {
|
||||
allChunkMessage.join("") +
|
||||
allChunkTool.map((tool) => {
|
||||
return `Tool Call ID: ${tool.id}\nType: ${tool.type}\nFunction: ${tool.function.name}\nArguments: ${tool.function.arguments}`;
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
setShowGenerating(false);
|
||||
@@ -147,17 +149,17 @@ export default function ChatBOX(props: {
|
||||
|
||||
// estimate cost
|
||||
let cost = 0;
|
||||
if (chatStore.responseModelName) {
|
||||
if (response_model_name) {
|
||||
cost +=
|
||||
responseTokenCount *
|
||||
(models[chatStore.responseModelName]?.price?.completion ?? 0);
|
||||
(models[response_model_name]?.price?.completion ?? 0);
|
||||
let sum = 0;
|
||||
for (const msg of chatStore.history
|
||||
.filter(({ hide }) => !hide)
|
||||
.slice(chatStore.postBeginIndex)) {
|
||||
sum += msg.token;
|
||||
}
|
||||
cost += sum * (models[chatStore.responseModelName]?.price?.prompt ?? 0);
|
||||
cost += sum * (models[response_model_name]?.price?.prompt ?? 0);
|
||||
}
|
||||
|
||||
console.log("cost", cost);
|
||||
@@ -173,6 +175,7 @@ export default function ChatBOX(props: {
|
||||
example: false,
|
||||
audio: null,
|
||||
logprobs,
|
||||
response_model_name,
|
||||
};
|
||||
if (allChunkTool.length > 0) newMsg.tool_calls = allChunkTool;
|
||||
|
||||
@@ -180,7 +183,6 @@ export default function ChatBOX(props: {
|
||||
// manually copy status from client to chatStore
|
||||
chatStore.maxTokens = client.max_tokens;
|
||||
chatStore.tokenMargin = client.tokens_margin;
|
||||
update_total_tokens();
|
||||
setChatStore({ ...chatStore });
|
||||
setGeneratingMessage("");
|
||||
setShowGenerating(false);
|
||||
@@ -188,7 +190,6 @@ export default function ChatBOX(props: {
|
||||
|
||||
const _completeWithFetchMode = async (response: Response) => {
|
||||
const data = (await response.json()) as FetchResponse;
|
||||
chatStore.responseModelName = data.model ?? "";
|
||||
if (data.model) {
|
||||
let cost = 0;
|
||||
cost +=
|
||||
@@ -228,6 +229,7 @@ export default function ChatBOX(props: {
|
||||
example: false,
|
||||
audio: null,
|
||||
logprobs: data.choices[0]?.logprobs,
|
||||
response_model_name: data.model,
|
||||
});
|
||||
setShowGenerating(false);
|
||||
};
|
||||
@@ -277,7 +279,7 @@ export default function ChatBOX(props: {
|
||||
setShowGenerating(true);
|
||||
const response = await client._fetch(
|
||||
chatStore.streamMode,
|
||||
chatStore.logprobs
|
||||
chatStore.logprobs,
|
||||
);
|
||||
const contentType = response.headers.get("content-type");
|
||||
if (contentType?.startsWith("text/event-stream")) {
|
||||
@@ -311,7 +313,6 @@ export default function ChatBOX(props: {
|
||||
console.log("empty message");
|
||||
return;
|
||||
}
|
||||
if (call_complete) chatStore.responseModelName = "";
|
||||
|
||||
let content: string | MessageDetail[] = inputMsg;
|
||||
if (images.length > 0) {
|
||||
@@ -328,6 +329,7 @@ export default function ChatBOX(props: {
|
||||
example: false,
|
||||
audio: null,
|
||||
logprobs: null,
|
||||
response_model_name: null,
|
||||
});
|
||||
|
||||
// manually calculate token length
|
||||
@@ -347,33 +349,33 @@ export default function ChatBOX(props: {
|
||||
|
||||
const [templates, _setTemplates] = useState(
|
||||
JSON.parse(
|
||||
localStorage.getItem(STORAGE_NAME_TEMPLATE) || "[]"
|
||||
) as TemplateChatStore[]
|
||||
localStorage.getItem(STORAGE_NAME_TEMPLATE) || "[]",
|
||||
) as TemplateChatStore[],
|
||||
);
|
||||
const [templateAPIs, _setTemplateAPIs] = useState(
|
||||
JSON.parse(
|
||||
localStorage.getItem(STORAGE_NAME_TEMPLATE_API) || "[]"
|
||||
) as TemplateAPI[]
|
||||
localStorage.getItem(STORAGE_NAME_TEMPLATE_API) || "[]",
|
||||
) as TemplateAPI[],
|
||||
);
|
||||
const [templateAPIsWhisper, _setTemplateAPIsWhisper] = useState(
|
||||
JSON.parse(
|
||||
localStorage.getItem(STORAGE_NAME_TEMPLATE_API_WHISPER) || "[]"
|
||||
) as TemplateAPI[]
|
||||
localStorage.getItem(STORAGE_NAME_TEMPLATE_API_WHISPER) || "[]",
|
||||
) as TemplateAPI[],
|
||||
);
|
||||
const [templateAPIsTTS, _setTemplateAPIsTTS] = useState(
|
||||
JSON.parse(
|
||||
localStorage.getItem(STORAGE_NAME_TEMPLATE_API_TTS) || "[]"
|
||||
) as TemplateAPI[]
|
||||
localStorage.getItem(STORAGE_NAME_TEMPLATE_API_TTS) || "[]",
|
||||
) as TemplateAPI[],
|
||||
);
|
||||
const [templateAPIsImageGen, _setTemplateAPIsImageGen] = useState(
|
||||
JSON.parse(
|
||||
localStorage.getItem(STORAGE_NAME_TEMPLATE_API_IMAGE_GEN) || "[]"
|
||||
) as TemplateAPI[]
|
||||
localStorage.getItem(STORAGE_NAME_TEMPLATE_API_IMAGE_GEN) || "[]",
|
||||
) as TemplateAPI[],
|
||||
);
|
||||
const [toolsTemplates, _setToolsTemplates] = useState(
|
||||
JSON.parse(
|
||||
localStorage.getItem(STORAGE_NAME_TEMPLATE_TOOLS) || "[]"
|
||||
) as TemplateTools[]
|
||||
localStorage.getItem(STORAGE_NAME_TEMPLATE_TOOLS) || "[]",
|
||||
) as TemplateTools[],
|
||||
);
|
||||
const setTemplates = (templates: TemplateChatStore[]) => {
|
||||
localStorage.setItem(STORAGE_NAME_TEMPLATE, JSON.stringify(templates));
|
||||
@@ -382,42 +384,42 @@ export default function ChatBOX(props: {
|
||||
const setTemplateAPIs = (templateAPIs: TemplateAPI[]) => {
|
||||
localStorage.setItem(
|
||||
STORAGE_NAME_TEMPLATE_API,
|
||||
JSON.stringify(templateAPIs)
|
||||
JSON.stringify(templateAPIs),
|
||||
);
|
||||
_setTemplateAPIs(templateAPIs);
|
||||
};
|
||||
const setTemplateAPIsWhisper = (templateAPIWhisper: TemplateAPI[]) => {
|
||||
localStorage.setItem(
|
||||
STORAGE_NAME_TEMPLATE_API_WHISPER,
|
||||
JSON.stringify(templateAPIWhisper)
|
||||
JSON.stringify(templateAPIWhisper),
|
||||
);
|
||||
_setTemplateAPIsWhisper(templateAPIWhisper);
|
||||
};
|
||||
const setTemplateAPIsTTS = (templateAPITTS: TemplateAPI[]) => {
|
||||
localStorage.setItem(
|
||||
STORAGE_NAME_TEMPLATE_API_TTS,
|
||||
JSON.stringify(templateAPITTS)
|
||||
JSON.stringify(templateAPITTS),
|
||||
);
|
||||
_setTemplateAPIsTTS(templateAPITTS);
|
||||
};
|
||||
const setTemplateAPIsImageGen = (templateAPIImageGen: TemplateAPI[]) => {
|
||||
localStorage.setItem(
|
||||
STORAGE_NAME_TEMPLATE_API_IMAGE_GEN,
|
||||
JSON.stringify(templateAPIImageGen)
|
||||
JSON.stringify(templateAPIImageGen),
|
||||
);
|
||||
_setTemplateAPIsImageGen(templateAPIImageGen);
|
||||
};
|
||||
const setTemplateTools = (templateTools: TemplateTools[]) => {
|
||||
localStorage.setItem(
|
||||
STORAGE_NAME_TEMPLATE_TOOLS,
|
||||
JSON.stringify(templateTools)
|
||||
JSON.stringify(templateTools),
|
||||
);
|
||||
_setToolsTemplates(templateTools);
|
||||
};
|
||||
const userInputRef = createRef();
|
||||
|
||||
return (
|
||||
<div className="grow flex flex-col p-2 dark:text-black">
|
||||
<div className="grow flex flex-col p-2 w-full">
|
||||
{showSettings && (
|
||||
<Settings
|
||||
chatStore={chatStore}
|
||||
@@ -438,52 +440,29 @@ export default function ChatBOX(props: {
|
||||
setTemplateTools={setTemplateTools}
|
||||
/>
|
||||
)}
|
||||
<div
|
||||
className="cursor-pointer rounded bg-cyan-300 dark:text-white p-1 dark:bg-cyan-800"
|
||||
onClick={() => setShowSettings(true)}
|
||||
>
|
||||
<div>
|
||||
<button className="underline">
|
||||
{chatStore.systemMessageContent.length > 16
|
||||
? chatStore.systemMessageContent.slice(0, 16) + ".."
|
||||
: chatStore.systemMessageContent}
|
||||
</button>{" "}
|
||||
<button className="underline">
|
||||
{chatStore.streamMode ? Tr("STREAM") : Tr("FETCH")}
|
||||
</button>{" "}
|
||||
{chatStore.toolsString.trim() && (
|
||||
<button className="underline">TOOL</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs">
|
||||
<span className="underline">{chatStore.model}</span>{" "}
|
||||
<span>
|
||||
Tokens:{" "}
|
||||
<span className="underline">
|
||||
{chatStore.totalTokens}/{chatStore.maxTokens}
|
||||
</span>
|
||||
</span>{" "}
|
||||
<span>
|
||||
{Tr("Cut")}:{" "}
|
||||
<span className="underline">
|
||||
{chatStore.postBeginIndex}/
|
||||
{chatStore.history.filter(({ hide }) => !hide).length}
|
||||
</span>{" "}
|
||||
</span>{" "}
|
||||
<span>
|
||||
{Tr("Cost")}:{" "}
|
||||
<span className="underline">${chatStore.cost.toFixed(4)}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{showSearch && (
|
||||
<Search
|
||||
setSelectedChatIndex={props.setSelectedChatIndex}
|
||||
db={props.db}
|
||||
chatStore={chatStore}
|
||||
setShow={setShowSearch}
|
||||
/>
|
||||
)}
|
||||
|
||||
<StatusBar
|
||||
chatStore={chatStore}
|
||||
setShowSettings={setShowSettings}
|
||||
setShowSearch={setShowSearch}
|
||||
/>
|
||||
|
||||
<div className="grow overflow-scroll">
|
||||
{!chatStore.apiKey && (
|
||||
<p className="opacity-60 p-6 rounded bg-white my-3 text-left dark:text-black">
|
||||
<p className="bg-base-200 p-6 rounded my-3 text-left">
|
||||
{Tr("Please click above to set")} (OpenAI) API KEY
|
||||
</p>
|
||||
)}
|
||||
{!chatStore.apiEndpoint && (
|
||||
<p className="opacity-60 p-6 rounded bg-white my-3 text-left dark:text-black">
|
||||
<p className="bg-base-200 p-6 rounded my-3 text-left">
|
||||
{Tr("Please click above to set")} API Endpoint
|
||||
</p>
|
||||
)}
|
||||
@@ -545,7 +524,7 @@ export default function ChatBOX(props: {
|
||||
)}
|
||||
|
||||
{chatStore.history.filter((msg) => !msg.example).length == 0 && (
|
||||
<div className="break-all opacity-80 p-3 rounded bg-white my-3 text-left dark:text-black">
|
||||
<div className="bg-base-200 break-all p-3 my-3 text-left">
|
||||
<h2>
|
||||
<span>{Tr("Saved prompt templates")}</span>
|
||||
<button
|
||||
@@ -560,106 +539,14 @@ export default function ChatBOX(props: {
|
||||
{Tr("Reset Current")}
|
||||
</button>
|
||||
</h2>
|
||||
<hr className="my-2" />
|
||||
<div className="divider"></div>
|
||||
<div className="flex flex-wrap">
|
||||
{templates.map((t, index) => (
|
||||
<div
|
||||
className="cursor-pointer rounded bg-green-400 w-fit p-2 m-1 flex flex-col"
|
||||
onClick={() => {
|
||||
const newChatStore: ChatStore = structuredClone(t);
|
||||
// @ts-ignore
|
||||
delete newChatStore.name;
|
||||
if (!newChatStore.apiEndpoint) {
|
||||
newChatStore.apiEndpoint = getDefaultParams(
|
||||
"api",
|
||||
chatStore.apiEndpoint
|
||||
);
|
||||
}
|
||||
if (!newChatStore.apiKey) {
|
||||
newChatStore.apiKey = getDefaultParams(
|
||||
"key",
|
||||
chatStore.apiKey
|
||||
);
|
||||
}
|
||||
if (!newChatStore.whisper_api) {
|
||||
newChatStore.whisper_api = getDefaultParams(
|
||||
"whisper-api",
|
||||
chatStore.whisper_api
|
||||
);
|
||||
}
|
||||
if (!newChatStore.whisper_key) {
|
||||
newChatStore.whisper_key = getDefaultParams(
|
||||
"whisper-key",
|
||||
chatStore.whisper_key
|
||||
);
|
||||
}
|
||||
if (!newChatStore.tts_api) {
|
||||
newChatStore.tts_api = getDefaultParams(
|
||||
"tts-api",
|
||||
chatStore.tts_api
|
||||
);
|
||||
}
|
||||
if (!newChatStore.tts_key) {
|
||||
newChatStore.tts_key = getDefaultParams(
|
||||
"tts-key",
|
||||
chatStore.tts_key
|
||||
);
|
||||
}
|
||||
if (!newChatStore.image_gen_api) {
|
||||
newChatStore.image_gen_api = getDefaultParams(
|
||||
"image-gen-api",
|
||||
chatStore.image_gen_api
|
||||
);
|
||||
}
|
||||
if (!newChatStore.image_gen_key) {
|
||||
newChatStore.image_gen_key = getDefaultParams(
|
||||
"image-gen-key",
|
||||
chatStore.image_gen_key
|
||||
);
|
||||
}
|
||||
newChatStore.cost = 0;
|
||||
|
||||
// manage undefined value because of version update
|
||||
newChatStore.toolsString = newChatStore.toolsString || "";
|
||||
|
||||
setChatStore({ ...newChatStore });
|
||||
}}
|
||||
>
|
||||
<span className="w-full text-center">{t.name}</span>
|
||||
<hr className="mt-2" />
|
||||
<span className="flex justify-between">
|
||||
<button
|
||||
onClick={(event) => {
|
||||
// prevent triggert other event
|
||||
event.stopPropagation();
|
||||
|
||||
const name = prompt("Give template a name");
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
t.name = name;
|
||||
setTemplates(structuredClone(templates));
|
||||
}}
|
||||
>
|
||||
🖋
|
||||
</button>
|
||||
<button
|
||||
onClick={(event) => {
|
||||
// prevent triggert other event
|
||||
event.stopPropagation();
|
||||
|
||||
if (!confirm("Are you sure to delete this template?")) {
|
||||
return;
|
||||
}
|
||||
templates.splice(index, 1);
|
||||
setTemplates(structuredClone(templates));
|
||||
}}
|
||||
>
|
||||
❌
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
<Templates
|
||||
templates={templates}
|
||||
setTemplates={setTemplates}
|
||||
chatStore={chatStore}
|
||||
setChatStore={setChatStore}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -671,21 +558,32 @@ export default function ChatBOX(props: {
|
||||
<br />↖{Tr("Click the conor to create a new chat")}
|
||||
<br />⚠
|
||||
{Tr(
|
||||
"All chat history and settings are stored in the local browser"
|
||||
"All chat history and settings are stored in the local browser",
|
||||
)}
|
||||
<br />
|
||||
</p>
|
||||
)}
|
||||
{chatStore.systemMessageContent.trim() && (
|
||||
<div className="chat chat-start">
|
||||
<div className="chat-header">Prompt</div>
|
||||
<div
|
||||
className="chat-bubble chat-bubble-accent cursor-pointer message-content"
|
||||
onClick={() => setShowSettings(true)}
|
||||
>
|
||||
{chatStore.systemMessageContent}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{chatStore.history.map((_, messageIndex) => (
|
||||
<Message
|
||||
chatStore={chatStore}
|
||||
setChatStore={setChatStore}
|
||||
messageIndex={messageIndex}
|
||||
update_total_tokens={update_total_tokens}
|
||||
/>
|
||||
))}
|
||||
{showGenerating && (
|
||||
<p className="p-2 my-2 animate-pulse dark:text-white message-content">
|
||||
<p className="p-2 my-2 animate-pulse message-content">
|
||||
{generatingMessage || Tr("Generating...")}
|
||||
...
|
||||
</p>
|
||||
@@ -693,7 +591,7 @@ export default function ChatBOX(props: {
|
||||
<p className="text-center">
|
||||
{chatStore.history.length > 0 && (
|
||||
<button
|
||||
className="disabled:line-through disabled:bg-slate-500 rounded m-2 p-2 border-2 bg-teal-500 hover:bg-teal-600"
|
||||
className="btn btn-sm btn-warning disabled:line-through disabled:btn-neutral disabled:text-white m-2 p-2"
|
||||
disabled={showGenerating}
|
||||
onClick={async () => {
|
||||
const messageIndex = chatStore.history.length - 1;
|
||||
@@ -702,7 +600,6 @@ export default function ChatBOX(props: {
|
||||
}
|
||||
|
||||
//chatStore.totalTokens =
|
||||
update_total_tokens();
|
||||
setChatStore({ ...chatStore });
|
||||
|
||||
await complete();
|
||||
@@ -713,7 +610,7 @@ export default function ChatBOX(props: {
|
||||
)}
|
||||
{chatStore.develop_mode && chatStore.history.length > 0 && (
|
||||
<button
|
||||
className="disabled:line-through disabled:bg-slate-500 rounded m-2 p-2 border-2 bg-yellow-500 hover:bg-yellow-600"
|
||||
className="btn btn-outline btn-sm btn-warning disabled:line-through disabled:bg-neural"
|
||||
disabled={showGenerating}
|
||||
onClick={async () => {
|
||||
await complete();
|
||||
@@ -724,11 +621,6 @@ export default function ChatBOX(props: {
|
||||
)}
|
||||
</p>
|
||||
<p className="p-2 my-2 text-center opacity-50 dark:text-white">
|
||||
{chatStore.responseModelName && (
|
||||
<>
|
||||
{Tr("Generated by")} {chatStore.responseModelName}
|
||||
</>
|
||||
)}
|
||||
{chatStore.postBeginIndex !== 0 && (
|
||||
<>
|
||||
<br />
|
||||
@@ -737,43 +629,7 @@ export default function ChatBOX(props: {
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
{chatStore.chatgpt_api_web_version < "v1.3.0" && (
|
||||
<p className="p-2 my-2 text-center dark:text-white">
|
||||
<br />
|
||||
{Tr("Warning: current chatStore version")}:{" "}
|
||||
{chatStore.chatgpt_api_web_version} {"< v1.3.0"}
|
||||
<br />
|
||||
v1.3.0
|
||||
引入与旧版不兼容的消息裁切算法。继续使用旧版可能会导致消息裁切过多或过少(表现为失去上下文或输出不完整)。
|
||||
<br />
|
||||
请在左上角创建新会话:)
|
||||
</p>
|
||||
)}
|
||||
{chatStore.chatgpt_api_web_version < "v1.4.0" && (
|
||||
<p className="p-2 my-2 text-center dark:text-white">
|
||||
<br />
|
||||
{Tr("Warning: current chatStore version")}:{" "}
|
||||
{chatStore.chatgpt_api_web_version} {"< v1.4.0"}
|
||||
<br />
|
||||
v1.4.0 增加了更多参数,继续使用旧版可能因参数确实导致未定义的行为
|
||||
<br />
|
||||
请在左上角创建新会话:)
|
||||
</p>
|
||||
)}
|
||||
{chatStore.chatgpt_api_web_version < "v1.6.0" && (
|
||||
<p className="p-2 my-2 text-center dark:text-white">
|
||||
<br />
|
||||
提示:当前会话版本 {chatStore.chatgpt_api_web_version}
|
||||
{Tr("Warning: current chatStore version")}:{" "}
|
||||
{chatStore.chatgpt_api_web_version} {"< v1.6.0"}
|
||||
。
|
||||
<br />
|
||||
v1.6.0 开始保存会话模板时会将 apiKey 和 apiEndpoint
|
||||
设置为空,继续使用旧版可能在保存读取模板时出现问题
|
||||
<br />
|
||||
请在左上角创建新会话:)
|
||||
</p>
|
||||
)}
|
||||
<VersionHint chatStore={chatStore} />
|
||||
{showRetry && (
|
||||
<p className="text-right p-2 my-2 dark:text-white">
|
||||
<button
|
||||
@@ -804,19 +660,29 @@ export default function ChatBOX(props: {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between">
|
||||
{(chatStore.model.match("vision") ||
|
||||
(chatStore.image_gen_api && chatStore.image_gen_key)) && (
|
||||
<button
|
||||
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
|
||||
disabled={showGenerating || !chatStore.apiKey}
|
||||
onClick={() => {
|
||||
setShowAddImage(!showAddImage);
|
||||
}}
|
||||
>
|
||||
Img
|
||||
</button>
|
||||
)}
|
||||
{generatingMessage && (
|
||||
<span
|
||||
className="p-2 m-2 rounded bg-white dark:text-black dark:bg-white dark:bg-opacity-50"
|
||||
style={{ textAlign: "right" }}
|
||||
onClick={() => {
|
||||
setFollow(!follow);
|
||||
}}
|
||||
>
|
||||
<label>Follow</label>
|
||||
<input type="checkbox" checked={follow} />
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between my-1">
|
||||
<button
|
||||
className="btn btn-primary disabled:line-through disabled:text-white disabled:bg-neutral m-1 p-1"
|
||||
disabled={showGenerating || !chatStore.apiKey}
|
||||
onClick={() => {
|
||||
setShowAddImage(!showAddImage);
|
||||
}}
|
||||
>
|
||||
Image
|
||||
</button>
|
||||
{showAddImage && (
|
||||
<AddImage
|
||||
chatStore={chatStore}
|
||||
@@ -846,11 +712,14 @@ export default function ChatBOX(props: {
|
||||
autoHeight(event.target);
|
||||
setInputMsg(event.target.value);
|
||||
}}
|
||||
className="rounded grow m-1 p-1 border-2 border-gray-400 w-0"
|
||||
className="textarea textarea-bordered textarea-sm grow w-0"
|
||||
style={{
|
||||
lineHeight: "1.39",
|
||||
}}
|
||||
placeholder="Type here..."
|
||||
></textarea>
|
||||
<button
|
||||
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
|
||||
className="btn btn-primary disabled:btn-neutral disabled:line-through m-1 p-1"
|
||||
disabled={showGenerating}
|
||||
onClick={() => {
|
||||
send(inputMsg, true);
|
||||
@@ -860,129 +729,16 @@ export default function ChatBOX(props: {
|
||||
>
|
||||
{Tr("Send")}
|
||||
</button>
|
||||
{chatStore.whisper_api &&
|
||||
chatStore.whisper_key &&
|
||||
(chatStore.whisper_key || chatStore.apiKey) && (
|
||||
<button
|
||||
className={`disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 ${
|
||||
isRecording === "Recording"
|
||||
? "bg-red-400 hover:bg-red-600"
|
||||
: "bg-cyan-400 hover:bg-cyan-600"
|
||||
} ${isRecording !== "Mic" ? "animate-pulse" : ""}`}
|
||||
disabled={isRecording === "Transcribing"}
|
||||
ref={mediaRef}
|
||||
onClick={async () => {
|
||||
if (isRecording === "Recording") {
|
||||
// @ts-ignore
|
||||
window.mediaRecorder.stop();
|
||||
setIsRecording("Transcribing");
|
||||
return;
|
||||
}
|
||||
|
||||
// build prompt
|
||||
const prompt = [chatStore.systemMessageContent]
|
||||
.concat(
|
||||
chatStore.history
|
||||
.filter(({ hide }) => !hide)
|
||||
.slice(chatStore.postBeginIndex)
|
||||
.map(({ content }) => {
|
||||
if (typeof content === "string") {
|
||||
return content;
|
||||
} else {
|
||||
return content.map((c) => c?.text).join(" ");
|
||||
}
|
||||
})
|
||||
)
|
||||
.concat([inputMsg])
|
||||
.join(" ");
|
||||
console.log({ prompt });
|
||||
|
||||
setIsRecording("Recording");
|
||||
console.log("start recording");
|
||||
|
||||
try {
|
||||
const mediaRecorder = new MediaRecorder(
|
||||
await navigator.mediaDevices.getUserMedia({
|
||||
audio: true,
|
||||
}),
|
||||
{ audioBitsPerSecond: 64 * 1000 }
|
||||
);
|
||||
|
||||
// mount mediaRecorder to ref
|
||||
// @ts-ignore
|
||||
window.mediaRecorder = mediaRecorder;
|
||||
|
||||
mediaRecorder.start();
|
||||
const audioChunks: Blob[] = [];
|
||||
mediaRecorder.addEventListener("dataavailable", (event) => {
|
||||
audioChunks.push(event.data);
|
||||
});
|
||||
mediaRecorder.addEventListener("stop", async () => {
|
||||
// Stop the MediaRecorder
|
||||
mediaRecorder.stop();
|
||||
// Stop the media stream
|
||||
mediaRecorder.stream.getTracks()[0].stop();
|
||||
|
||||
setIsRecording("Transcribing");
|
||||
const audioBlob = new Blob(audioChunks);
|
||||
const audioUrl = URL.createObjectURL(audioBlob);
|
||||
console.log({ audioUrl });
|
||||
const audio = new Audio(audioUrl);
|
||||
// audio.play();
|
||||
const reader = new FileReader();
|
||||
reader.readAsDataURL(audioBlob);
|
||||
|
||||
// file-like object with mimetype
|
||||
const blob = new Blob([audioBlob], {
|
||||
type: "application/octet-stream",
|
||||
});
|
||||
|
||||
reader.onloadend = async () => {
|
||||
try {
|
||||
const base64data = reader.result;
|
||||
|
||||
// post to openai whisper api
|
||||
const formData = new FormData();
|
||||
// append file
|
||||
formData.append("file", blob, "audio.ogg");
|
||||
formData.append("model", "whisper-1");
|
||||
formData.append("response_format", "text");
|
||||
formData.append("prompt", prompt);
|
||||
|
||||
const response = await fetch(chatStore.whisper_api, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${
|
||||
chatStore.whisper_key || chatStore.apiKey
|
||||
}`,
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
|
||||
setInputMsg(inputMsg ? inputMsg + " " + text : text);
|
||||
} catch (error) {
|
||||
alert(error);
|
||||
console.log(error);
|
||||
} finally {
|
||||
setIsRecording("Mic");
|
||||
}
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
alert(error);
|
||||
console.log(error);
|
||||
setIsRecording("Mic");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isRecording}
|
||||
</button>
|
||||
)}
|
||||
{chatStore.whisper_api && chatStore.whisper_key && (
|
||||
<WhisperButton
|
||||
chatStore={chatStore}
|
||||
inputMsg={inputMsg}
|
||||
setInputMsg={setInputMsg}
|
||||
/>
|
||||
)}
|
||||
{chatStore.develop_mode && (
|
||||
<button
|
||||
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
|
||||
className="btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1"
|
||||
disabled={showGenerating || !chatStore.apiKey}
|
||||
onClick={() => {
|
||||
chatStore.history.push({
|
||||
@@ -995,8 +751,8 @@ export default function ChatBOX(props: {
|
||||
example: false,
|
||||
audio: null,
|
||||
logprobs: null,
|
||||
response_model_name: null,
|
||||
});
|
||||
update_total_tokens();
|
||||
setInputMsg("");
|
||||
setChatStore({ ...chatStore });
|
||||
}}
|
||||
@@ -1006,7 +762,7 @@ export default function ChatBOX(props: {
|
||||
)}
|
||||
{chatStore.develop_mode && (
|
||||
<button
|
||||
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
|
||||
className="btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1"
|
||||
disabled={showGenerating || !chatStore.apiKey}
|
||||
onClick={() => {
|
||||
send(inputMsg, false);
|
||||
@@ -1017,7 +773,7 @@ export default function ChatBOX(props: {
|
||||
)}
|
||||
{chatStore.develop_mode && (
|
||||
<button
|
||||
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
|
||||
className="btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1"
|
||||
disabled={showGenerating || !chatStore.apiKey}
|
||||
onClick={() => {
|
||||
setShowAddToolMsg(true);
|
||||
@@ -1027,82 +783,11 @@ export default function ChatBOX(props: {
|
||||
</button>
|
||||
)}
|
||||
{showAddToolMsg && (
|
||||
<div
|
||||
className="absolute z-10 bg-black bg-opacity-50 w-full h-full flex justify-center items-center left-0 top-0 overflow-scroll"
|
||||
onClick={() => {
|
||||
setShowAddToolMsg(false);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="bg-white rounded p-2 z-20 flex flex-col"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<h2>Add Tool Message</h2>
|
||||
<hr className="my-2" />
|
||||
<span>
|
||||
<label>tool_call_id</label>
|
||||
<input
|
||||
className="rounded m-1 p-1 border-2 border-gray-400"
|
||||
type="text"
|
||||
value={newToolCallID}
|
||||
onChange={(event: any) =>
|
||||
setNewToolCallID(event.target.value)
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<label>Content</label>
|
||||
<textarea
|
||||
className="rounded m-1 p-1 border-2 border-gray-400"
|
||||
rows={5}
|
||||
value={newToolContent}
|
||||
onChange={(event: any) =>
|
||||
setNewToolContent(event.target.value)
|
||||
}
|
||||
></textarea>
|
||||
</span>
|
||||
<span className={`flex justify-between p-2`}>
|
||||
<button
|
||||
className="rounded m-1 p-1 border-2 bg-red-400 hover:bg-red-600"
|
||||
onClick={() => setShowAddToolMsg(false)}
|
||||
>
|
||||
{Tr("Cancle")}
|
||||
</button>
|
||||
<button
|
||||
className="rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600"
|
||||
onClick={() => {
|
||||
if (!newToolCallID.trim()) {
|
||||
alert("tool_call_id is empty");
|
||||
return;
|
||||
}
|
||||
if (!newToolContent.trim()) {
|
||||
alert("content is empty");
|
||||
return;
|
||||
}
|
||||
chatStore.history.push({
|
||||
role: "tool",
|
||||
tool_call_id: newToolCallID.trim(),
|
||||
content: newToolContent.trim(),
|
||||
token: calculate_token_length(newToolContent),
|
||||
hide: false,
|
||||
example: false,
|
||||
audio: null,
|
||||
logprobs: null,
|
||||
});
|
||||
update_total_tokens();
|
||||
setChatStore({ ...chatStore });
|
||||
setNewToolCallID("");
|
||||
setNewToolContent("");
|
||||
setShowAddToolMsg(false);
|
||||
}}
|
||||
>
|
||||
{Tr("Add")}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<AddToolMsg
|
||||
chatStore={chatStore}
|
||||
setChatStore={setChatStore}
|
||||
setShowAddToolMsg={setShowAddToolMsg}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
180
src/search.tsx
Normal file
180
src/search.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
import { IDBPDatabase } from "idb";
|
||||
import { StateUpdater, useRef, useState, Dispatch } from "preact/hooks";
|
||||
|
||||
import { ChatStore } from "@/types/chatstore";
|
||||
|
||||
interface ChatStoreSearchResult {
|
||||
key: IDBValidKey;
|
||||
cs: ChatStore;
|
||||
query: string;
|
||||
preview: string;
|
||||
}
|
||||
|
||||
export default function Search(props: {
|
||||
db: Promise<IDBPDatabase<ChatStore>>;
|
||||
setSelectedChatIndex: Dispatch<StateUpdater<number>>;
|
||||
chatStore: ChatStore;
|
||||
setShow: (show: boolean) => void;
|
||||
}) {
|
||||
const [searchResult, setSearchResult] = useState<ChatStoreSearchResult[]>([]);
|
||||
const [searching, setSearching] = useState<boolean>(false);
|
||||
const [searchingNow, setSearchingNow] = useState<number>(0);
|
||||
const [pageIndex, setPageIndex] = useState<number>(0);
|
||||
const searchAbortRef = useRef<AbortController | null>(null);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => props.setShow(false)}
|
||||
className="left-0 top-0 overflow-scroll flex justify-center absolute w-screen h-full bg-black bg-opacity-50 z-10"
|
||||
>
|
||||
<div
|
||||
onClick={(event: any) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
className="m-2 p-2 bg-base-300 rounded-lg h-fit w-2/3 z-20"
|
||||
>
|
||||
<div className="flex justify-between">
|
||||
<span className="m-1 p-1 font-bold">Search</span>
|
||||
<button
|
||||
className="m-1 p-1 btn btn-sm btn-secondary"
|
||||
onClick={() => props.setShow(false)}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
autoFocus
|
||||
className="input input-bordered w-full border"
|
||||
type="text"
|
||||
placeholder="Type Something..."
|
||||
onInput={async (event: any) => {
|
||||
const query = event.target.value.trim().toLowerCase();
|
||||
if (!query) {
|
||||
setSearchResult([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// abort previous search
|
||||
if (searchAbortRef.current) {
|
||||
searchAbortRef.current.abort();
|
||||
}
|
||||
|
||||
// Create a new AbortController for the new operation
|
||||
const abortController = new AbortController();
|
||||
searchAbortRef.current = abortController;
|
||||
const signal = abortController.signal;
|
||||
|
||||
setSearching(true);
|
||||
|
||||
const db = await props.db;
|
||||
const resultKeys = await db.getAllKeys("chatgpt-api-web");
|
||||
|
||||
const result: ChatStoreSearchResult[] = [];
|
||||
for (const key of resultKeys) {
|
||||
// abort the operation if the signal is set
|
||||
if (signal.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Math.floor(
|
||||
(result.length / resultKeys.length) * 100,
|
||||
);
|
||||
if (now !== searchingNow) setSearchingNow(now);
|
||||
|
||||
const value: ChatStore = await db.get("chatgpt-api-web", key);
|
||||
const content = value.contents_for_index
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
if (content.includes(query)) {
|
||||
const beginIndex: number = content.indexOf(query);
|
||||
const preview = content.slice(
|
||||
Math.max(0, beginIndex - 100),
|
||||
Math.min(content.length, beginIndex + 239),
|
||||
);
|
||||
result.push({
|
||||
key,
|
||||
cs: value,
|
||||
query: query,
|
||||
preview: preview,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// sort by key desc
|
||||
result.sort((a, b) => {
|
||||
if (a.key < b.key) {
|
||||
return 1;
|
||||
}
|
||||
if (a.key > b.key) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
console.log(result);
|
||||
|
||||
setPageIndex(0);
|
||||
setSearchResult(result);
|
||||
setSearching(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{searching && <div>Searching {searchingNow}%...</div>}
|
||||
|
||||
<div>
|
||||
{searchResult
|
||||
.slice(pageIndex * 10, (pageIndex + 1) * 10)
|
||||
.map((result: ChatStoreSearchResult) => {
|
||||
return (
|
||||
<div
|
||||
className="flex justify-start p-1 m-1 rounded border bg-base-200 cursor-pointer"
|
||||
key={result.key}
|
||||
onClick={() => {
|
||||
props.setSelectedChatIndex(parseInt(result.key.toString()));
|
||||
props.setShow(false);
|
||||
}}
|
||||
>
|
||||
<div className="m-1 p-1 font-bold">{result.key}</div>
|
||||
<div className="m-1 p-1">{result.preview}</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{searchResult.length > 0 && (
|
||||
<div className="flex justify-center my-2">
|
||||
<div className="join">
|
||||
<button
|
||||
className="join-item btn btn-sm"
|
||||
disabled={pageIndex === 0}
|
||||
onClick={() => {
|
||||
if (pageIndex === 0) {
|
||||
return;
|
||||
}
|
||||
setPageIndex(pageIndex - 1);
|
||||
}}
|
||||
>
|
||||
«
|
||||
</button>
|
||||
<button className="join-item btn btn-sm">
|
||||
Page {pageIndex + 1} /{" "}
|
||||
{Math.floor(searchResult.length / 10) + 1}
|
||||
</button>
|
||||
<button
|
||||
className="join-item btn btn-sm"
|
||||
disabled={pageIndex === Math.floor(searchResult.length / 10)}
|
||||
onClick={() => {
|
||||
if (pageIndex === Math.floor(searchResult.length / 10)) {
|
||||
return;
|
||||
}
|
||||
setPageIndex(pageIndex + 1);
|
||||
}}
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TemplateAPI } from "./app";
|
||||
import { Tr } from "./translate";
|
||||
import { TemplateAPI } from "@/types/chatstore";
|
||||
import { Tr } from "@/translate";
|
||||
|
||||
interface Props {
|
||||
tmps: TemplateAPI[];
|
||||
@@ -17,7 +17,7 @@ export function SetAPIsTemplate({
|
||||
}: Props) {
|
||||
return (
|
||||
<button
|
||||
className="p-1 m-1 rounded bg-blue-300"
|
||||
className="btn btn-primary btn-sm mt-3"
|
||||
onClick={() => {
|
||||
const name = prompt(`Give this **${label}** template a name:`);
|
||||
if (!name) {
|
||||
|
||||
1311
src/settings.tsx
1311
src/settings.tsx
File diff suppressed because it is too large
Load Diff
@@ -3,7 +3,7 @@ export const autoHeight = (target: any) => {
|
||||
// max 70% of screen height
|
||||
target.style.height = `${Math.min(
|
||||
target.scrollHeight,
|
||||
window.innerHeight * 0.7
|
||||
window.innerHeight * 0.7,
|
||||
)}px`;
|
||||
console.log("set auto height", target.style.height);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createContext } from "preact";
|
||||
import MAP_zh_CN from "./zh_CN";
|
||||
import MAP_zh_CN from "@/translate/zh_CN";
|
||||
|
||||
interface LangOption {
|
||||
name: string;
|
||||
|
||||
13
src/tts.tsx
13
src/tts.tsx
@@ -1,6 +1,9 @@
|
||||
import { SpeakerWaveIcon } from "@heroicons/react/24/outline";
|
||||
import { useMemo, useState } from "preact/hooks";
|
||||
import { ChatStore, ChatStoreMessage, addTotalCost } from "./app";
|
||||
import { Message, getMessageText } from "./chatgpt";
|
||||
|
||||
import { addTotalCost } from "@/utils/totalCost";
|
||||
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
|
||||
import { Message, getMessageText } from "@/chatgpt";
|
||||
|
||||
interface TTSProps {
|
||||
chatStore: ChatStore;
|
||||
@@ -78,7 +81,11 @@ export default function TTSButton(props: TTSProps) {
|
||||
});
|
||||
}}
|
||||
>
|
||||
{generating ? "🤔" : "🔈"}
|
||||
{generating ? (
|
||||
<span className="loading loading-dots loading-xs"></span>
|
||||
) : (
|
||||
<SpeakerWaveIcon className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
72
src/types/chatstore.ts
Normal file
72
src/types/chatstore.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Logprobs, Message } from "@/chatgpt";
|
||||
|
||||
/**
|
||||
* ChatStore is the main object of the chatgpt-api-web,
|
||||
* stored in IndexedDB and passed across various components.
|
||||
* It contains all the information needed for a conversation.
|
||||
*/
|
||||
export interface ChatStore {
|
||||
chatgpt_api_web_version: string;
|
||||
systemMessageContent: string;
|
||||
toolsString: string;
|
||||
history: ChatStoreMessage[];
|
||||
postBeginIndex: number;
|
||||
tokenMargin: number;
|
||||
totalTokens: number;
|
||||
maxTokens: number;
|
||||
maxGenTokens: number;
|
||||
maxGenTokens_enabled: boolean;
|
||||
apiKey: string;
|
||||
apiEndpoint: string;
|
||||
streamMode: boolean;
|
||||
model: string;
|
||||
cost: number;
|
||||
temperature: number;
|
||||
temperature_enabled: boolean;
|
||||
top_p: number;
|
||||
top_p_enabled: boolean;
|
||||
presence_penalty: number;
|
||||
frequency_penalty: number;
|
||||
develop_mode: boolean;
|
||||
whisper_api: string;
|
||||
whisper_key: string;
|
||||
tts_api: string;
|
||||
tts_key: string;
|
||||
tts_voice: string;
|
||||
tts_speed: number;
|
||||
tts_speed_enabled: boolean;
|
||||
tts_format: string;
|
||||
image_gen_api: string;
|
||||
image_gen_key: string;
|
||||
json_mode: boolean;
|
||||
logprobs: boolean;
|
||||
contents_for_index: string[];
|
||||
}
|
||||
|
||||
export interface TemplateChatStore extends ChatStore {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TemplateAPI {
|
||||
name: string;
|
||||
key: string;
|
||||
endpoint: string;
|
||||
}
|
||||
|
||||
export interface TemplateTools {
|
||||
name: string;
|
||||
toolsString: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ChatStoreMessage extends the Message type defined by OpenAI.
|
||||
* It adds more fields to be stored within the ChatStore structure.
|
||||
*/
|
||||
export interface ChatStoreMessage extends Message {
|
||||
hide: boolean;
|
||||
token: number;
|
||||
example: boolean;
|
||||
audio: Blob | null;
|
||||
logprobs: Logprobs | null;
|
||||
response_model_name: string | null;
|
||||
}
|
||||
114
src/types/models.ts
Normal file
114
src/types/models.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
interface Model {
|
||||
maxToken: number;
|
||||
price: {
|
||||
prompt: number;
|
||||
completion: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const models: Record<string, Model> = {
|
||||
"gpt-4o": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 0.005 / 1000, completion: 0.015 / 1000 },
|
||||
},
|
||||
"gpt-4o-2024-11-20": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 0.0025 / 1000, completion: 0.01 / 1000 },
|
||||
},
|
||||
"gpt-4o-2024-08-06": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 0.0025 / 1000, completion: 0.01 / 1000 },
|
||||
},
|
||||
"gpt-4o-2024-05-13": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 0.005 / 1000, completion: 0.015 / 1000 },
|
||||
},
|
||||
"gpt-4o-mini": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 0.15 / 1000 / 1000, completion: 0.6 / 1000 / 1000 },
|
||||
},
|
||||
"gpt-4o-mini-2024-07-18": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 0.15 / 1000 / 1000, completion: 0.6 / 1000 / 1000 },
|
||||
},
|
||||
"o1-preview": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 15 / 1000 / 1000, completion: 60 / 1000 / 1000 },
|
||||
},
|
||||
"o1-preview-2024-09-12": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 15 / 1000 / 1000, completion: 60 / 1000 / 1000 },
|
||||
},
|
||||
"o1-mini": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 3 / 1000 / 1000, completion: 12 / 1000 / 1000 },
|
||||
},
|
||||
"o1-mini-2024-09-12": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 3 / 1000 / 1000, completion: 12 / 1000 / 1000 },
|
||||
},
|
||||
"chatgpt-4o-latest": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 0.005 / 1000, completion: 0.015 / 1000 },
|
||||
},
|
||||
"gpt-4-turbo": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 0.01 / 1000, completion: 0.03 / 1000 },
|
||||
},
|
||||
"gpt-4-turbo-2024-04-09": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 0.01 / 1000, completion: 0.03 / 1000 },
|
||||
},
|
||||
"gpt-4": {
|
||||
maxToken: 8192,
|
||||
price: { prompt: 0.03 / 1000, completion: 0.06 / 1000 },
|
||||
},
|
||||
"gpt-4-32k": {
|
||||
maxToken: 8192,
|
||||
price: { prompt: 0.06 / 1000, completion: 0.12 / 1000 },
|
||||
},
|
||||
"gpt-4-0125-preview": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 0.01 / 1000, completion: 0.03 / 1000 },
|
||||
},
|
||||
"gpt-4-1106-preview": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 0.01 / 1000, completion: 0.03 / 1000 },
|
||||
},
|
||||
"gpt-4-vision-preview": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 0.01 / 1000, completion: 0.03 / 1000 },
|
||||
},
|
||||
"gpt-4-1106-vision-preview": {
|
||||
maxToken: 128000,
|
||||
price: { prompt: 0.01 / 1000, completion: 0.03 / 1000 },
|
||||
},
|
||||
"gpt-3.5-turbo": {
|
||||
maxToken: 4096,
|
||||
price: { prompt: 0.0015 / 1000, completion: 0.002 / 1000 },
|
||||
},
|
||||
"gpt-3.5-turbo-0125": {
|
||||
maxToken: 16385,
|
||||
price: { prompt: 0.0005 / 1000, completion: 0.0015 / 1000 },
|
||||
},
|
||||
"gpt-3.5-turbo-instruct": {
|
||||
maxToken: 16385,
|
||||
price: { prompt: 0.5 / 1000 / 1000, completion: 2 / 1000 / 1000 },
|
||||
},
|
||||
"gpt-3.5-turbo-1106": {
|
||||
maxToken: 16385,
|
||||
price: { prompt: 0.001 / 1000, completion: 0.002 / 1000 },
|
||||
},
|
||||
"gpt-3.5-turbo-0613": {
|
||||
maxToken: 16385,
|
||||
price: { prompt: 1.5 / 1000 / 1000, completion: 2 / 1000 / 1000 },
|
||||
},
|
||||
"gpt-3.5-turbo-16k-0613": {
|
||||
maxToken: 16385,
|
||||
price: { prompt: 0.003 / 1000, completion: 0.004 / 1000 },
|
||||
},
|
||||
"gpt-3.5-turbo-0301": {
|
||||
maxToken: 16385,
|
||||
price: { prompt: 1.5 / 1000 / 1000, completion: 2 / 1000 / 1000 },
|
||||
},
|
||||
};
|
||||
93
src/types/newChatstore.ts
Normal file
93
src/types/newChatstore.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
DefaultAPIEndpoint,
|
||||
DefaultModel,
|
||||
CHATGPT_API_WEB_VERSION,
|
||||
} from "@/const";
|
||||
import { getDefaultParams } from "@/utils/getDefaultParam";
|
||||
import { ChatStore } from "@/types/chatstore";
|
||||
import { models } from "@/types/models";
|
||||
|
||||
interface NewChatStoreOptions {
|
||||
apiKey?: string;
|
||||
systemMessageContent?: string;
|
||||
apiEndpoint?: string;
|
||||
streamMode?: boolean;
|
||||
model?: string;
|
||||
temperature?: number;
|
||||
temperature_enabled?: boolean;
|
||||
top_p?: number;
|
||||
top_p_enabled?: boolean;
|
||||
presence_penalty?: number;
|
||||
frequency_penalty?: number;
|
||||
dev?: boolean;
|
||||
whisper_api?: string;
|
||||
whisper_key?: string;
|
||||
tts_api?: string;
|
||||
tts_key?: string;
|
||||
tts_voice?: string;
|
||||
tts_speed?: number;
|
||||
tts_speed_enabled?: boolean;
|
||||
tts_format?: string;
|
||||
toolsString?: string;
|
||||
image_gen_api?: string;
|
||||
image_gen_key?: string;
|
||||
json_mode?: boolean;
|
||||
logprobs?: boolean;
|
||||
}
|
||||
|
||||
export const newChatStore = (options: NewChatStoreOptions): ChatStore => {
|
||||
return {
|
||||
chatgpt_api_web_version: CHATGPT_API_WEB_VERSION,
|
||||
systemMessageContent: getDefaultParams(
|
||||
"sys",
|
||||
options.systemMessageContent ?? ""
|
||||
),
|
||||
toolsString: options.toolsString ?? "",
|
||||
history: [],
|
||||
postBeginIndex: 0,
|
||||
tokenMargin: 1024,
|
||||
totalTokens: 0,
|
||||
maxTokens: getDefaultParams(
|
||||
"max",
|
||||
models[getDefaultParams("model", options.model ?? DefaultModel)]
|
||||
?.maxToken ?? 2048
|
||||
),
|
||||
maxGenTokens: 2048,
|
||||
maxGenTokens_enabled: false,
|
||||
apiKey: getDefaultParams("key", options.apiKey ?? ""),
|
||||
apiEndpoint: getDefaultParams(
|
||||
"api",
|
||||
options.apiEndpoint ?? DefaultAPIEndpoint
|
||||
),
|
||||
streamMode: getDefaultParams("mode", options.streamMode ?? true),
|
||||
model: getDefaultParams("model", options.model ?? DefaultModel),
|
||||
cost: 0,
|
||||
temperature: getDefaultParams("temp", options.temperature ?? 1),
|
||||
temperature_enabled: options.temperature_enabled ?? true,
|
||||
top_p: options.top_p ?? 1,
|
||||
top_p_enabled: options.top_p_enabled ?? false,
|
||||
presence_penalty: options.presence_penalty ?? 0,
|
||||
frequency_penalty: options.frequency_penalty ?? 0,
|
||||
develop_mode: getDefaultParams("dev", options.dev ?? false),
|
||||
whisper_api: getDefaultParams(
|
||||
"whisper-api",
|
||||
options.whisper_api ?? "https://api.openai.com/v1/audio/transcriptions"
|
||||
),
|
||||
whisper_key: getDefaultParams("whisper-key", options.whisper_key ?? ""),
|
||||
tts_api: getDefaultParams(
|
||||
"tts-api",
|
||||
options.tts_api ?? "https://api.openai.com/v1/audio/speech"
|
||||
),
|
||||
tts_key: getDefaultParams("tts-key", options.tts_key ?? ""),
|
||||
tts_voice: options.tts_voice ?? "alloy",
|
||||
tts_speed: options.tts_speed ?? 1.0,
|
||||
tts_speed_enabled: options.tts_speed_enabled ?? false,
|
||||
image_gen_api:
|
||||
options.image_gen_api ?? "https://api.openai.com/v1/images/generations",
|
||||
image_gen_key: options.image_gen_key ?? "",
|
||||
json_mode: options.json_mode ?? false,
|
||||
tts_format: options.tts_format ?? "mp3",
|
||||
logprobs: options.logprobs ?? false,
|
||||
contents_for_index: [],
|
||||
};
|
||||
};
|
||||
@@ -1,7 +1,8 @@
|
||||
function getDefaultParams(param: string, val: string): string;
|
||||
function getDefaultParams(param: string, val: number): number;
|
||||
function getDefaultParams(param: string, val: boolean): boolean;
|
||||
function getDefaultParams(param: any, val: any) {
|
||||
export function getDefaultParams(param: string, val: string): string;
|
||||
export function getDefaultParams(param: string, val: number): number;
|
||||
export function getDefaultParams(param: string, val: boolean): boolean;
|
||||
|
||||
export function getDefaultParams(param: any, val: any) {
|
||||
const queryParameters = new URLSearchParams(window.location.search);
|
||||
const get = queryParameters.get(param);
|
||||
if (typeof val === "string") {
|
||||
@@ -16,5 +17,3 @@ function getDefaultParams(param: any, val: any) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
|
||||
export default getDefaultParams;
|
||||
18
src/utils/totalCost.ts
Normal file
18
src/utils/totalCost.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { STORAGE_NAME_TOTALCOST } from "@/const";
|
||||
|
||||
export function addTotalCost(cost: number) {
|
||||
let totalCost = getTotalCost();
|
||||
totalCost += cost;
|
||||
localStorage.setItem(STORAGE_NAME_TOTALCOST, `${totalCost}`);
|
||||
}
|
||||
|
||||
export function getTotalCost(): number {
|
||||
let totalCost = parseFloat(
|
||||
localStorage.getItem(STORAGE_NAME_TOTALCOST) ?? "0",
|
||||
);
|
||||
return totalCost;
|
||||
}
|
||||
|
||||
export function clearTotalCost() {
|
||||
localStorage.setItem(STORAGE_NAME_TOTALCOST, `0`);
|
||||
}
|
||||
@@ -1,8 +1,42 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
|
||||
daisyui: {
|
||||
themes: ["light",
|
||||
"dark",
|
||||
"cupcake",
|
||||
"bumblebee",
|
||||
"emerald",
|
||||
"corporate",
|
||||
"synthwave",
|
||||
"retro",
|
||||
"cyberpunk",
|
||||
"valentine",
|
||||
"halloween",
|
||||
"garden",
|
||||
"forest",
|
||||
"aqua",
|
||||
"lofi",
|
||||
"pastel",
|
||||
"fantasy",
|
||||
"wireframe",
|
||||
"black",
|
||||
"luxury",
|
||||
"dracula",
|
||||
"cmyk",
|
||||
"autumn",
|
||||
"business",
|
||||
"acid",
|
||||
"lemonade",
|
||||
"night",
|
||||
"coffee",
|
||||
"winter",
|
||||
"dim",
|
||||
"nord",
|
||||
"sunset",],
|
||||
},
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [require('daisyui')],
|
||||
};
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": "src",
|
||||
"paths": {
|
||||
"@/*": ["*"]
|
||||
},
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import preact from '@preact/preset-vite'
|
||||
import path from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [preact()],
|
||||
base: './',
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, 'src')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user