Compare commits
22 Commits
f0f040c42c
...
6aca74a7b4
| Author | SHA1 | Date | |
|---|---|---|---|
|
6aca74a7b4
|
|||
|
a763355420
|
|||
|
8122f6d8bf
|
|||
|
31c49ff888
|
|||
|
fd5f87f845
|
|||
|
587d0ba57d
|
|||
|
795cb16ed4
|
|||
|
47d96198e8
|
|||
|
eb8a6dc8ed
|
|||
|
32866d9a7f
|
|||
|
9cfb09a5ac
|
|||
|
7196799625
|
|||
|
915987cbfe
|
|||
|
9855027876
|
|||
|
2670183343
|
|||
|
ad291bd72e
|
|||
|
1fbd4ee87b
|
|||
|
af2ae82e74
|
|||
|
9e74e419c9
|
|||
|
ee9da49f70
|
|||
|
dccf4827c9
|
|||
|
04bac03fd7
|
@@ -13,10 +13,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 20.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
- run: npm install
|
- run: npm install
|
||||||
- run: npm run build
|
- run: npm run build
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
const CHATGPT_API_WEB_VERSION = "v2.1.0";
|
|
||||||
|
|
||||||
export default CHATGPT_API_WEB_VERSION;
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { ChatStore } from "@/app";
|
import { ChatStore } from "@/types/chatstore";
|
||||||
import { MessageDetail } from "@/chatgpt";
|
import { MessageDetail } from "@/chatgpt";
|
||||||
import { Tr } from "@/translate";
|
import { Tr } from "@/translate";
|
||||||
|
|
||||||
|
|||||||
501
src/app.tsx
501
src/app.tsx
@@ -1,501 +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;
|
|
||||||
contents_for_index: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const _defaultAPIEndpoint = "https://api.openai.com/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 = false,
|
|
||||||
): 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: false,
|
|
||||||
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,
|
|
||||||
contents_for_index: [],
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
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 BuildFiledForSearch(chatStore: ChatStore): string[] {
|
|
||||||
const contents_for_index: string[] = [];
|
|
||||||
|
|
||||||
if (chatStore.systemMessageContent.trim()) {
|
|
||||||
contents_for_index.push(chatStore.systemMessageContent.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const msg of chatStore.history) {
|
|
||||||
if (typeof msg.content === "string") {
|
|
||||||
contents_for_index.push(msg.content);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const chunk of msg.content) {
|
|
||||||
if (chunk.type === "text") {
|
|
||||||
const text = chunk.text;
|
|
||||||
if (text?.trim()) {
|
|
||||||
contents_for_index.push(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return contents_for_index;
|
|
||||||
}
|
|
||||||
|
|
||||||
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, {
|
|
||||||
async upgrade(db, oldVersion, newVersion, transaction) {
|
|
||||||
if (oldVersion < 1) {
|
|
||||||
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. 🎉",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldVersion < 11) {
|
|
||||||
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");
|
|
||||||
}
|
|
||||||
transaction.objectStore(STORAGE_NAME).createIndex(
|
|
||||||
"contents_for_index", // name
|
|
||||||
"contents_for_index", // keyPath
|
|
||||||
{
|
|
||||||
multiEntry: true,
|
|
||||||
unique: false,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// iter through all chatStore and update contents_for_index
|
|
||||||
const store = transaction.objectStore(STORAGE_NAME);
|
|
||||||
const allChatStoreIndexes = await store.getAllKeys();
|
|
||||||
for (const i of allChatStoreIndexes) {
|
|
||||||
const chatStore: ChatStore = await store.get(i);
|
|
||||||
|
|
||||||
chatStore.contents_for_index = BuildFiledForSearch(chatStore);
|
|
||||||
await store.put(chatStore, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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) => {
|
|
||||||
// building field for search
|
|
||||||
chatStore.contents_for_index = BuildFiledForSearch(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,
|
|
||||||
false, // logprobs default to false
|
|
||||||
),
|
|
||||||
);
|
|
||||||
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">
|
|
||||||
<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 () => {
|
|
||||||
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="btn btn-sm btn-warning p-1 my-1 w-full"
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
<ChatBOX
|
|
||||||
db={db}
|
|
||||||
chatStore={chatStore}
|
|
||||||
setChatStore={setChatStore}
|
|
||||||
selectedChatIndex={selectedChatIndex}
|
|
||||||
setSelectedChatIndex={setSelectedChatIndex}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { defaultModel } from "@/models";
|
import { DefaultModel } from "@/const";
|
||||||
|
|
||||||
export interface ImageURL {
|
export interface ImageURL {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -157,7 +157,7 @@ class Chat {
|
|||||||
enable_max_gen_tokens = true,
|
enable_max_gen_tokens = true,
|
||||||
tokens_margin = 1024,
|
tokens_margin = 1024,
|
||||||
apiEndPoint = "https://api.openai.com/v1/chat/completions",
|
apiEndPoint = "https://api.openai.com/v1/chat/completions",
|
||||||
model = defaultModel,
|
model = DefaultModel,
|
||||||
temperature = 0.7,
|
temperature = 0.7,
|
||||||
enable_temperature = true,
|
enable_temperature = true,
|
||||||
top_p = 1,
|
top_p = 1,
|
||||||
|
|||||||
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,6 +1,6 @@
|
|||||||
import { useState, useEffect, StateUpdater, Dispatch } from "preact/hooks";
|
import { useState, useEffect, StateUpdater, Dispatch } from "preact/hooks";
|
||||||
import { Tr, langCodeContext, LANG_OPTIONS, tr } from "@/translate";
|
import { Tr, langCodeContext, LANG_OPTIONS, tr } from "@/translate";
|
||||||
import { ChatStore, ChatStoreMessage } from "@/app";
|
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
|
||||||
import { EditMessageString } from "@/editMessageString";
|
import { EditMessageString } from "@/editMessageString";
|
||||||
import { EditMessageDetail } from "@/editMessageDetail";
|
import { EditMessageDetail } from "@/editMessageDetail";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChatStore, ChatStoreMessage } from "@/app";
|
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
|
||||||
import { calculate_token_length } from "@/chatgpt";
|
import { calculate_token_length } from "@/chatgpt";
|
||||||
import { Tr } from "@/translate";
|
import { Tr } from "@/translate";
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChatStore, ChatStoreMessage } from "@/app";
|
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
|
||||||
import { isVailedJSON } from "@/message";
|
import { isVailedJSON } from "@/message";
|
||||||
import { calculate_token_length } from "@/chatgpt";
|
import { calculate_token_length } from "@/chatgpt";
|
||||||
import { Tr } from "@/translate";
|
import { Tr } from "@/translate";
|
||||||
|
|||||||
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,4 +1,4 @@
|
|||||||
import { ChatStore, TemplateAPI } from "@/app";
|
import { ChatStore, TemplateAPI } from "@/types/chatstore";
|
||||||
import { Tr } from "@/translate";
|
import { Tr } from "@/translate";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChatStore, TemplateTools } from "@/app";
|
import { ChatStore, TemplateTools } from "@/types/chatstore";
|
||||||
import { Tr } from "@/translate";
|
import { Tr } from "@/translate";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { themeChange } from "theme-change";
|
import { themeChange } from "theme-change";
|
||||||
import { render } from "preact";
|
import { render } from "preact";
|
||||||
import { useState, useEffect } from "preact/hooks";
|
import { useState, useEffect } from "preact/hooks";
|
||||||
import { App } from "@/app";
|
import { App } from "@/pages/App";
|
||||||
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
||||||
|
|
||||||
function Base() {
|
function Base() {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Markdown from "preact-markdown";
|
|||||||
import { useState, useEffect, StateUpdater } from "preact/hooks";
|
import { useState, useEffect, StateUpdater } from "preact/hooks";
|
||||||
|
|
||||||
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
||||||
import { ChatStore, ChatStoreMessage } from "@/app";
|
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
|
||||||
import { calculate_token_length, getMessageText } from "@/chatgpt";
|
import { calculate_token_length, getMessageText } from "@/chatgpt";
|
||||||
import TTSButton, { TTSPlay } from "@/tts";
|
import TTSButton, { TTSPlay } from "@/tts";
|
||||||
import { MessageHide } from "@/messageHide";
|
import { MessageHide } from "@/messageHide";
|
||||||
@@ -26,7 +26,6 @@ interface Props {
|
|||||||
messageIndex: number;
|
messageIndex: number;
|
||||||
chatStore: ChatStore;
|
chatStore: ChatStore;
|
||||||
setChatStore: (cs: ChatStore) => void;
|
setChatStore: (cs: ChatStore) => void;
|
||||||
update_total_tokens: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Message(props: Props) {
|
export default function Message(props: Props) {
|
||||||
@@ -205,7 +204,6 @@ export default function Message(props: Props) {
|
|||||||
className="input input-bordered input-xs w-16"
|
className="input input-bordered input-xs w-16"
|
||||||
onChange={(event: any) => {
|
onChange={(event: any) => {
|
||||||
chat.token = parseInt(event.target.value);
|
chat.token = parseInt(event.target.value);
|
||||||
props.update_total_tokens();
|
|
||||||
setChatStore({ ...chatStore });
|
setChatStore({ ...chatStore });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChatStoreMessage } from "@/app";
|
import { ChatStoreMessage } from "@/types/chatstore";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
chat: ChatStoreMessage;
|
chat: ChatStoreMessage;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChatStoreMessage } from "@/app";
|
import { ChatStoreMessage } from "@/types/chatstore";
|
||||||
import { getMessageText } from "@/chatgpt";
|
import { getMessageText } from "@/chatgpt";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChatStoreMessage } from "@/app";
|
import { ChatStoreMessage } from "@/types/chatstore";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
chat: ChatStoreMessage;
|
chat: ChatStoreMessage;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ChatStoreMessage } from "@/app";
|
import { ChatStoreMessage } from "@/types/chatstore";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
chat: ChatStoreMessage;
|
chat: ChatStoreMessage;
|
||||||
|
|||||||
92
src/pages/AddToolMsg.tsx
Normal file
92
src/pages/AddToolMsg.tsx
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
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,
|
||||||
|
});
|
||||||
|
setChatStore({ ...chatStore });
|
||||||
|
setNewToolCallID("");
|
||||||
|
setNewToolContent("");
|
||||||
|
setShowAddToolMsg(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Tr("Add")}
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddToolMsg;
|
||||||
236
src/pages/App.tsx
Normal file
236
src/pages/App.tsx
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
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.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);
|
||||||
|
|
||||||
|
// 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,54 +1,44 @@
|
|||||||
import {
|
|
||||||
MagnifyingGlassIcon,
|
|
||||||
CubeIcon,
|
|
||||||
BanknotesIcon,
|
|
||||||
DocumentTextIcon,
|
|
||||||
ChatBubbleLeftEllipsisIcon,
|
|
||||||
ScissorsIcon,
|
|
||||||
SwatchIcon,
|
|
||||||
SparklesIcon,
|
|
||||||
} from "@heroicons/react/24/outline";
|
|
||||||
|
|
||||||
import { IDBPDatabase } from "idb";
|
import { IDBPDatabase } from "idb";
|
||||||
import structuredClone from "@ungap/structured-clone";
|
|
||||||
import { createRef } from "preact";
|
import { createRef } from "preact";
|
||||||
import { StateUpdater, useEffect, useState, Dispatch } from "preact/hooks";
|
import { StateUpdater, useEffect, useState, Dispatch } from "preact/hooks";
|
||||||
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
||||||
import {
|
import {
|
||||||
ChatStore,
|
|
||||||
ChatStoreMessage,
|
|
||||||
STORAGE_NAME_TEMPLATE,
|
STORAGE_NAME_TEMPLATE,
|
||||||
STORAGE_NAME_TEMPLATE_API,
|
STORAGE_NAME_TEMPLATE_API,
|
||||||
STORAGE_NAME_TEMPLATE_API_IMAGE_GEN,
|
STORAGE_NAME_TEMPLATE_API_IMAGE_GEN,
|
||||||
STORAGE_NAME_TEMPLATE_API_TTS,
|
STORAGE_NAME_TEMPLATE_API_TTS,
|
||||||
STORAGE_NAME_TEMPLATE_API_WHISPER,
|
STORAGE_NAME_TEMPLATE_API_WHISPER,
|
||||||
STORAGE_NAME_TEMPLATE_TOOLS,
|
STORAGE_NAME_TEMPLATE_TOOLS,
|
||||||
TemplateAPI,
|
} from "@/const";
|
||||||
TemplateTools,
|
import { addTotalCost, getTotalCost } from "@/utils/totalCost";
|
||||||
addTotalCost,
|
|
||||||
getTotalCost,
|
|
||||||
} from "@/app";
|
|
||||||
import ChatGPT, {
|
import ChatGPT, {
|
||||||
calculate_token_length,
|
calculate_token_length,
|
||||||
ChunkMessage,
|
|
||||||
FetchResponse,
|
FetchResponse,
|
||||||
Message as MessageType,
|
Message as MessageType,
|
||||||
MessageDetail,
|
MessageDetail,
|
||||||
ToolCall,
|
ToolCall,
|
||||||
Logprobs,
|
Logprobs,
|
||||||
} from "@/chatgpt";
|
} from "@/chatgpt";
|
||||||
|
import {
|
||||||
|
ChatStore,
|
||||||
|
ChatStoreMessage,
|
||||||
|
TemplateChatStore,
|
||||||
|
TemplateAPI,
|
||||||
|
TemplateTools,
|
||||||
|
} from "../types/chatstore";
|
||||||
import Message from "@/message";
|
import Message from "@/message";
|
||||||
import models from "@/models";
|
import { models } from "@/types/models";
|
||||||
import Settings from "@/settings";
|
import Settings from "@/settings";
|
||||||
import getDefaultParams from "@/getDefaultParam";
|
|
||||||
import { AddImage } from "@/addImage";
|
import { AddImage } from "@/addImage";
|
||||||
import { ListAPIs } from "@/listAPIs";
|
import { ListAPIs } from "@/listAPIs";
|
||||||
import { ListToolsTempaltes } from "@/listToolsTemplates";
|
import { ListToolsTempaltes } from "@/listToolsTemplates";
|
||||||
import { autoHeight } from "@/textarea";
|
import { autoHeight } from "@/textarea";
|
||||||
import Search from "@/search";
|
import Search from "@/search";
|
||||||
export interface TemplateChatStore extends ChatStore {
|
import Templates from "@/components/Templates";
|
||||||
name: string;
|
import VersionHint from "@/components/VersionHint";
|
||||||
}
|
import StatusBar from "@/components/StatusBar";
|
||||||
|
import WhisperButton from "@/components/WhisperButton";
|
||||||
|
import AddToolMsg from "./AddToolMsg";
|
||||||
|
|
||||||
export default function ChatBOX(props: {
|
export default function ChatBOX(props: {
|
||||||
db: Promise<IDBPDatabase<ChatStore>>;
|
db: Promise<IDBPDatabase<ChatStore>>;
|
||||||
@@ -66,17 +56,13 @@ export default function ChatBOX(props: {
|
|||||||
const [showGenerating, setShowGenerating] = useState(false);
|
const [showGenerating, setShowGenerating] = useState(false);
|
||||||
const [generatingMessage, setGeneratingMessage] = useState("");
|
const [generatingMessage, setGeneratingMessage] = useState("");
|
||||||
const [showRetry, setShowRetry] = useState(false);
|
const [showRetry, setShowRetry] = useState(false);
|
||||||
const [isRecording, setIsRecording] = useState("Mic");
|
|
||||||
const [showAddToolMsg, setShowAddToolMsg] = useState(false);
|
const [showAddToolMsg, setShowAddToolMsg] = useState(false);
|
||||||
const [newToolCallID, setNewToolCallID] = useState("");
|
|
||||||
const [newToolContent, setNewToolContent] = useState("");
|
|
||||||
const [showSearch, setShowSearch] = useState(false);
|
const [showSearch, setShowSearch] = useState(false);
|
||||||
let default_follow = localStorage.getItem("follow");
|
let default_follow = localStorage.getItem("follow");
|
||||||
if (default_follow === null) {
|
if (default_follow === null) {
|
||||||
default_follow = "true";
|
default_follow = "true";
|
||||||
}
|
}
|
||||||
const [follow, _setFollow] = useState(default_follow === "true");
|
const [follow, _setFollow] = useState(default_follow === "true");
|
||||||
const mediaRef = createRef();
|
|
||||||
|
|
||||||
const setFollow = (follow: boolean) => {
|
const setFollow = (follow: boolean) => {
|
||||||
console.log("set follow", follow);
|
console.log("set follow", follow);
|
||||||
@@ -93,19 +79,6 @@ export default function ChatBOX(props: {
|
|||||||
|
|
||||||
const client = new ChatGPT(chatStore.apiKey);
|
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) => {
|
const _completeWithStreamMode = async (response: Response) => {
|
||||||
let responseTokenCount = 0;
|
let responseTokenCount = 0;
|
||||||
const allChunkMessage: string[] = [];
|
const allChunkMessage: string[] = [];
|
||||||
@@ -208,7 +181,6 @@ export default function ChatBOX(props: {
|
|||||||
// manually copy status from client to chatStore
|
// manually copy status from client to chatStore
|
||||||
chatStore.maxTokens = client.max_tokens;
|
chatStore.maxTokens = client.max_tokens;
|
||||||
chatStore.tokenMargin = client.tokens_margin;
|
chatStore.tokenMargin = client.tokens_margin;
|
||||||
update_total_tokens();
|
|
||||||
setChatStore({ ...chatStore });
|
setChatStore({ ...chatStore });
|
||||||
setGeneratingMessage("");
|
setGeneratingMessage("");
|
||||||
setShowGenerating(false);
|
setShowGenerating(false);
|
||||||
@@ -475,244 +447,12 @@ export default function ChatBOX(props: {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="navbar bg-base-100 p-0">
|
<StatusBar
|
||||||
<div className="navbar-start">
|
chatStore={chatStore}
|
||||||
<div className="dropdown lg:hidden">
|
setShowSettings={setShowSettings}
|
||||||
<div
|
setShowSearch={setShowSearch}
|
||||||
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>
|
|
||||||
{/* <div
|
|
||||||
className="relative cursor-pointer rounded p-2"
|
|
||||||
onClick={() => setShowSettings(true)}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="absolute right-1 rounded p-1 m-1"
|
|
||||||
onClick={(event) => {
|
|
||||||
// stop propagation to parent
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
setShowSearch(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<MagnifyingGlassIcon className="w-5 h-5" />
|
|
||||||
</button>
|
|
||||||
<div className="hidden lg:inline-grid"></div>
|
|
||||||
<div className="lg:hidden">
|
|
||||||
<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>
|
|
||||||
</div> */}
|
|
||||||
<div className="grow overflow-scroll">
|
<div className="grow overflow-scroll">
|
||||||
{!chatStore.apiKey && (
|
{!chatStore.apiKey && (
|
||||||
<p className="bg-base-200 p-6 rounded my-3 text-left">
|
<p className="bg-base-200 p-6 rounded my-3 text-left">
|
||||||
@@ -799,104 +539,12 @@ export default function ChatBOX(props: {
|
|||||||
</h2>
|
</h2>
|
||||||
<div className="divider"></div>
|
<div className="divider"></div>
|
||||||
<div className="flex flex-wrap">
|
<div className="flex flex-wrap">
|
||||||
{templates.map((t, index) => (
|
<Templates
|
||||||
<div
|
templates={templates}
|
||||||
className="cursor-pointer rounded bg-green-400 w-fit p-2 m-1 flex flex-col"
|
setTemplates={setTemplates}
|
||||||
onClick={() => {
|
chatStore={chatStore}
|
||||||
const newChatStore: ChatStore = structuredClone(t);
|
setChatStore={setChatStore}
|
||||||
// @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>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -930,7 +578,6 @@ export default function ChatBOX(props: {
|
|||||||
chatStore={chatStore}
|
chatStore={chatStore}
|
||||||
setChatStore={setChatStore}
|
setChatStore={setChatStore}
|
||||||
messageIndex={messageIndex}
|
messageIndex={messageIndex}
|
||||||
update_total_tokens={update_total_tokens}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{showGenerating && (
|
{showGenerating && (
|
||||||
@@ -951,7 +598,6 @@ export default function ChatBOX(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//chatStore.totalTokens =
|
//chatStore.totalTokens =
|
||||||
update_total_tokens();
|
|
||||||
setChatStore({ ...chatStore });
|
setChatStore({ ...chatStore });
|
||||||
|
|
||||||
await complete();
|
await complete();
|
||||||
@@ -986,43 +632,7 @@ export default function ChatBOX(props: {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
{chatStore.chatgpt_api_web_version < "v1.3.0" && (
|
<VersionHint chatStore={chatStore} />
|
||||||
<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>
|
|
||||||
)}
|
|
||||||
{showRetry && (
|
{showRetry && (
|
||||||
<p className="text-right p-2 my-2 dark:text-white">
|
<p className="text-right p-2 my-2 dark:text-white">
|
||||||
<button
|
<button
|
||||||
@@ -1122,123 +732,12 @@ export default function ChatBOX(props: {
|
|||||||
>
|
>
|
||||||
{Tr("Send")}
|
{Tr("Send")}
|
||||||
</button>
|
</button>
|
||||||
{chatStore.whisper_api &&
|
{chatStore.whisper_api && chatStore.whisper_key && (
|
||||||
chatStore.whisper_key &&
|
<WhisperButton
|
||||||
(chatStore.whisper_key || chatStore.apiKey) && (
|
chatStore={chatStore}
|
||||||
<button
|
inputMsg={inputMsg}
|
||||||
className={`btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1 ${
|
setInputMsg={setInputMsg}
|
||||||
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>
|
|
||||||
)}
|
)}
|
||||||
{chatStore.develop_mode && (
|
{chatStore.develop_mode && (
|
||||||
<button
|
<button
|
||||||
@@ -1256,7 +755,6 @@ export default function ChatBOX(props: {
|
|||||||
audio: null,
|
audio: null,
|
||||||
logprobs: null,
|
logprobs: null,
|
||||||
});
|
});
|
||||||
update_total_tokens();
|
|
||||||
setInputMsg("");
|
setInputMsg("");
|
||||||
setChatStore({ ...chatStore });
|
setChatStore({ ...chatStore });
|
||||||
}}
|
}}
|
||||||
@@ -1287,82 +785,11 @@ export default function ChatBOX(props: {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{showAddToolMsg && (
|
{showAddToolMsg && (
|
||||||
<div
|
<AddToolMsg
|
||||||
className="absolute z-10 bg-black bg-opacity-50 w-full h-full flex justify-center items-center left-0 top-0 overflow-scroll"
|
chatStore={chatStore}
|
||||||
onClick={() => {
|
setChatStore={setChatStore}
|
||||||
setShowAddToolMsg(false);
|
setShowAddToolMsg={setShowAddToolMsg}
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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,
|
|
||||||
});
|
|
||||||
update_total_tokens();
|
|
||||||
setChatStore({ ...chatStore });
|
|
||||||
setNewToolCallID("");
|
|
||||||
setNewToolContent("");
|
|
||||||
setShowAddToolMsg(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Tr("Add")}
|
|
||||||
</button>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { IDBPDatabase } from "idb";
|
import { IDBPDatabase } from "idb";
|
||||||
import { StateUpdater, useRef, useState, Dispatch } from "preact/hooks";
|
import { StateUpdater, useRef, useState, Dispatch } from "preact/hooks";
|
||||||
|
|
||||||
import { ChatStore } from "@/app";
|
import { ChatStore } from "@/types/chatstore";
|
||||||
|
|
||||||
interface ChatStoreSearchResult {
|
interface ChatStoreSearchResult {
|
||||||
key: IDBValidKey;
|
key: IDBValidKey;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { TemplateAPI } from "@/app";
|
import { TemplateAPI } from "@/types/chatstore";
|
||||||
import { Tr } from "@/translate";
|
import { Tr } from "@/translate";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
|||||||
@@ -21,20 +21,19 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
Dispatch,
|
Dispatch,
|
||||||
} from "preact/hooks";
|
} from "preact/hooks";
|
||||||
|
import { clearTotalCost, getTotalCost } from "@/utils/totalCost";
|
||||||
import {
|
import {
|
||||||
ChatStore,
|
ChatStore,
|
||||||
|
TemplateChatStore,
|
||||||
TemplateAPI,
|
TemplateAPI,
|
||||||
TemplateTools,
|
TemplateTools,
|
||||||
clearTotalCost,
|
} from "@/types/chatstore";
|
||||||
getTotalCost,
|
import { models } from "@/types/models";
|
||||||
} from "@/app";
|
|
||||||
import models from "@/models";
|
|
||||||
import { TemplateChatStore } from "@/chatbox";
|
|
||||||
import { tr, Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
import { tr, Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
|
||||||
import { isVailedJSON } from "@/message";
|
import { isVailedJSON } from "@/message";
|
||||||
import { SetAPIsTemplate } from "@/setAPIsTemplate";
|
import { SetAPIsTemplate } from "@/setAPIsTemplate";
|
||||||
import { autoHeight } from "@/textarea";
|
import { autoHeight } from "@/textarea";
|
||||||
import getDefaultParams from "@/getDefaultParam";
|
import { getDefaultParams } from "@/utils/getDefaultParam";
|
||||||
|
|
||||||
const TTS_VOICES: string[] = [
|
const TTS_VOICES: string[] = [
|
||||||
"alloy",
|
"alloy",
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { SpeakerWaveIcon } from "@heroicons/react/24/outline";
|
import { SpeakerWaveIcon } from "@heroicons/react/24/outline";
|
||||||
import { useMemo, useState } from "preact/hooks";
|
import { useMemo, useState } from "preact/hooks";
|
||||||
|
|
||||||
import { ChatStore, ChatStoreMessage, addTotalCost } from "@/app";
|
import { addTotalCost } from "@/utils/totalCost";
|
||||||
|
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
|
||||||
import { Message, getMessageText } from "@/chatgpt";
|
import { Message, getMessageText } from "@/chatgpt";
|
||||||
|
|
||||||
interface TTSProps {
|
interface TTSProps {
|
||||||
|
|||||||
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;
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ interface Model {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const models: Record<string, Model> = {
|
export const models: Record<string, Model> = {
|
||||||
"gpt-4o": {
|
"gpt-4o": {
|
||||||
maxToken: 128000,
|
maxToken: 128000,
|
||||||
price: { prompt: 0.005 / 1000, completion: 0.015 / 1000 },
|
price: { prompt: 0.005 / 1000, completion: 0.015 / 1000 },
|
||||||
@@ -80,7 +80,3 @@ const models: Record<string, Model> = {
|
|||||||
price: { prompt: 0.06 / 1000, completion: 0.12 / 1000 },
|
price: { prompt: 0.06 / 1000, completion: 0.12 / 1000 },
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultModel = "gpt-4o-mini";
|
|
||||||
|
|
||||||
export default models;
|
|
||||||
88
src/types/newChatstore.ts
Normal file
88
src/types/newChatstore.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
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;
|
||||||
|
dev?: boolean;
|
||||||
|
whisper_api?: string;
|
||||||
|
whisper_key?: string;
|
||||||
|
tts_api?: string;
|
||||||
|
tts_key?: 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),
|
||||||
|
responseModelName: "",
|
||||||
|
cost: 0,
|
||||||
|
temperature: getDefaultParams("temp", options.temperature ?? 0.7),
|
||||||
|
temperature_enabled: true,
|
||||||
|
top_p: 1,
|
||||||
|
top_p_enabled: false,
|
||||||
|
presence_penalty: 0,
|
||||||
|
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: "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;
|
export function getDefaultParams(param: string, val: string): string;
|
||||||
function getDefaultParams(param: string, val: number): number;
|
export function getDefaultParams(param: string, val: number): number;
|
||||||
function getDefaultParams(param: string, val: boolean): boolean;
|
export function getDefaultParams(param: string, val: boolean): boolean;
|
||||||
function getDefaultParams(param: any, val: any) {
|
|
||||||
|
export function getDefaultParams(param: any, val: any) {
|
||||||
const queryParameters = new URLSearchParams(window.location.search);
|
const queryParameters = new URLSearchParams(window.location.search);
|
||||||
const get = queryParameters.get(param);
|
const get = queryParameters.get(param);
|
||||||
if (typeof val === "string") {
|
if (typeof val === "string") {
|
||||||
@@ -16,5 +17,3 @@ function getDefaultParams(param: any, val: any) {
|
|||||||
return val;
|
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`);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user