Compare commits

...

22 Commits

Author SHA1 Message Date
6aca74a7b4 remove: update_total_token()
All checks were successful
Build static content / build (push) Successful in 8m10s
2024-10-15 18:20:21 +08:00
a763355420 rename: tsx 2024-10-15 18:01:13 +08:00
8122f6d8bf refac: pages/addToolMsg.tx 2024-10-15 18:00:25 +08:00
31c49ff888 refac: components/WhisperButton.tsx 2024-10-15 17:52:32 +08:00
fd5f87f845 refac: components/StatusBar.tsx 2024-10-15 17:40:52 +08:00
587d0ba57d rename components 2024-10-15 17:34:01 +08:00
795cb16ed4 refac: componets/versionHint.tsx 2024-10-15 17:32:33 +08:00
47d96198e8 refac: components/templates.tsx 2024-10-15 17:27:56 +08:00
eb8a6dc8ed move chatbox.tsx to pages/ 2024-10-15 17:20:28 +08:00
32866d9a7f move: app.tsx to pages/ 2024-10-15 17:14:44 +08:00
9cfb09a5ac refac: app.tsx 2024-10-15 17:13:21 +08:00
7196799625 remove; buildFieldForSearch 2024-10-15 16:48:18 +08:00
915987cbfe refac: @/indexed/upgrade 2024-10-15 16:47:03 +08:00
9855027876 refac: @/utils/buildForSearch.ts 2024-10-15 15:11:21 +08:00
2670183343 refact: @/utils/totalCost.tx 2024-10-15 15:07:05 +08:00
ad291bd72e refac: move const STORAGE_NAME 2024-10-15 15:04:27 +08:00
1fbd4ee87b refac: export getDefaultParam 2024-10-15 15:01:51 +08:00
af2ae82e74 refac: newChatStore to use options 2024-10-15 14:59:20 +08:00
9e74e419c9 bump gitlab ci node to version 20.x 2024-10-15 14:57:57 +08:00
ee9da49f70 refac: models newChatStore 2024-10-15 10:34:35 +08:00
dccf4827c9 refac: @/types/chatstore.ts 2024-10-15 10:18:20 +08:00
04bac03fd7 move chatstore to @types/chatsotre.ts 2024-10-15 09:44:40 +08:00
36 changed files with 1175 additions and 1154 deletions

View File

@@ -13,10 +13,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js 18.x
- name: Use Node.js 20.x
uses: actions/setup-node@v3
with:
node-version: 18.x
node-version: 20.x
cache: 'npm'
- run: npm install
- run: npm run build

View File

@@ -1,3 +0,0 @@
const CHATGPT_API_WEB_VERSION = "v2.1.0";
export default CHATGPT_API_WEB_VERSION;

View File

@@ -1,5 +1,5 @@
import { useState } from "preact/hooks";
import { ChatStore } from "@/app";
import { ChatStore } from "@/types/chatstore";
import { MessageDetail } from "@/chatgpt";
import { Tr } from "@/translate";

View File

@@ -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>
);
}

View File

@@ -1,4 +1,4 @@
import { defaultModel } from "@/models";
import { DefaultModel } from "@/const";
export interface ImageURL {
url: string;
@@ -157,7 +157,7 @@ class Chat {
enable_max_gen_tokens = true,
tokens_margin = 1024,
apiEndPoint = "https://api.openai.com/v1/chat/completions",
model = defaultModel,
model = DefaultModel,
temperature = 0.7,
enable_temperature = true,
top_p = 1,

View 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;

View 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;

View 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;

View 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
View 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`;

View File

@@ -1,6 +1,6 @@
import { useState, useEffect, StateUpdater, Dispatch } from "preact/hooks";
import { Tr, langCodeContext, LANG_OPTIONS, tr } from "@/translate";
import { ChatStore, ChatStoreMessage } from "@/app";
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
import { EditMessageString } from "@/editMessageString";
import { EditMessageDetail } from "@/editMessageDetail";

View File

@@ -1,4 +1,4 @@
import { ChatStore, ChatStoreMessage } from "@/app";
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
import { calculate_token_length } from "@/chatgpt";
import { Tr } from "@/translate";

View File

@@ -1,4 +1,4 @@
import { ChatStore, ChatStoreMessage } from "@/app";
import { ChatStore, ChatStoreMessage } from "@/types/chatstore";
import { isVailedJSON } from "@/message";
import { calculate_token_length } from "@/chatgpt";
import { Tr } from "@/translate";

24
src/indexedDB/upgrade.ts Normal file
View 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
View 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
View 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");
}
}

View File

@@ -1,4 +1,4 @@
import { ChatStore, TemplateAPI } from "@/app";
import { ChatStore, TemplateAPI } from "@/types/chatstore";
import { Tr } from "@/translate";
interface Props {

View File

@@ -1,4 +1,4 @@
import { ChatStore, TemplateTools } from "@/app";
import { ChatStore, TemplateTools } from "@/types/chatstore";
import { Tr } from "@/translate";
interface Props {

View File

@@ -1,7 +1,7 @@
import { themeChange } from "theme-change";
import { render } from "preact";
import { useState, useEffect } from "preact/hooks";
import { App } from "@/app";
import { App } from "@/pages/App";
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
function Base() {

View File

@@ -3,7 +3,7 @@ import Markdown from "preact-markdown";
import { useState, useEffect, StateUpdater } from "preact/hooks";
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 TTSButton, { TTSPlay } from "@/tts";
import { MessageHide } from "@/messageHide";
@@ -26,7 +26,6 @@ interface Props {
messageIndex: number;
chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void;
update_total_tokens: () => void;
}
export default function Message(props: Props) {
@@ -205,7 +204,6 @@ export default function Message(props: Props) {
className="input input-bordered input-xs w-16"
onChange={(event: any) => {
chat.token = parseInt(event.target.value);
props.update_total_tokens();
setChatStore({ ...chatStore });
}}
/>

View File

@@ -1,4 +1,4 @@
import { ChatStoreMessage } from "@/app";
import { ChatStoreMessage } from "@/types/chatstore";
interface Props {
chat: ChatStoreMessage;

View File

@@ -1,4 +1,4 @@
import { ChatStoreMessage } from "@/app";
import { ChatStoreMessage } from "@/types/chatstore";
import { getMessageText } from "@/chatgpt";
interface Props {

View File

@@ -1,4 +1,4 @@
import { ChatStoreMessage } from "@/app";
import { ChatStoreMessage } from "@/types/chatstore";
interface Props {
chat: ChatStoreMessage;

View File

@@ -1,4 +1,4 @@
import { ChatStoreMessage } from "@/app";
import { ChatStoreMessage } from "@/types/chatstore";
interface Props {
chat: ChatStoreMessage;

92
src/pages/AddToolMsg.tsx Normal file
View 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
View 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>
);
}

View File

@@ -1,54 +1,44 @@
import {
MagnifyingGlassIcon,
CubeIcon,
BanknotesIcon,
DocumentTextIcon,
ChatBubbleLeftEllipsisIcon,
ScissorsIcon,
SwatchIcon,
SparklesIcon,
} from "@heroicons/react/24/outline";
import { IDBPDatabase } from "idb";
import structuredClone from "@ungap/structured-clone";
import { createRef } from "preact";
import { StateUpdater, useEffect, useState, Dispatch } from "preact/hooks";
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
import {
ChatStore,
ChatStoreMessage,
STORAGE_NAME_TEMPLATE,
STORAGE_NAME_TEMPLATE_API,
STORAGE_NAME_TEMPLATE_API_IMAGE_GEN,
STORAGE_NAME_TEMPLATE_API_TTS,
STORAGE_NAME_TEMPLATE_API_WHISPER,
STORAGE_NAME_TEMPLATE_TOOLS,
TemplateAPI,
TemplateTools,
addTotalCost,
getTotalCost,
} from "@/app";
} from "@/const";
import { addTotalCost, getTotalCost } from "@/utils/totalCost";
import ChatGPT, {
calculate_token_length,
ChunkMessage,
FetchResponse,
Message as MessageType,
MessageDetail,
ToolCall,
Logprobs,
} from "@/chatgpt";
import {
ChatStore,
ChatStoreMessage,
TemplateChatStore,
TemplateAPI,
TemplateTools,
} from "../types/chatstore";
import Message from "@/message";
import models from "@/models";
import { models } from "@/types/models";
import Settings from "@/settings";
import getDefaultParams from "@/getDefaultParam";
import { AddImage } from "@/addImage";
import { ListAPIs } from "@/listAPIs";
import { ListToolsTempaltes } from "@/listToolsTemplates";
import { autoHeight } from "@/textarea";
import Search from "@/search";
export interface TemplateChatStore extends ChatStore {
name: string;
}
import Templates from "@/components/Templates";
import VersionHint from "@/components/VersionHint";
import StatusBar from "@/components/StatusBar";
import WhisperButton from "@/components/WhisperButton";
import AddToolMsg from "./AddToolMsg";
export default function ChatBOX(props: {
db: Promise<IDBPDatabase<ChatStore>>;
@@ -66,17 +56,13 @@ export default function ChatBOX(props: {
const [showGenerating, setShowGenerating] = useState(false);
const [generatingMessage, setGeneratingMessage] = useState("");
const [showRetry, setShowRetry] = useState(false);
const [isRecording, setIsRecording] = useState("Mic");
const [showAddToolMsg, setShowAddToolMsg] = useState(false);
const [newToolCallID, setNewToolCallID] = useState("");
const [newToolContent, setNewToolContent] = useState("");
const [showSearch, setShowSearch] = useState(false);
let default_follow = localStorage.getItem("follow");
if (default_follow === null) {
default_follow = "true";
}
const [follow, _setFollow] = useState(default_follow === "true");
const mediaRef = createRef();
const setFollow = (follow: boolean) => {
console.log("set follow", follow);
@@ -93,19 +79,6 @@ export default function ChatBOX(props: {
const client = new ChatGPT(chatStore.apiKey);
const update_total_tokens = () => {
// manually estimate token
client.total_tokens = calculate_token_length(
chatStore.systemMessageContent,
);
for (const msg of chatStore.history
.filter(({ hide }) => !hide)
.slice(chatStore.postBeginIndex)) {
client.total_tokens += msg.token;
}
chatStore.totalTokens = client.total_tokens;
};
const _completeWithStreamMode = async (response: Response) => {
let responseTokenCount = 0;
const allChunkMessage: string[] = [];
@@ -208,7 +181,6 @@ export default function ChatBOX(props: {
// manually copy status from client to chatStore
chatStore.maxTokens = client.max_tokens;
chatStore.tokenMargin = client.tokens_margin;
update_total_tokens();
setChatStore({ ...chatStore });
setGeneratingMessage("");
setShowGenerating(false);
@@ -475,244 +447,12 @@ export default function ChatBOX(props: {
/>
)}
<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>
<StatusBar
chatStore={chatStore}
setShowSettings={setShowSettings}
setShowSearch={setShowSearch}
/>
<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">
{!chatStore.apiKey && (
<p className="bg-base-200 p-6 rounded my-3 text-left">
@@ -799,104 +539,12 @@ export default function ChatBOX(props: {
</h2>
<div className="divider"></div>
<div className="flex flex-wrap">
{templates.map((t, index) => (
<div
className="cursor-pointer rounded bg-green-400 w-fit p-2 m-1 flex flex-col"
onClick={() => {
const newChatStore: ChatStore = structuredClone(t);
// @ts-ignore
delete newChatStore.name;
if (!newChatStore.apiEndpoint) {
newChatStore.apiEndpoint = getDefaultParams(
"api",
chatStore.apiEndpoint,
);
}
if (!newChatStore.apiKey) {
newChatStore.apiKey = getDefaultParams(
"key",
chatStore.apiKey,
);
}
if (!newChatStore.whisper_api) {
newChatStore.whisper_api = getDefaultParams(
"whisper-api",
chatStore.whisper_api,
);
}
if (!newChatStore.whisper_key) {
newChatStore.whisper_key = getDefaultParams(
"whisper-key",
chatStore.whisper_key,
);
}
if (!newChatStore.tts_api) {
newChatStore.tts_api = getDefaultParams(
"tts-api",
chatStore.tts_api,
);
}
if (!newChatStore.tts_key) {
newChatStore.tts_key = getDefaultParams(
"tts-key",
chatStore.tts_key,
);
}
if (!newChatStore.image_gen_api) {
newChatStore.image_gen_api = getDefaultParams(
"image-gen-api",
chatStore.image_gen_api,
);
}
if (!newChatStore.image_gen_key) {
newChatStore.image_gen_key = getDefaultParams(
"image-gen-key",
chatStore.image_gen_key,
);
}
newChatStore.cost = 0;
// manage undefined value because of version update
newChatStore.toolsString = newChatStore.toolsString || "";
setChatStore({ ...newChatStore });
}}
>
<span className="w-full text-center">{t.name}</span>
<hr className="mt-2" />
<span className="flex justify-between">
<button
onClick={(event) => {
// prevent triggert other event
event.stopPropagation();
const name = prompt("Give template a name");
if (!name) {
return;
}
t.name = name;
setTemplates(structuredClone(templates));
}}
>
🖋
</button>
<button
onClick={(event) => {
// prevent triggert other event
event.stopPropagation();
if (!confirm("Are you sure to delete this template?")) {
return;
}
templates.splice(index, 1);
setTemplates(structuredClone(templates));
}}
>
</button>
</span>
</div>
))}
<Templates
templates={templates}
setTemplates={setTemplates}
chatStore={chatStore}
setChatStore={setChatStore}
/>
</div>
</div>
)}
@@ -930,7 +578,6 @@ export default function ChatBOX(props: {
chatStore={chatStore}
setChatStore={setChatStore}
messageIndex={messageIndex}
update_total_tokens={update_total_tokens}
/>
))}
{showGenerating && (
@@ -951,7 +598,6 @@ export default function ChatBOX(props: {
}
//chatStore.totalTokens =
update_total_tokens();
setChatStore({ ...chatStore });
await complete();
@@ -986,43 +632,7 @@ export default function ChatBOX(props: {
</>
)}
</p>
{chatStore.chatgpt_api_web_version < "v1.3.0" && (
<p className="p-2 my-2 text-center dark:text-white">
<br />
{Tr("Warning: current chatStore version")}:{" "}
{chatStore.chatgpt_api_web_version} {"< v1.3.0"}
<br />
v1.3.0
使
<br />
</p>
)}
{chatStore.chatgpt_api_web_version < "v1.4.0" && (
<p className="p-2 my-2 text-center dark:text-white">
<br />
{Tr("Warning: current chatStore version")}:{" "}
{chatStore.chatgpt_api_web_version} {"< v1.4.0"}
<br />
v1.4.0 使
<br />
</p>
)}
{chatStore.chatgpt_api_web_version < "v1.6.0" && (
<p className="p-2 my-2 text-center dark:text-white">
<br />
{chatStore.chatgpt_api_web_version}
{Tr("Warning: current chatStore version")}:{" "}
{chatStore.chatgpt_api_web_version} {"< v1.6.0"}
<br />
v1.6.0 apiKey apiEndpoint
使
<br />
</p>
)}
<VersionHint chatStore={chatStore} />
{showRetry && (
<p className="text-right p-2 my-2 dark:text-white">
<button
@@ -1122,124 +732,13 @@ export default function ChatBOX(props: {
>
{Tr("Send")}
</button>
{chatStore.whisper_api &&
chatStore.whisper_key &&
(chatStore.whisper_key || chatStore.apiKey) && (
<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>
)}
{chatStore.whisper_api && chatStore.whisper_key && (
<WhisperButton
chatStore={chatStore}
inputMsg={inputMsg}
setInputMsg={setInputMsg}
/>
)}
{chatStore.develop_mode && (
<button
className="btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1"
@@ -1256,7 +755,6 @@ export default function ChatBOX(props: {
audio: null,
logprobs: null,
});
update_total_tokens();
setInputMsg("");
setChatStore({ ...chatStore });
}}
@@ -1287,82 +785,11 @@ export default function ChatBOX(props: {
</button>
)}
{showAddToolMsg && (
<div
className="absolute z-10 bg-black bg-opacity-50 w-full h-full flex justify-center items-center left-0 top-0 overflow-scroll"
onClick={() => {
setShowAddToolMsg(false);
}}
>
<div
className="bg-white rounded p-2 z-20 flex flex-col"
onClick={(event) => {
event.stopPropagation();
}}
>
<h2>Add Tool Message</h2>
<hr className="my-2" />
<span>
<label>tool_call_id</label>
<input
className="rounded m-1 p-1 border-2 border-gray-400"
type="text"
value={newToolCallID}
onChange={(event: any) =>
setNewToolCallID(event.target.value)
}
/>
</span>
<span>
<label>Content</label>
<textarea
className="rounded m-1 p-1 border-2 border-gray-400"
rows={5}
value={newToolContent}
onChange={(event: any) =>
setNewToolContent(event.target.value)
}
></textarea>
</span>
<span className={`flex justify-between p-2`}>
<button
className="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>
<AddToolMsg
chatStore={chatStore}
setChatStore={setChatStore}
setShowAddToolMsg={setShowAddToolMsg}
/>
)}
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { IDBPDatabase } from "idb";
import { StateUpdater, useRef, useState, Dispatch } from "preact/hooks";
import { ChatStore } from "@/app";
import { ChatStore } from "@/types/chatstore";
interface ChatStoreSearchResult {
key: IDBValidKey;

View File

@@ -1,4 +1,4 @@
import { TemplateAPI } from "@/app";
import { TemplateAPI } from "@/types/chatstore";
import { Tr } from "@/translate";
interface Props {

View File

@@ -21,20 +21,19 @@ import {
useState,
Dispatch,
} from "preact/hooks";
import { clearTotalCost, getTotalCost } from "@/utils/totalCost";
import {
ChatStore,
TemplateChatStore,
TemplateAPI,
TemplateTools,
clearTotalCost,
getTotalCost,
} from "@/app";
import models from "@/models";
import { TemplateChatStore } from "@/chatbox";
} from "@/types/chatstore";
import { models } from "@/types/models";
import { tr, Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
import { isVailedJSON } from "@/message";
import { SetAPIsTemplate } from "@/setAPIsTemplate";
import { autoHeight } from "@/textarea";
import getDefaultParams from "@/getDefaultParam";
import { getDefaultParams } from "@/utils/getDefaultParam";
const TTS_VOICES: string[] = [
"alloy",

View File

@@ -1,7 +1,8 @@
import { SpeakerWaveIcon } from "@heroicons/react/24/outline";
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";
interface TTSProps {

72
src/types/chatstore.ts Normal file
View 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;
}

View File

@@ -6,7 +6,7 @@ interface Model {
};
}
const models: Record<string, Model> = {
export const models: Record<string, Model> = {
"gpt-4o": {
maxToken: 128000,
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 },
},
};
export const defaultModel = "gpt-4o-mini";
export default models;

88
src/types/newChatstore.ts Normal file
View 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: [],
};
};

View File

@@ -1,7 +1,8 @@
function getDefaultParams(param: string, val: string): string;
function getDefaultParams(param: string, val: number): number;
function getDefaultParams(param: string, val: boolean): boolean;
function getDefaultParams(param: any, val: any) {
export function getDefaultParams(param: string, val: string): string;
export function getDefaultParams(param: string, val: number): number;
export function getDefaultParams(param: string, val: boolean): boolean;
export function getDefaultParams(param: any, val: any) {
const queryParameters = new URLSearchParams(window.location.search);
const get = queryParameters.get(param);
if (typeof val === "string") {
@@ -16,5 +17,3 @@ function getDefaultParams(param: any, val: any) {
return val;
}
}
export default getDefaultParams;

18
src/utils/totalCost.ts Normal file
View 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`);
}