56 Commits

Author SHA1 Message Date
f0f040c42c use @ import alias
All checks were successful
Build static content / build (push) Successful in 10m51s
2024-10-14 18:09:07 +08:00
1c3c94bae4 rename class to className 2024-10-14 17:50:55 +08:00
f5d43ec4b9 fix: type hiint dispatch stateupdater 2024-10-14 17:50:37 +08:00
6df6ad031a update dependency 2024-10-14 17:19:13 +08:00
3cc80fd8fe refactor: disable maxGenTokens feature in newChatStore 2024-09-19 09:40:00 +08:00
e09036860f add model gpt-4o-2024-08-06 2024-08-09 09:30:26 +08:00
49537a0d58 add word-wrap: anywhere 2024-08-06 10:39:57 +08:00
243f1a5ea5 Update ChatBOX component to display total tokens in the short status bar 2024-08-02 16:41:46 +08:00
b3a2988907 fix overflow-hidden class from navbar 2024-08-02 16:29:53 +08:00
4e2ac186d5 show model price in long status bar 2024-08-02 16:17:16 +08:00
41e3026ac5 bring the long status bar back 2024-08-02 11:19:37 +08:00
c123f9454a set chat bubble width to max 2024-07-30 18:54:32 +08:00
c473fd496e fix settings window overflow 2024-07-26 17:02:09 +08:00
6a848580f6 Update navbar model name text size 2024-07-26 16:57:34 +08:00
91f7043b7c replace class with className, clean style 2024-07-26 16:51:29 +08:00
2b430bd395 Update navbar styling in ChatBOX component 2024-07-26 16:42:01 +08:00
46c8a87a06 remove 0613 model 2024-07-26 16:21:31 +08:00
fb48723d34 Adjust styling and add line height to textarea 2024-07-24 10:55:45 +08:00
370a680d94 update model list
- add gpt-4o-mini
- remove EOL models
- set default model to gpt-4o-mini
2024-07-19 09:34:22 +08:00
9417b99ad4 fix prompt wrapping issue 2024-07-18 10:34:17 +08:00
63b2f41b97 click prompt to open settings 2024-07-18 10:32:39 +08:00
44f5d28565 click status bar model name to open settings 2024-07-18 10:30:53 +08:00
b3d84ea454 chatbox width: 100%
this fix the issue of the sidebar width changing.
2024-07-18 09:37:04 +08:00
b9fdfb8905 hide prompt if systemMessageContent is empty 2024-07-18 09:34:09 +08:00
3328b3e94d fix: message word-wrap and develop mode UI 2024-07-18 09:31:29 +08:00
603ec23d24 Merge pull request #2 from ecwu/master
Refactor UI Design with DaisyUI
2024-07-18 09:14:06 +08:00
ecwu
08a7670509 fix type problem 2024-07-18 00:21:17 +08:00
ecwu
3ee5cd32bc redesign top stats with a navbar 2024-07-18 00:18:20 +08:00
ecwu
9298839b4f fix over tight textarea for text input 2024-07-17 23:38:51 +08:00
ecwu
9d0f93ecf6 fix overflow api card 2024-07-17 11:34:39 +08:00
ecwu
c2c17e5956 refine setting layout 2024-07-17 11:25:09 +08:00
ecwu
415fb934ae clean up setting panel 2024-07-17 02:06:13 +08:00
ecwu
148d912be5 refine search panel 2024-07-17 00:30:17 +08:00
ecwu
52d8c3280e adding new stat panel 2024-07-17 00:17:26 +08:00
ecwu
a45785c607 fix absolute main.tsx path 2024-07-16 23:06:35 +08:00
ecwu
8c17b842b2 fix absolute main.tsx path 2024-07-16 23:04:07 +08:00
ecwu
4bf3e02962 reform the message box with bubble style 2024-07-16 23:01:18 +08:00
ecwu
0ae53ff954 reconstitution ui with daisyui 2024-07-16 21:51:58 +08:00
4079ec77f9 Fix storage upgrade alert condition 2024-06-03 16:13:24 +08:00
6e647d9181 fix delta is undefined 2024-05-22 15:41:56 +08:00
4162866fd6 Add autoFocus to search input and improve code formatting 2024-05-15 18:22:52 +08:00
09f6e5b490 Update models in models.ts 2024-05-15 15:57:42 +08:00
6247d75234 Convert search query and content to lowercase 2024-05-15 00:59:57 +08:00
4dd29af256 Refactor search.tsx to use preact/hooks 2024-05-14 19:06:06 +08:00
245db574f8 Add query and preview to ChatStoreSearchResult 2024-05-14 19:04:12 +08:00
2386e6f2e9 Update storage version to 11 and add alert during upgrade 2024-05-14 18:56:40 +08:00
117fce390c Add search function 2024-05-14 18:51:41 +08:00
8e1e82cf4b Update AddImage component UI 2024-05-14 09:40:27 +08:00
c0ec74638a Refactor chat message type switching logic 2024-05-14 09:35:33 +08:00
d4da4c3e32 Update headings and styles, and fix image layout in chatbox and editMessageDetail components 2024-05-14 09:34:12 +08:00
2a21985a17 add gpt-4o models 2024-05-14 09:26:07 +08:00
f0c16a3cd1 localStorage follow
All checks were successful
Build static content / build (push) Successful in 3m8s
2024-03-30 11:40:34 +08:00
f54b192616 add follow scroll option 2024-03-30 11:37:19 +08:00
b20de667a4 fix: set logprobs to default false 2024-03-16 18:32:39 +08:00
a76cf224f6 new button w-full
All checks were successful
Build static content / build (push) Successful in 5m8s
2024-03-16 15:15:31 +08:00
943cb5f392 add gitea action
All checks were successful
Build static content / build (push) Successful in 4m30s
2024-03-07 02:08:22 +08:00
31 changed files with 2474 additions and 1318 deletions

View File

@@ -0,0 +1,27 @@
name: Build static content
on:
# Runs on pushes targeting the default branch
push:
branches: ["master"]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Use Node.js 18.x
uses: actions/setup-node@v3
with:
node-version: 18.x
cache: 'npm'
- run: npm install
- run: npm run build
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: dist-files
path: './dist/'

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html data-theme="cupcake" lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta <meta

View File

@@ -9,19 +9,22 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@types/ungap__structured-clone": "^0.3.1", "@heroicons/react": "^2.1.5",
"@types/ungap__structured-clone": "^1.2.0",
"@ungap/structured-clone": "^1.2.0", "@ungap/structured-clone": "^1.2.0",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.20",
"idb": "^7.1.1", "idb": "^8.0.0",
"postcss": "^8.4.31", "postcss": "^8.4.47",
"preact": "^10.18.1", "preact": "^10.24.3",
"preact-markdown": "^2.1.0", "preact-markdown": "^2.1.0",
"sakura.css": "^1.5.0", "sakura.css": "^1.5.0",
"tailwindcss": "^3.3.4" "tailwindcss": "^3.4.13"
}, },
"devDependencies": { "devDependencies": {
"@preact/preset-vite": "^2.6.0", "@preact/preset-vite": "^2.9.1",
"typescript": "^5.2.2", "daisyui": "^4.12.13",
"vite": "^4.5.0" "theme-change": "^2.5.0",
"typescript": "^5.6.3",
"vite": "^5.4.8"
} }
} }

View File

@@ -1,7 +1,7 @@
import { useState } from "preact/hooks"; import { useState } from "preact/hooks";
import { ChatStore } from "./app"; import { ChatStore } from "@/app";
import { MessageDetail } from "./chatgpt"; import { MessageDetail } from "@/chatgpt";
import { Tr } from "./translate"; import { Tr } from "@/translate";
interface Props { interface Props {
chatStore: ChatStore; chatStore: ChatStore;
@@ -41,15 +41,25 @@ export function AddImage({
}} }}
> >
<div <div
className="bg-white rounded p-2 z-20" className="bg-base-200 p-2 z-20"
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
}} }}
> >
<h2>Add Images</h2> <div className="flex justify-between items-center p-1">
<span> <h3>Add Images</h3>
<button <button
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600" className="btn btn-sm btn-neutral"
onClick={() => {
setShowAddImage(false);
}}
>
Done
</button>
</div>
<span className="">
<button
className="btn btn-secondary btn-sm disabled:btn-disabled"
onClick={() => { onClick={() => {
const image_url = prompt("Image URL"); const image_url = prompt("Image URL");
if (!image_url) { if (!image_url) {
@@ -70,7 +80,7 @@ export function AddImage({
Add from URL Add from URL
</button> </button>
<button <button
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600" className="btn btn-primary btn-sm disabled:btn-disabled"
onClick={() => { onClick={() => {
// select file and load it to base64 image URL format // select file and load it to base64 image URL format
const input = document.createElement("input"); const input = document.createElement("input");
@@ -111,23 +121,24 @@ export function AddImage({
<input type="checkbox" checked={enableHighResolution} /> <input type="checkbox" checked={enableHighResolution} />
</span> </span>
</span> </span>
<div className="divider"></div>
{chatStore.image_gen_api && chatStore.image_gen_key && ( {chatStore.image_gen_api && chatStore.image_gen_key && (
<div className="flex flex-col"> <div className="flex flex-col">
<hr className="my-2" />
<h3>Generate Image</h3> <h3>Generate Image</h3>
<span className="flex flex-row justify-between m-1 p-1"> <span className="flex flex-col justify-between m-1 p-1">
<label>Prompt: </label> <label>Prompt: </label>
<textarea <textarea
className="border rounded border-gray-400" className="textarea textarea-sm textarea-bordered"
value={imageGenPrompt} value={imageGenPrompt}
onChange={(e: any) => { onChange={(e: any) => {
setImageGenPrompt(e.target.value); setImageGenPrompt(e.target.value);
}} }}
/> />
</span> </span>
<span className="flex flex-row justify-between m-1 p-1"> <span className="flex flex-row justify-between items-center m-1 p-1">
<label>Model: </label> <label>Model: </label>
<select <select
className="select select-sm select-bordered"
value={imageGenModel} value={imageGenModel}
onChange={(e: any) => { onChange={(e: any) => {
setImageGenModel(e.target.value); setImageGenModel(e.target.value);
@@ -137,9 +148,10 @@ export function AddImage({
<option value="dall-e-2">DALL-E 2</option> <option value="dall-e-2">DALL-E 2</option>
</select> </select>
</span> </span>
<span className="flex flex-row justify-between m-1 p-1"> <span className="flex flex-row justify-between items-center m-1 p-1">
<label>n: </label> <label>n: </label>
<input <input
className="input input-sm input-bordered"
value={imageGenN} value={imageGenN}
type="number" type="number"
min={1} min={1}
@@ -147,9 +159,10 @@ export function AddImage({
onChange={(e: any) => setImageGenN(parseInt(e.target.value))} onChange={(e: any) => setImageGenN(parseInt(e.target.value))}
/> />
</span> </span>
<span className="flex flex-row justify-between m-1 p-1"> <span className="flex flex-row justify-between items-center m-1 p-1">
<label>Quality: </label> <label>Quality: </label>
<select <select
className="select select-sm select-bordered"
value={imageGenQuality} value={imageGenQuality}
onChange={(e: any) => setImageGEnQuality(e.target.value)} onChange={(e: any) => setImageGEnQuality(e.target.value)}
> >
@@ -157,9 +170,10 @@ export function AddImage({
<option value="standard">Standard</option> <option value="standard">Standard</option>
</select> </select>
</span> </span>
<span className="flex flex-row justify-between m-1 p-1"> <span className="flex flex-row justify-between items-center m-1 p-1">
<label>Response Format: </label> <label>Response Format: </label>
<select <select
className="select select-sm select-bordered"
value={imageGenResponseFormat} value={imageGenResponseFormat}
onChange={(e: any) => setImageGenResponseFormat(e.target.value)} onChange={(e: any) => setImageGenResponseFormat(e.target.value)}
> >
@@ -167,9 +181,10 @@ export function AddImage({
<option value="url">url</option> <option value="url">url</option>
</select> </select>
</span> </span>
<span className="flex flex-row justify-between m-1 p-1"> <span className="flex flex-row justify-between items-center m-1 p-1">
<label>Size: </label> <label>Size: </label>
<select <select
className="select select-sm select-bordered"
value={imageGenSize} value={imageGenSize}
onChange={(e: any) => setImageGenSize(e.target.value)} onChange={(e: any) => setImageGenSize(e.target.value)}
> >
@@ -180,9 +195,10 @@ export function AddImage({
<option value="1024x1792">1024x1792 (dall-e-3)</option> <option value="1024x1792">1024x1792 (dall-e-3)</option>
</select> </select>
</span> </span>
<span className="flex flex-row justify-between m-1 p-1"> <span className="flex flex-row justify-between items-center m-1 p-1">
<label>Style (only dall-e-3): </label> <label>Style (only dall-e-3): </label>
<select <select
className="select select-sm select-bordered"
value={imageGenStyle} value={imageGenStyle}
onChange={(e: any) => setImageGenStyle(e.target.value)} onChange={(e: any) => setImageGenStyle(e.target.value)}
> >
@@ -190,9 +206,9 @@ export function AddImage({
<option value="natural">natural</option> <option value="natural">natural</option>
</select> </select>
</span> </span>
<span className="flex flex-row justify-between m-1 p-1"> <span className="flex flex-row justify-between items-center m-1 p-1">
<button <button
className="bg-sky-400 m-1 p-1 rounded disabled:bg-slate-500" className="btn btn-primary btn-sm"
disabled={imageGenGenerating} disabled={imageGenGenerating}
onClick={async () => { onClick={async () => {
try { try {

View File

@@ -1,14 +1,14 @@
import { IDBPDatabase, openDB } from "idb"; import { IDBPDatabase, openDB } from "idb";
import { useEffect, useState } from "preact/hooks"; import { useEffect, useState } from "preact/hooks";
import "./global.css"; import "@/global.css";
import { calculate_token_length, Logprobs, Message } from "./chatgpt"; import { calculate_token_length, Logprobs, Message } from "@/chatgpt";
import getDefaultParams from "./getDefaultParam"; import getDefaultParams from "@/getDefaultParam";
import ChatBOX from "./chatbox"; import ChatBOX from "@/chatbox";
import models, { defaultModel } from "./models"; import models, { defaultModel } from "@/models";
import { Tr, langCodeContext, LANG_OPTIONS } from "./translate"; import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
import CHATGPT_API_WEB_VERSION from "./CHATGPT_API_WEB_VERSION"; import CHATGPT_API_WEB_VERSION from "@/CHATGPT_API_WEB_VERSION";
export interface ChatStoreMessage extends Message { export interface ChatStoreMessage extends Message {
hide: boolean; hide: boolean;
@@ -65,6 +65,7 @@ export interface ChatStore {
image_gen_key: string; image_gen_key: string;
json_mode: boolean; json_mode: boolean;
logprobs: boolean; logprobs: boolean;
contents_for_index: string[];
} }
const _defaultAPIEndpoint = "https://api.openai.com/v1/chat/completions"; const _defaultAPIEndpoint = "https://api.openai.com/v1/chat/completions";
@@ -87,7 +88,7 @@ export const newChatStore = (
image_gen_api = "https://api.openai.com/v1/images/generations", image_gen_api = "https://api.openai.com/v1/images/generations",
image_gen_key = "", image_gen_key = "",
json_mode = false, json_mode = false,
logprobs = true logprobs = false,
): ChatStore => { ): ChatStore => {
return { return {
chatgpt_api_web_version: CHATGPT_API_WEB_VERSION, chatgpt_api_web_version: CHATGPT_API_WEB_VERSION,
@@ -99,10 +100,10 @@ export const newChatStore = (
totalTokens: 0, totalTokens: 0,
maxTokens: getDefaultParams( maxTokens: getDefaultParams(
"max", "max",
models[getDefaultParams("model", model)]?.maxToken ?? 2048 models[getDefaultParams("model", model)]?.maxToken ?? 2048,
), ),
maxGenTokens: 2048, maxGenTokens: 2048,
maxGenTokens_enabled: true, maxGenTokens_enabled: false,
apiKey: getDefaultParams("key", apiKey), apiKey: getDefaultParams("key", apiKey),
apiEndpoint: getDefaultParams("api", apiEndpoint), apiEndpoint: getDefaultParams("api", apiEndpoint),
streamMode: getDefaultParams("mode", streamMode), streamMode: getDefaultParams("mode", streamMode),
@@ -128,6 +129,7 @@ export const newChatStore = (
json_mode: json_mode, json_mode: json_mode,
tts_format: tts_format, tts_format: tts_format,
logprobs, logprobs,
contents_for_index: [],
}; };
}; };
@@ -150,7 +152,7 @@ export function addTotalCost(cost: number) {
export function getTotalCost(): number { export function getTotalCost(): number {
let totalCost = parseFloat( let totalCost = parseFloat(
localStorage.getItem(STORAGE_NAME_TOTALCOST) ?? "0" localStorage.getItem(STORAGE_NAME_TOTALCOST) ?? "0",
); );
return totalCost; return totalCost;
} }
@@ -159,10 +161,36 @@ export function clearTotalCost() {
localStorage.setItem(STORAGE_NAME_TOTALCOST, `0`); 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() { export function App() {
// init selected index // init selected index
const [selectedChatIndex, setSelectedChatIndex] = useState( const [selectedChatIndex, setSelectedChatIndex] = useState(
parseInt(localStorage.getItem(STORAGE_NAME_SELECTED) ?? "1") parseInt(localStorage.getItem(STORAGE_NAME_SELECTED) ?? "1"),
); );
console.log("selectedChatIndex", selectedChatIndex); console.log("selectedChatIndex", selectedChatIndex);
useEffect(() => { useEffect(() => {
@@ -170,30 +198,67 @@ export function App() {
localStorage.setItem(STORAGE_NAME_SELECTED, `${selectedChatIndex}`); localStorage.setItem(STORAGE_NAME_SELECTED, `${selectedChatIndex}`);
}, [selectedChatIndex]); }, [selectedChatIndex]);
const db = openDB<ChatStore>(STORAGE_NAME, 1, { const db = openDB<ChatStore>(STORAGE_NAME, 11, {
upgrade(db) { async upgrade(db, oldVersion, newVersion, transaction) {
const store = db.createObjectStore(STORAGE_NAME, { if (oldVersion < 1) {
autoIncrement: true, const store = db.createObjectStore(STORAGE_NAME, {
}); autoIncrement: true,
});
// copy from localStorage to indexedDB // copy from localStorage to indexedDB
const allChatStoreIndexes: number[] = JSON.parse( const allChatStoreIndexes: number[] = JSON.parse(
localStorage.getItem(STORAGE_NAME_INDEXES) ?? "[]" 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. 🎉"
); );
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);
}
} }
}, },
}); });
@@ -222,11 +287,14 @@ export function App() {
const [chatStore, _setChatStore] = useState(newChatStore()); const [chatStore, _setChatStore] = useState(newChatStore());
const setChatStore = async (chatStore: ChatStore) => { const setChatStore = async (chatStore: ChatStore) => {
// building field for search
chatStore.contents_for_index = BuildFiledForSearch(chatStore);
console.log("recalculate postBeginIndex"); console.log("recalculate postBeginIndex");
const max = chatStore.maxTokens - chatStore.tokenMargin; const max = chatStore.maxTokens - chatStore.tokenMargin;
let sum = 0; let sum = 0;
chatStore.postBeginIndex = chatStore.history.filter( chatStore.postBeginIndex = chatStore.history.filter(
({ hide }) => !hide ({ hide }) => !hide,
).length; ).length;
for (const msg of chatStore.history for (const msg of chatStore.history
.filter(({ hide }) => !hide) .filter(({ hide }) => !hide)
@@ -241,7 +309,7 @@ export function App() {
// manually estimate token // manually estimate token
chatStore.totalTokens = calculate_token_length( chatStore.totalTokens = calculate_token_length(
chatStore.systemMessageContent chatStore.systemMessageContent,
); );
for (const msg of chatStore.history for (const msg of chatStore.history
.filter(({ hide }) => !hide) .filter(({ hide }) => !hide)
@@ -263,7 +331,7 @@ export function App() {
// all chat store indexes // all chat store indexes
const [allChatStoreIndexes, setAllChatStoreIndexes] = useState<IDBValidKey>( const [allChatStoreIndexes, setAllChatStoreIndexes] = useState<IDBValidKey>(
[] [],
); );
const handleNewChatStoreWithOldOne = async (chatStore: ChatStore) => { const handleNewChatStoreWithOldOne = async (chatStore: ChatStore) => {
@@ -290,8 +358,8 @@ export function App() {
chatStore.image_gen_api, chatStore.image_gen_api,
chatStore.image_gen_key, chatStore.image_gen_key,
chatStore.json_mode, chatStore.json_mode,
chatStore.logprobs false, // logprobs default to false
) ),
); );
setSelectedChatIndex(newKey as number); setSelectedChatIndex(newKey as number);
setAllChatStoreIndexes(await (await db).getAllKeys(STORAGE_NAME)); setAllChatStoreIndexes(await (await db).getAllKeys(STORAGE_NAME));
@@ -335,16 +403,16 @@ export function App() {
}, []); }, []);
return ( return (
<div className="flex text-sm h-full bg-slate-200 dark:bg-slate-800 dark:text-white"> <div className="flex text-sm h-full">
<div className="flex flex-col h-full p-2 border-r-indigo-500 border-2 dark:border-slate-800 dark:border-r-indigo-500 dark:text-black"> <div className="flex flex-col h-full p-2 bg-primary">
<div className="grow overflow-scroll"> <div className="grow overflow-scroll">
<button <button
className="bg-violet-300 p-1 rounded hover:bg-violet-400" className="btn btn-sm btn-info p-1 my-1 w-full"
onClick={handleNewChatStore} onClick={handleNewChatStore}
> >
{Tr("NEW")} {Tr("NEW")}
</button> </button>
<ul> <ul className="pt-2">
{(allChatStoreIndexes as number[]) {(allChatStoreIndexes as number[])
.slice() .slice()
.reverse() .reverse()
@@ -353,8 +421,8 @@ export function App() {
return ( return (
<li> <li>
<button <button
className={`w-full my-1 p-1 rounded hover:bg-blue-500 ${ className={`w-full my-1 p-1 btn btn-sm ${
i === selectedChatIndex ? "bg-blue-500" : "bg-blue-200" i === selectedChatIndex ? "btn-accent" : "btn-secondary"
}`} }`}
onClick={() => { onClick={() => {
setSelectedChatIndex(i); setSelectedChatIndex(i);
@@ -367,54 +435,62 @@ export function App() {
})} })}
</ul> </ul>
</div> </div>
<button <div>
className="rounded bg-rose-400 p-1 my-1 w-full"
onClick={async () => {
if (!confirm("Are you sure you want to delete this chat history?"))
return;
console.log("remove item", `${STORAGE_NAME}-${selectedChatIndex}`);
(await db).delete(STORAGE_NAME, selectedChatIndex);
const newAllChatStoreIndexes = await (
await db
).getAllKeys(STORAGE_NAME);
if (newAllChatStoreIndexes.length === 0) {
handleNewChatStore();
return;
}
// find nex selected chat index
const next =
newAllChatStoreIndexes[newAllChatStoreIndexes.length - 1];
console.log("next is", next);
setSelectedChatIndex(next as number);
setAllChatStoreIndexes(newAllChatStoreIndexes);
}}
>
{Tr("DEL")}
</button>
{chatStore.develop_mode && (
<button <button
className="rounded bg-rose-800 p-1 my-1 w-full text-white" className="btn btn-warning btn-sm p-1 my-1 w-full"
onClick={async () => { onClick={async () => {
if ( if (
!confirm( !confirm("Are you sure you want to delete this chat history?")
"Are you sure you want to delete **ALL** chat history?"
)
) )
return; return;
console.log(
"remove item",
`${STORAGE_NAME}-${selectedChatIndex}`,
);
(await db).delete(STORAGE_NAME, selectedChatIndex);
const newAllChatStoreIndexes = await (
await db
).getAllKeys(STORAGE_NAME);
await (await db).clear(STORAGE_NAME); if (newAllChatStoreIndexes.length === 0) {
setAllChatStoreIndexes([]); handleNewChatStore();
setSelectedChatIndex(1); return;
window.location.reload(); }
// find nex selected chat index
const next =
newAllChatStoreIndexes[newAllChatStoreIndexes.length - 1];
console.log("next is", next);
setSelectedChatIndex(next as number);
setAllChatStoreIndexes(newAllChatStoreIndexes);
}} }}
> >
{Tr("CLS")} {Tr("DEL")}
</button> </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> </div>
<ChatBOX <ChatBOX
db={db}
chatStore={chatStore} chatStore={chatStore}
setChatStore={setChatStore} setChatStore={setChatStore}
selectedChatIndex={selectedChatIndex} selectedChatIndex={selectedChatIndex}

View File

@@ -1,7 +1,19 @@
import { Tr, langCodeContext, LANG_OPTIONS } from "./translate"; 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 structuredClone from "@ungap/structured-clone";
import { createRef } from "preact"; import { createRef } from "preact";
import { StateUpdater, useEffect, useState } from "preact/hooks"; import { StateUpdater, useEffect, useState, Dispatch } from "preact/hooks";
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
import { import {
ChatStore, ChatStore,
ChatStoreMessage, ChatStoreMessage,
@@ -14,7 +26,8 @@ import {
TemplateAPI, TemplateAPI,
TemplateTools, TemplateTools,
addTotalCost, addTotalCost,
} from "./app"; getTotalCost,
} from "@/app";
import ChatGPT, { import ChatGPT, {
calculate_token_length, calculate_token_length,
ChunkMessage, ChunkMessage,
@@ -23,25 +36,26 @@ import ChatGPT, {
MessageDetail, MessageDetail,
ToolCall, ToolCall,
Logprobs, Logprobs,
} from "./chatgpt"; } from "@/chatgpt";
import Message from "./message"; import Message from "@/message";
import models from "./models"; import models from "@/models";
import Settings from "./settings"; import Settings from "@/settings";
import getDefaultParams from "./getDefaultParam"; 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";
export interface TemplateChatStore extends ChatStore { export interface TemplateChatStore extends ChatStore {
name: string; name: string;
} }
export default function ChatBOX(props: { export default function ChatBOX(props: {
db: Promise<IDBPDatabase<ChatStore>>;
chatStore: ChatStore; chatStore: ChatStore;
setChatStore: (cs: ChatStore) => void; setChatStore: (cs: ChatStore) => void;
selectedChatIndex: number; selectedChatIndex: number;
setSelectedChatIndex: StateUpdater<number>; setSelectedChatIndex: Dispatch<StateUpdater<number>>;
}) { }) {
const { chatStore, setChatStore } = props; const { chatStore, setChatStore } = props;
// prevent error // prevent error
@@ -56,11 +70,25 @@ export default function ChatBOX(props: {
const [showAddToolMsg, setShowAddToolMsg] = useState(false); const [showAddToolMsg, setShowAddToolMsg] = useState(false);
const [newToolCallID, setNewToolCallID] = useState(""); const [newToolCallID, setNewToolCallID] = useState("");
const [newToolContent, setNewToolContent] = 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 mediaRef = createRef();
const setFollow = (follow: boolean) => {
console.log("set follow", follow);
localStorage.setItem("follow", follow.toString());
_setFollow(follow);
};
const messagesEndRef = createRef(); const messagesEndRef = createRef();
useEffect(() => { useEffect(() => {
messagesEndRef.current.scrollIntoView({ behavior: "smooth" }); if (follow) {
messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
}
}, [showRetry, showGenerating, generatingMessage]); }, [showRetry, showGenerating, generatingMessage]);
const client = new ChatGPT(chatStore.apiKey); const client = new ChatGPT(chatStore.apiKey);
@@ -68,7 +96,7 @@ export default function ChatBOX(props: {
const update_total_tokens = () => { const update_total_tokens = () => {
// manually estimate token // manually estimate token
client.total_tokens = calculate_token_length( client.total_tokens = calculate_token_length(
chatStore.systemMessageContent chatStore.systemMessageContent,
); );
for (const msg of chatStore.history for (const msg of chatStore.history
.filter(({ hide }) => !hide) .filter(({ hide }) => !hide)
@@ -98,14 +126,14 @@ export default function ChatBOX(props: {
const logprob = c?.logprobs?.content[0]?.logprob; const logprob = c?.logprobs?.content[0]?.logprob;
if (logprob !== undefined) { if (logprob !== undefined) {
logprobs.content.push({ logprobs.content.push({
token: c.delta.content ?? "", token: c?.delta?.content ?? "",
logprob, logprob,
}); });
console.log(c.delta.content, logprob); console.log(c?.delta?.content, logprob);
} }
allChunkMessage.push(c.delta.content ?? ""); allChunkMessage.push(c?.delta?.content ?? "");
const tool_calls = c.delta.tool_calls; const tool_calls = c?.delta?.tool_calls;
if (tool_calls) { if (tool_calls) {
for (const tool_call of tool_calls) { for (const tool_call of tool_calls) {
// init // init
@@ -124,7 +152,7 @@ export default function ChatBOX(props: {
// update tool call arguments // update tool call arguments
const tool = allChunkTool.find( const tool = allChunkTool.find(
(tool) => tool.index === tool_call.index (tool) => tool.index === tool_call.index,
); );
if (!tool) { if (!tool) {
@@ -139,7 +167,7 @@ export default function ChatBOX(props: {
allChunkMessage.join("") + allChunkMessage.join("") +
allChunkTool.map((tool) => { allChunkTool.map((tool) => {
return `Tool Call ID: ${tool.id}\nType: ${tool.type}\nFunction: ${tool.function.name}\nArguments: ${tool.function.arguments}`; return `Tool Call ID: ${tool.id}\nType: ${tool.type}\nFunction: ${tool.function.name}\nArguments: ${tool.function.arguments}`;
}) }),
); );
} }
setShowGenerating(false); setShowGenerating(false);
@@ -277,7 +305,7 @@ export default function ChatBOX(props: {
setShowGenerating(true); setShowGenerating(true);
const response = await client._fetch( const response = await client._fetch(
chatStore.streamMode, chatStore.streamMode,
chatStore.logprobs chatStore.logprobs,
); );
const contentType = response.headers.get("content-type"); const contentType = response.headers.get("content-type");
if (contentType?.startsWith("text/event-stream")) { if (contentType?.startsWith("text/event-stream")) {
@@ -347,33 +375,33 @@ export default function ChatBOX(props: {
const [templates, _setTemplates] = useState( const [templates, _setTemplates] = useState(
JSON.parse( JSON.parse(
localStorage.getItem(STORAGE_NAME_TEMPLATE) || "[]" localStorage.getItem(STORAGE_NAME_TEMPLATE) || "[]",
) as TemplateChatStore[] ) as TemplateChatStore[],
); );
const [templateAPIs, _setTemplateAPIs] = useState( const [templateAPIs, _setTemplateAPIs] = useState(
JSON.parse( JSON.parse(
localStorage.getItem(STORAGE_NAME_TEMPLATE_API) || "[]" localStorage.getItem(STORAGE_NAME_TEMPLATE_API) || "[]",
) as TemplateAPI[] ) as TemplateAPI[],
); );
const [templateAPIsWhisper, _setTemplateAPIsWhisper] = useState( const [templateAPIsWhisper, _setTemplateAPIsWhisper] = useState(
JSON.parse( JSON.parse(
localStorage.getItem(STORAGE_NAME_TEMPLATE_API_WHISPER) || "[]" localStorage.getItem(STORAGE_NAME_TEMPLATE_API_WHISPER) || "[]",
) as TemplateAPI[] ) as TemplateAPI[],
); );
const [templateAPIsTTS, _setTemplateAPIsTTS] = useState( const [templateAPIsTTS, _setTemplateAPIsTTS] = useState(
JSON.parse( JSON.parse(
localStorage.getItem(STORAGE_NAME_TEMPLATE_API_TTS) || "[]" localStorage.getItem(STORAGE_NAME_TEMPLATE_API_TTS) || "[]",
) as TemplateAPI[] ) as TemplateAPI[],
); );
const [templateAPIsImageGen, _setTemplateAPIsImageGen] = useState( const [templateAPIsImageGen, _setTemplateAPIsImageGen] = useState(
JSON.parse( JSON.parse(
localStorage.getItem(STORAGE_NAME_TEMPLATE_API_IMAGE_GEN) || "[]" localStorage.getItem(STORAGE_NAME_TEMPLATE_API_IMAGE_GEN) || "[]",
) as TemplateAPI[] ) as TemplateAPI[],
); );
const [toolsTemplates, _setToolsTemplates] = useState( const [toolsTemplates, _setToolsTemplates] = useState(
JSON.parse( JSON.parse(
localStorage.getItem(STORAGE_NAME_TEMPLATE_TOOLS) || "[]" localStorage.getItem(STORAGE_NAME_TEMPLATE_TOOLS) || "[]",
) as TemplateTools[] ) as TemplateTools[],
); );
const setTemplates = (templates: TemplateChatStore[]) => { const setTemplates = (templates: TemplateChatStore[]) => {
localStorage.setItem(STORAGE_NAME_TEMPLATE, JSON.stringify(templates)); localStorage.setItem(STORAGE_NAME_TEMPLATE, JSON.stringify(templates));
@@ -382,42 +410,42 @@ export default function ChatBOX(props: {
const setTemplateAPIs = (templateAPIs: TemplateAPI[]) => { const setTemplateAPIs = (templateAPIs: TemplateAPI[]) => {
localStorage.setItem( localStorage.setItem(
STORAGE_NAME_TEMPLATE_API, STORAGE_NAME_TEMPLATE_API,
JSON.stringify(templateAPIs) JSON.stringify(templateAPIs),
); );
_setTemplateAPIs(templateAPIs); _setTemplateAPIs(templateAPIs);
}; };
const setTemplateAPIsWhisper = (templateAPIWhisper: TemplateAPI[]) => { const setTemplateAPIsWhisper = (templateAPIWhisper: TemplateAPI[]) => {
localStorage.setItem( localStorage.setItem(
STORAGE_NAME_TEMPLATE_API_WHISPER, STORAGE_NAME_TEMPLATE_API_WHISPER,
JSON.stringify(templateAPIWhisper) JSON.stringify(templateAPIWhisper),
); );
_setTemplateAPIsWhisper(templateAPIWhisper); _setTemplateAPIsWhisper(templateAPIWhisper);
}; };
const setTemplateAPIsTTS = (templateAPITTS: TemplateAPI[]) => { const setTemplateAPIsTTS = (templateAPITTS: TemplateAPI[]) => {
localStorage.setItem( localStorage.setItem(
STORAGE_NAME_TEMPLATE_API_TTS, STORAGE_NAME_TEMPLATE_API_TTS,
JSON.stringify(templateAPITTS) JSON.stringify(templateAPITTS),
); );
_setTemplateAPIsTTS(templateAPITTS); _setTemplateAPIsTTS(templateAPITTS);
}; };
const setTemplateAPIsImageGen = (templateAPIImageGen: TemplateAPI[]) => { const setTemplateAPIsImageGen = (templateAPIImageGen: TemplateAPI[]) => {
localStorage.setItem( localStorage.setItem(
STORAGE_NAME_TEMPLATE_API_IMAGE_GEN, STORAGE_NAME_TEMPLATE_API_IMAGE_GEN,
JSON.stringify(templateAPIImageGen) JSON.stringify(templateAPIImageGen),
); );
_setTemplateAPIsImageGen(templateAPIImageGen); _setTemplateAPIsImageGen(templateAPIImageGen);
}; };
const setTemplateTools = (templateTools: TemplateTools[]) => { const setTemplateTools = (templateTools: TemplateTools[]) => {
localStorage.setItem( localStorage.setItem(
STORAGE_NAME_TEMPLATE_TOOLS, STORAGE_NAME_TEMPLATE_TOOLS,
JSON.stringify(templateTools) JSON.stringify(templateTools),
); );
_setToolsTemplates(templateTools); _setToolsTemplates(templateTools);
}; };
const userInputRef = createRef(); const userInputRef = createRef();
return ( return (
<div className="grow flex flex-col p-2 dark:text-black"> <div className="grow flex flex-col p-2 w-full">
{showSettings && ( {showSettings && (
<Settings <Settings
chatStore={chatStore} chatStore={chatStore}
@@ -438,52 +466,261 @@ export default function ChatBOX(props: {
setTemplateTools={setTemplateTools} setTemplateTools={setTemplateTools}
/> />
)} )}
<div {showSearch && (
className="cursor-pointer rounded bg-cyan-300 dark:text-white p-1 dark:bg-cyan-800" <Search
onClick={() => setShowSettings(true)} setSelectedChatIndex={props.setSelectedChatIndex}
> db={props.db}
<div> chatStore={chatStore}
<button className="underline"> setShow={setShowSearch}
{chatStore.systemMessageContent.length > 16 />
? chatStore.systemMessageContent.slice(0, 16) + ".." )}
: chatStore.systemMessageContent}
</button>{" "} <div className="navbar bg-base-100 p-0">
<button className="underline"> <div className="navbar-start">
{chatStore.streamMode ? Tr("STREAM") : Tr("FETCH")} <div className="dropdown lg:hidden">
</button>{" "} <div
{chatStore.toolsString.trim() && ( tabindex={0}
<button className="underline">TOOL</button> 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>
<div className="text-xs"> <div
<span className="underline">{chatStore.model}</span>{" "} className="navbar-center cursor-pointer py-1"
<span> onClick={() => {
Tokens:{" "} setShowSettings(true);
<span className="underline"> }}
{chatStore.totalTokens}/{chatStore.maxTokens} >
</span> {/* the long staus bar */}
</span>{" "} <div className="stats shadow hidden lg:inline-grid">
<span> <div className="stat">
{Tr("Cut")}:{" "} <div className="stat-figure text-secondary">
<span className="underline"> <CubeIcon className="h-10 w-10" />
{chatStore.postBeginIndex}/ </div>
{chatStore.history.filter(({ hide }) => !hide).length} <div className="stat-title">Model</div>
</span>{" "} <div className="stat-value text-base">{chatStore.model}</div>
</span>{" "} <div className="stat-desc">
<span> {models[chatStore.model]?.price?.prompt * 1000 * 1000} $/M
{Tr("Cost")}:{" "} tokens
<span className="underline">${chatStore.cost.toFixed(4)}</span> </div>
</span> </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> </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="opacity-60 p-6 rounded bg-white my-3 text-left dark:text-black"> <p className="bg-base-200 p-6 rounded my-3 text-left">
{Tr("Please click above to set")} (OpenAI) API KEY {Tr("Please click above to set")} (OpenAI) API KEY
</p> </p>
)} )}
{!chatStore.apiEndpoint && ( {!chatStore.apiEndpoint && (
<p className="opacity-60 p-6 rounded bg-white my-3 text-left dark:text-black"> <p className="bg-base-200 p-6 rounded my-3 text-left">
{Tr("Please click above to set")} API Endpoint {Tr("Please click above to set")} API Endpoint
</p> </p>
)} )}
@@ -545,7 +782,7 @@ export default function ChatBOX(props: {
)} )}
{chatStore.history.filter((msg) => !msg.example).length == 0 && ( {chatStore.history.filter((msg) => !msg.example).length == 0 && (
<div className="break-all opacity-80 p-3 rounded bg-white my-3 text-left dark:text-black"> <div className="bg-base-200 break-all p-3 my-3 text-left">
<h2> <h2>
<span>{Tr("Saved prompt templates")}</span> <span>{Tr("Saved prompt templates")}</span>
<button <button
@@ -560,7 +797,7 @@ export default function ChatBOX(props: {
{Tr("Reset Current")} {Tr("Reset Current")}
</button> </button>
</h2> </h2>
<hr className="my-2" /> <div className="divider"></div>
<div className="flex flex-wrap"> <div className="flex flex-wrap">
{templates.map((t, index) => ( {templates.map((t, index) => (
<div <div
@@ -572,49 +809,49 @@ export default function ChatBOX(props: {
if (!newChatStore.apiEndpoint) { if (!newChatStore.apiEndpoint) {
newChatStore.apiEndpoint = getDefaultParams( newChatStore.apiEndpoint = getDefaultParams(
"api", "api",
chatStore.apiEndpoint chatStore.apiEndpoint,
); );
} }
if (!newChatStore.apiKey) { if (!newChatStore.apiKey) {
newChatStore.apiKey = getDefaultParams( newChatStore.apiKey = getDefaultParams(
"key", "key",
chatStore.apiKey chatStore.apiKey,
); );
} }
if (!newChatStore.whisper_api) { if (!newChatStore.whisper_api) {
newChatStore.whisper_api = getDefaultParams( newChatStore.whisper_api = getDefaultParams(
"whisper-api", "whisper-api",
chatStore.whisper_api chatStore.whisper_api,
); );
} }
if (!newChatStore.whisper_key) { if (!newChatStore.whisper_key) {
newChatStore.whisper_key = getDefaultParams( newChatStore.whisper_key = getDefaultParams(
"whisper-key", "whisper-key",
chatStore.whisper_key chatStore.whisper_key,
); );
} }
if (!newChatStore.tts_api) { if (!newChatStore.tts_api) {
newChatStore.tts_api = getDefaultParams( newChatStore.tts_api = getDefaultParams(
"tts-api", "tts-api",
chatStore.tts_api chatStore.tts_api,
); );
} }
if (!newChatStore.tts_key) { if (!newChatStore.tts_key) {
newChatStore.tts_key = getDefaultParams( newChatStore.tts_key = getDefaultParams(
"tts-key", "tts-key",
chatStore.tts_key chatStore.tts_key,
); );
} }
if (!newChatStore.image_gen_api) { if (!newChatStore.image_gen_api) {
newChatStore.image_gen_api = getDefaultParams( newChatStore.image_gen_api = getDefaultParams(
"image-gen-api", "image-gen-api",
chatStore.image_gen_api chatStore.image_gen_api,
); );
} }
if (!newChatStore.image_gen_key) { if (!newChatStore.image_gen_key) {
newChatStore.image_gen_key = getDefaultParams( newChatStore.image_gen_key = getDefaultParams(
"image-gen-key", "image-gen-key",
chatStore.image_gen_key chatStore.image_gen_key,
); );
} }
newChatStore.cost = 0; newChatStore.cost = 0;
@@ -671,11 +908,23 @@ export default function ChatBOX(props: {
<br />{Tr("Click the conor to create a new chat")} <br />{Tr("Click the conor to create a new chat")}
<br /> <br />
{Tr( {Tr(
"All chat history and settings are stored in the local browser" "All chat history and settings are stored in the local browser",
)} )}
<br /> <br />
</p> </p>
)} )}
{chatStore.systemMessageContent.trim() && (
<div className="chat chat-start">
<div className="chat-header">Prompt</div>
<div
className="chat-bubble chat-bubble-accent cursor-pointer message-content"
onClick={() => setShowSettings(true)}
>
{chatStore.systemMessageContent}
</div>
</div>
)}
{chatStore.history.map((_, messageIndex) => ( {chatStore.history.map((_, messageIndex) => (
<Message <Message
chatStore={chatStore} chatStore={chatStore}
@@ -685,7 +934,7 @@ export default function ChatBOX(props: {
/> />
))} ))}
{showGenerating && ( {showGenerating && (
<p className="p-2 my-2 animate-pulse dark:text-white message-content"> <p className="p-2 my-2 animate-pulse message-content">
{generatingMessage || Tr("Generating...")} {generatingMessage || Tr("Generating...")}
... ...
</p> </p>
@@ -693,7 +942,7 @@ export default function ChatBOX(props: {
<p className="text-center"> <p className="text-center">
{chatStore.history.length > 0 && ( {chatStore.history.length > 0 && (
<button <button
className="disabled:line-through disabled:bg-slate-500 rounded m-2 p-2 border-2 bg-teal-500 hover:bg-teal-600" className="btn btn-sm btn-warning disabled:line-through disabled:btn-neutral disabled:text-white m-2 p-2"
disabled={showGenerating} disabled={showGenerating}
onClick={async () => { onClick={async () => {
const messageIndex = chatStore.history.length - 1; const messageIndex = chatStore.history.length - 1;
@@ -713,7 +962,7 @@ export default function ChatBOX(props: {
)} )}
{chatStore.develop_mode && chatStore.history.length > 0 && ( {chatStore.develop_mode && chatStore.history.length > 0 && (
<button <button
className="disabled:line-through disabled:bg-slate-500 rounded m-2 p-2 border-2 bg-yellow-500 hover:bg-yellow-600" className="btn btn-outline btn-sm btn-warning disabled:line-through disabled:bg-neural"
disabled={showGenerating} disabled={showGenerating}
onClick={async () => { onClick={async () => {
await complete(); await complete();
@@ -804,19 +1053,29 @@ export default function ChatBOX(props: {
</div> </div>
)} )}
<div className="flex justify-between"> {generatingMessage && (
{(chatStore.model.match("vision") || <span
(chatStore.image_gen_api && chatStore.image_gen_key)) && ( className="p-2 m-2 rounded bg-white dark:text-black dark:bg-white dark:bg-opacity-50"
<button style={{ textAlign: "right" }}
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600" onClick={() => {
disabled={showGenerating || !chatStore.apiKey} setFollow(!follow);
onClick={() => { }}
setShowAddImage(!showAddImage); >
}} <label>Follow</label>
> <input type="checkbox" checked={follow} />
Img </span>
</button> )}
)}
<div className="flex justify-between my-1">
<button
className="btn btn-primary disabled:line-through disabled:text-white disabled:bg-neutral m-1 p-1"
disabled={showGenerating || !chatStore.apiKey}
onClick={() => {
setShowAddImage(!showAddImage);
}}
>
Image
</button>
{showAddImage && ( {showAddImage && (
<AddImage <AddImage
chatStore={chatStore} chatStore={chatStore}
@@ -846,11 +1105,14 @@ export default function ChatBOX(props: {
autoHeight(event.target); autoHeight(event.target);
setInputMsg(event.target.value); setInputMsg(event.target.value);
}} }}
className="rounded grow m-1 p-1 border-2 border-gray-400 w-0" className="textarea textarea-bordered textarea-sm grow w-0"
style={{
lineHeight: "1.39",
}}
placeholder="Type here..." placeholder="Type here..."
></textarea> ></textarea>
<button <button
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600" className="btn btn-primary disabled:btn-neutral disabled:line-through m-1 p-1"
disabled={showGenerating} disabled={showGenerating}
onClick={() => { onClick={() => {
send(inputMsg, true); send(inputMsg, true);
@@ -864,10 +1126,8 @@ export default function ChatBOX(props: {
chatStore.whisper_key && chatStore.whisper_key &&
(chatStore.whisper_key || chatStore.apiKey) && ( (chatStore.whisper_key || chatStore.apiKey) && (
<button <button
className={`disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 ${ className={`btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1 ${
isRecording === "Recording" isRecording === "Recording" ? "btn-error" : "btn-success"
? "bg-red-400 hover:bg-red-600"
: "bg-cyan-400 hover:bg-cyan-600"
} ${isRecording !== "Mic" ? "animate-pulse" : ""}`} } ${isRecording !== "Mic" ? "animate-pulse" : ""}`}
disabled={isRecording === "Transcribing"} disabled={isRecording === "Transcribing"}
ref={mediaRef} ref={mediaRef}
@@ -891,7 +1151,7 @@ export default function ChatBOX(props: {
} else { } else {
return content.map((c) => c?.text).join(" "); return content.map((c) => c?.text).join(" ");
} }
}) }),
) )
.concat([inputMsg]) .concat([inputMsg])
.join(" "); .join(" ");
@@ -905,7 +1165,7 @@ export default function ChatBOX(props: {
await navigator.mediaDevices.getUserMedia({ await navigator.mediaDevices.getUserMedia({
audio: true, audio: true,
}), }),
{ audioBitsPerSecond: 64 * 1000 } { audioBitsPerSecond: 64 * 1000 },
); );
// mount mediaRecorder to ref // mount mediaRecorder to ref
@@ -982,7 +1242,7 @@ export default function ChatBOX(props: {
)} )}
{chatStore.develop_mode && ( {chatStore.develop_mode && (
<button <button
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600" className="btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1"
disabled={showGenerating || !chatStore.apiKey} disabled={showGenerating || !chatStore.apiKey}
onClick={() => { onClick={() => {
chatStore.history.push({ chatStore.history.push({
@@ -1006,7 +1266,7 @@ export default function ChatBOX(props: {
)} )}
{chatStore.develop_mode && ( {chatStore.develop_mode && (
<button <button
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600" className="btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1"
disabled={showGenerating || !chatStore.apiKey} disabled={showGenerating || !chatStore.apiKey}
onClick={() => { onClick={() => {
send(inputMsg, false); send(inputMsg, false);
@@ -1017,7 +1277,7 @@ export default function ChatBOX(props: {
)} )}
{chatStore.develop_mode && ( {chatStore.develop_mode && (
<button <button
className="disabled:line-through disabled:bg-slate-500 rounded m-1 p-1 border-2 bg-cyan-400 hover:bg-cyan-600" className="btn disabled:line-through disabled:btn-neutral disabled:text-white m-1 p-1"
disabled={showGenerating || !chatStore.apiKey} disabled={showGenerating || !chatStore.apiKey}
onClick={() => { onClick={() => {
setShowAddToolMsg(true); setShowAddToolMsg(true);
@@ -1065,7 +1325,7 @@ export default function ChatBOX(props: {
</span> </span>
<span className={`flex justify-between p-2`}> <span className={`flex justify-between p-2`}>
<button <button
className="rounded m-1 p-1 border-2 bg-red-400 hover:bg-red-600" className="btn btn-info m-1 p-1"
onClick={() => setShowAddToolMsg(false)} onClick={() => setShowAddToolMsg(false)}
> >
{Tr("Cancle")} {Tr("Cancle")}

View File

@@ -1,3 +1,5 @@
import { defaultModel } from "@/models";
export interface ImageURL { export interface ImageURL {
url: string; url: string;
detail: "low" | "high"; detail: "low" | "high";
@@ -108,7 +110,7 @@ function calculate_token_length_from_text(text: string): number {
} }
// https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them // https://help.openai.com/en/articles/4936856-what-are-tokens-and-how-to-count-them
export function calculate_token_length( export function calculate_token_length(
content: string | MessageDetail[] content: string | MessageDetail[],
): number { ): number {
if (typeof content === "string") { if (typeof content === "string") {
return calculate_token_length_from_text(content); return calculate_token_length_from_text(content);
@@ -155,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 = "gpt-3.5-turbo", model = defaultModel,
temperature = 0.7, temperature = 0.7,
enable_temperature = true, enable_temperature = true,
top_p = 1, top_p = 1,
@@ -163,7 +165,7 @@ class Chat {
presence_penalty = 0, presence_penalty = 0,
frequency_penalty = 0, frequency_penalty = 0,
json_mode = false, json_mode = false,
} = {} } = {},
) { ) {
this.OPENAI_API_KEY = OPENAI_API_KEY ?? ""; this.OPENAI_API_KEY = OPENAI_API_KEY ?? "";
this.messages = []; this.messages = [];
@@ -198,14 +200,14 @@ class Chat {
} }
if (msg.role === "system") { if (msg.role === "system") {
console.log( console.log(
"Warning: detected system message in the middle of history" "Warning: detected system message in the middle of history",
); );
} }
} }
for (const msg of this.messages) { for (const msg of this.messages) {
if (msg.name && msg.role !== "system") { if (msg.name && msg.role !== "system") {
console.log( console.log(
"Warning: detected message where name field set but role is system" "Warning: detected message where name field set but role is system",
); );
} }
} }

View File

@@ -1,15 +1,13 @@
import { Tr, langCodeContext, LANG_OPTIONS, tr } from "./translate"; import { useState, useEffect, StateUpdater, Dispatch } from "preact/hooks";
import { useState, useEffect, StateUpdater } from "preact/hooks"; import { Tr, langCodeContext, LANG_OPTIONS, tr } from "@/translate";
import { ChatStore, ChatStoreMessage } from "./app"; import { ChatStore, ChatStoreMessage } from "@/app";
import { calculate_token_length, getMessageText } from "./chatgpt"; import { EditMessageString } from "@/editMessageString";
import { isVailedJSON } from "./message"; import { EditMessageDetail } from "@/editMessageDetail";
import { EditMessageString } from "./editMessageString";
import { EditMessageDetail } from "./editMessageDetail";
interface EditMessageProps { interface EditMessageProps {
chat: ChatStoreMessage; chat: ChatStoreMessage;
chatStore: ChatStore; chatStore: ChatStore;
setShowEdit: StateUpdater<boolean>; setShowEdit: Dispatch<StateUpdater<boolean>>;
setChatStore: (cs: ChatStore) => void; setChatStore: (cs: ChatStore) => void;
} }
export function EditMessage(props: EditMessageProps) { export function EditMessage(props: EditMessageProps) {
@@ -44,17 +42,29 @@ export function EditMessage(props: EditMessageProps) {
/> />
)} )}
<div className={"w-full flex justify-center"}> <div className={"w-full flex justify-center"}>
{chatStore.develop_mode && <button {chatStore.develop_mode && (
className="w-full m-2 p-1 rounded bg-red-500" <button
onClick={() => { className="w-full m-2 p-1 rounded bg-red-500"
if (typeof chat.content === "string") { onClick={() => {
chat.content = [] const confirm = window.confirm(
} else { "Change message type will clear the content, are you sure?",
chat.content = '' );
} if (!confirm) return;
setChatStore({ ...chatStore })
}} if (typeof chat.content === "string") {
>Switch to {typeof chat.content === 'string' ? "media message" : "string message"}</button>} chat.content = [];
} else {
chat.content = "";
}
setChatStore({ ...chatStore });
}}
>
Switch to{" "}
{typeof chat.content === "string"
? "media message"
: "string message"}
</button>
)}
<button <button
className={"w-full m-2 p-1 rounded bg-purple-500"} className={"w-full m-2 p-1 rounded bg-purple-500"}
onClick={() => { onClick={() => {

View File

@@ -1,6 +1,6 @@
import { ChatStore, ChatStoreMessage } from "./app"; import { ChatStore, ChatStoreMessage } from "@/app";
import { calculate_token_length } from "./chatgpt"; import { calculate_token_length } from "@/chatgpt";
import { Tr } from "./translate"; import { Tr } from "@/translate";
interface Props { interface Props {
chat: ChatStoreMessage; chat: ChatStoreMessage;
@@ -22,10 +22,10 @@ export function EditMessageDetail({
> >
{chat.content.map((mdt, index) => ( {chat.content.map((mdt, index) => (
<div className={"w-full p-2 px-4"}> <div className={"w-full p-2 px-4"}>
<div className="flex justify-between"> <div className="flex justify-center">
{mdt.type === "text" ? ( {mdt.type === "text" ? (
<textarea <textarea
className={"w-full"} className={"w-full border p-1 rounded"}
value={mdt.text} value={mdt.text}
onChange={(event: any) => { onChange={(event: any) => {
if (typeof chat.content === "string") return; if (typeof chat.content === "string") return;
@@ -41,16 +41,16 @@ export function EditMessageDetail({
}} }}
></textarea> ></textarea>
) : ( ) : (
<> <div className="border p-1 rounded">
<img <img
className="max-h-32 max-w-xs cursor-pointer" className="max-h-32 max-w-xs cursor-pointer m-2"
src={mdt.image_url?.url} src={mdt.image_url?.url}
onClick={() => { onClick={() => {
window.open(mdt.image_url?.url, "_blank"); window.open(mdt.image_url?.url, "_blank");
}} }}
/> />
<button <button
className="bg-blue-300 p-1 rounded" className="bg-blue-300 p-1 rounded m-1"
onClick={() => { onClick={() => {
const image_url = prompt("image url", mdt.image_url?.url); const image_url = prompt("image url", mdt.image_url?.url);
if (image_url) { if (image_url) {
@@ -65,7 +65,7 @@ export function EditMessageDetail({
{Tr("Edit URL")} {Tr("Edit URL")}
</button> </button>
<button <button
className="bg-blue-300 p-1 rounded" className="bg-blue-300 p-1 rounded m-1"
onClick={() => { onClick={() => {
// select file and load it to base64 image URL format // select file and load it to base64 image URL format
const input = document.createElement("input"); const input = document.createElement("input");
@@ -95,7 +95,7 @@ export function EditMessageDetail({
{Tr("Upload")} {Tr("Upload")}
</button> </button>
<span <span
className="bg-blue-300 p-1 rounded" className="bg-blue-300 p-1 rounded m-1"
onClick={() => { onClick={() => {
if (typeof chat.content === "string") return; if (typeof chat.content === "string") return;
const obj = chat.content[index].image_url; const obj = chat.content[index].image_url;
@@ -111,7 +111,7 @@ export function EditMessageDetail({
checked={mdt.image_url?.detail === "high"} checked={mdt.image_url?.detail === "high"}
/> />
</span> </span>
</> </div>
)} )}
<button <button

View File

@@ -1,7 +1,7 @@
import { ChatStore, ChatStoreMessage } from "./app"; import { ChatStore, ChatStoreMessage } from "@/app";
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";
interface Props { interface Props {
chat: ChatStoreMessage; chat: ChatStoreMessage;
@@ -69,7 +69,7 @@ export function EditMessageString({
onClick={() => { onClick={() => {
if (!chat.tool_calls) return; if (!chat.tool_calls) return;
chat.tool_calls = chat.tool_calls.filter( chat.tool_calls = chat.tool_calls.filter(
(tc) => tc.id !== tool_call.id (tc) => tc.id !== tool_call.id,
); );
setChatStore({ ...chatStore }); setChatStore({ ...chatStore });
}} }}

View File

@@ -30,6 +30,7 @@ body::-webkit-scrollbar {
.message-content { .message-content {
white-space: pre-wrap; white-space: pre-wrap;
word-wrap: anywhere;
} }
.markup > h2 { .markup > h2 {
@@ -78,8 +79,14 @@ body::-webkit-scrollbar {
white-space: break-space; white-space: break-space;
background-color: rgba(175, 184, 193, 0.2); background-color: rgba(175, 184, 193, 0.2);
border-radius: 6px; border-radius: 6px;
font-family: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, font-family:
Liberation Mono, monospace; ui-monospace,
SFMono-Regular,
SF Mono,
Menlo,
Consolas,
Liberation Mono,
monospace;
} }
.markup > pre { .markup > pre {
@@ -138,3 +145,7 @@ body::-webkit-scrollbar {
background-color: #f5f5f5; background-color: #f5f5f5;
z-index: -1; z-index: -1;
} }
.stat {
padding: 0.39rem;
}

View File

@@ -1,5 +1,5 @@
import { ChatStore, TemplateAPI } from "./app"; import { ChatStore, TemplateAPI } from "@/app";
import { Tr } from "./translate"; import { Tr } from "@/translate";
interface Props { interface Props {
chatStore: ChatStore; chatStore: ChatStore;
@@ -20,7 +20,7 @@ export function ListAPIs({
keyField, keyField,
}: Props) { }: Props) {
return ( return (
<div className="break-all opacity-80 p-3 rounded bg-white my-3 text-left dark:text-black"> <div className="break-all opacity-80 p-3 rounded base-200 my-3 text-left">
<h2>{Tr(`Saved ${label} templates`)}</h2> <h2>{Tr(`Saved ${label} templates`)}</h2>
<hr className="my-2" /> <hr className="my-2" />
<div className="flex flex-wrap"> <div className="flex flex-wrap">
@@ -31,8 +31,8 @@ export function ListAPIs({
chatStore[apiField] === t.endpoint && chatStore[apiField] === t.endpoint &&
// @ts-ignore // @ts-ignore
chatStore[keyField] === t.key chatStore[keyField] === t.key
? "bg-red-600" ? "bg-info"
: "bg-red-400" : "bg-base-300"
} w-fit p-2 m-1 flex flex-col`} } w-fit p-2 m-1 flex flex-col`}
onClick={() => { onClick={() => {
// @ts-ignore // @ts-ignore
@@ -43,9 +43,9 @@ export function ListAPIs({
}} }}
> >
<span className="w-full text-center">{t.name}</span> <span className="w-full text-center">{t.name}</span>
<hr className="mt-2" /> <span className="flex justify-between gap-x-2">
<span className="flex justify-between">
<button <button
className="link"
onClick={() => { onClick={() => {
const name = prompt(`Give **${label}** template a name`); const name = prompt(`Give **${label}** template a name`);
if (!name) { if (!name) {
@@ -55,13 +55,14 @@ export function ListAPIs({
setTmps(structuredClone(tmps)); setTmps(structuredClone(tmps));
}} }}
> >
🖋 Edit
</button> </button>
<button <button
className="link"
onClick={() => { onClick={() => {
if ( if (
!confirm( !confirm(
`Are you sure to delete this **${label}** template?` `Are you sure to delete this **${label}** template?`,
) )
) { ) {
return; return;
@@ -70,7 +71,7 @@ export function ListAPIs({
setTmps(structuredClone(tmps)); setTmps(structuredClone(tmps));
}} }}
> >
Delete
</button> </button>
</span> </span>
</div> </div>

View File

@@ -1,5 +1,5 @@
import { ChatStore, TemplateTools } from "./app"; import { ChatStore, TemplateTools } from "@/app";
import { Tr } from "./translate"; import { Tr } from "@/translate";
interface Props { interface Props {
templateTools: TemplateTools[]; templateTools: TemplateTools[];
@@ -33,8 +33,8 @@ export function ListToolsTempaltes({
<div <div
className={`cursor-pointer rounded ${ className={`cursor-pointer rounded ${
chatStore.toolsString === t.toolsString chatStore.toolsString === t.toolsString
? "bg-red-600" ? "bg-info"
: "bg-red-400" : "bg-base-300"
} w-fit p-2 m-1 flex flex-col`} } w-fit p-2 m-1 flex flex-col`}
onClick={() => { onClick={() => {
chatStore.toolsString = t.toolsString; chatStore.toolsString = t.toolsString;
@@ -42,9 +42,9 @@ export function ListToolsTempaltes({
}} }}
> >
<span className="w-full text-center">{t.name}</span> <span className="w-full text-center">{t.name}</span>
<hr className="mt-2" /> <span className="flex justify-between gap-x-2">
<span className="flex justify-between">
<button <button
className="link"
onClick={() => { onClick={() => {
const name = prompt(`Give **tools** template a name`); const name = prompt(`Give **tools** template a name`);
if (!name) { if (!name) {
@@ -54,9 +54,10 @@ export function ListToolsTempaltes({
setTemplateTools(structuredClone(templateTools)); setTemplateTools(structuredClone(templateTools));
}} }}
> >
🖋 Edit
</button> </button>
<button <button
className="link"
onClick={() => { onClick={() => {
if ( if (
!confirm(`Are you sure to delete this **tools** template?`) !confirm(`Are you sure to delete this **tools** template?`)
@@ -67,7 +68,7 @@ export function ListToolsTempaltes({
setTemplateTools(structuredClone(templateTools)); setTemplateTools(structuredClone(templateTools));
}} }}
> >
Delete
</button> </button>
</span> </span>
</div> </div>

View File

@@ -6,7 +6,7 @@ const logprobToColor = (logprob: number) => {
// 绿色的RGB值为(0, 255, 0)红色的RGB值为(255, 0, 0) // 绿色的RGB值为(0, 255, 0)红色的RGB值为(255, 0, 0)
const red = Math.round(255 * (1 - percent / 100)); const red = Math.round(255 * (1 - percent / 100));
const green = Math.round(255 * (percent / 100)); const green = Math.round(255 * (percent / 100));
const color = `rgb(${red}, ${green}, 0)`; const color = `rgba(${red}, ${green}, 0, 0.5)`;
return color; return color;
}; };

View File

@@ -1,27 +1,29 @@
import { themeChange } from "theme-change";
import { render } from "preact"; import { render } from "preact";
import { App } from "./app";
import { useState, useEffect } from "preact/hooks"; import { useState, useEffect } from "preact/hooks";
import { Tr, langCodeContext, LANG_OPTIONS } from "./translate"; import { App } from "@/app";
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
function Base() { function Base() {
const [langCode, _setLangCode] = useState("en-US"); const [langCode, _setLangCode] = useState("en-US");
const setLangCode = (langCode: string) => { const setLangCode = (langCode: string) => {
_setLangCode(langCode) _setLangCode(langCode);
if (!localStorage) return if (!localStorage) return;
localStorage.setItem('chatgpt-api-web-lang', langCode) localStorage.setItem("chatgpt-api-web-lang", langCode);
} };
// select language // select language
useEffect(() => { useEffect(() => {
themeChange(false);
// query localStorage // query localStorage
if (localStorage) { if (localStorage) {
const lang = localStorage.getItem('chatgpt-api-web-lang') const lang = localStorage.getItem("chatgpt-api-web-lang");
if (lang) { if (lang) {
console.log(`query langCode ${lang} from localStorage`) console.log(`query langCode ${lang} from localStorage`);
_setLangCode(lang) _setLangCode(lang);
return return;
} }
} }

View File

@@ -1,15 +1,17 @@
import { Tr, langCodeContext, LANG_OPTIONS } from "./translate"; import { XMarkIcon } from "@heroicons/react/24/outline";
import { useState, useEffect, StateUpdater } from "preact/hooks";
import { ChatStore, ChatStoreMessage } from "./app";
import { calculate_token_length, getMessageText } from "./chatgpt";
import Markdown from "preact-markdown"; import Markdown from "preact-markdown";
import TTSButton, { TTSPlay } from "./tts"; import { useState, useEffect, StateUpdater } from "preact/hooks";
import { MessageHide } from "./messageHide";
import { MessageDetail } from "./messageDetail"; import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
import { MessageToolCall } from "./messageToolCall"; import { ChatStore, ChatStoreMessage } from "@/app";
import { MessageToolResp } from "./messageToolResp"; import { calculate_token_length, getMessageText } from "@/chatgpt";
import { EditMessage } from "./editMessage"; import TTSButton, { TTSPlay } from "@/tts";
import logprobToColor from "./logprob"; import { MessageHide } from "@/messageHide";
import { MessageDetail } from "@/messageDetail";
import { MessageToolCall } from "@/messageToolCall";
import { MessageToolResp } from "@/messageToolResp";
import { EditMessage } from "@/editMessage";
import logprobToColor from "@/logprob";
export const isVailedJSON = (str: string): boolean => { export const isVailedJSON = (str: string): boolean => {
try { try {
@@ -51,17 +53,26 @@ export default function Message(props: Props) {
setChatStore({ ...chatStore }); setChatStore({ ...chatStore });
}} }}
> >
🗑 Delete
</button> </button>
); );
const CopiedHint = () => ( const CopiedHint = () => (
<span <div role="alert" className="alert">
className={ <svg
"bg-purple-400 p-1 rounded shadow-md absolute z-20 left-1/2 top-3/4 transform -translate-x-1/2 -translate-y-1/2" xmlns="http://www.w3.org/2000/svg"
} fill="none"
> viewBox="0 0 24 24"
{Tr("Message copied to clipboard!")} className="stroke-info h-6 w-6 shrink-0"
</span> >
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span>{Tr("Message copied to clipboard!")}</span>
</div>
); );
const copyToClipboard = (text: string) => { const copyToClipboard = (text: string) => {
@@ -78,7 +89,7 @@ export default function Message(props: Props) {
copyToClipboard(textToCopy); copyToClipboard(textToCopy);
}} }}
> >
📋 Copy
</button> </button>
</> </>
); );
@@ -92,8 +103,8 @@ export default function Message(props: Props) {
chatStore.history.slice(0, messageIndex).filter(({ hide }) => !hide) chatStore.history.slice(0, messageIndex).filter(({ hide }) => !hide)
.length && ( .length && (
<div className="flex items-center relative justify-center"> <div className="flex items-center relative justify-center">
<hr className="w-full h-px my-4 border-0 bg-slate-800 dark:bg-white" /> <hr className="w-full h-px my-4 border-0" />
<span className="absolute px-3 bg-slate-800 text-white rounded p-1 dark:bg-white dark:text-black"> <span className="absolute px-3 rounded p-1">
Above messages are "forgotten" Above messages are "forgotten"
</span> </span>
</div> </div>
@@ -103,53 +114,66 @@ export default function Message(props: Props) {
chat.role === "assistant" ? "justify-start" : "justify-end" chat.role === "assistant" ? "justify-start" : "justify-end"
}`} }`}
> >
<div> <div className={`w-full`}>
<div <div
className={`w-fit p-2 rounded my-2 ${ className={`chat min-w-16 p-2 my-2 ${
chat.role === "assistant" chat.role === "assistant" ? "chat-start" : "chat-end"
? "bg-white dark:bg-gray-700 dark:text-white"
: "bg-green-400"
} ${chat.hide ? "opacity-50" : ""}`} } ${chat.hide ? "opacity-50" : ""}`}
> >
{chat.hide ? ( <div
<MessageHide chat={chat} /> className={`chat-bubble max-w-full ${
) : typeof chat.content !== "string" ? ( chat.role === "assistant"
<MessageDetail chat={chat} renderMarkdown={renderMarkdown} /> ? renderColor
) : chat.tool_calls ? ( ? "chat-bubble-neutral"
<MessageToolCall chat={chat} copyToClipboard={copyToClipboard} /> : "chat-bubble-secondary"
) : chat.role === "tool" ? ( : "chat-bubble-primary"
<MessageToolResp chat={chat} copyToClipboard={copyToClipboard} /> }`}
) : renderMarkdown ? ( >
// @ts-ignore {chat.hide ? (
<Markdown markdown={getMessageText(chat)} /> <MessageHide chat={chat} />
) : ( ) : typeof chat.content !== "string" ? (
<div className="message-content"> <MessageDetail chat={chat} renderMarkdown={renderMarkdown} />
{ ) : chat.tool_calls ? (
// only show when content is string or list of message <MessageToolCall
// this check is used to avoid rendering tool call chat={chat}
chat.content && copyToClipboard={copyToClipboard}
(chat.logprobs && renderColor />
? chat.logprobs.content ) : chat.role === "tool" ? (
.filter((c) => c.token) <MessageToolResp
.map((c) => ( chat={chat}
<div copyToClipboard={copyToClipboard}
style={{ />
color: logprobToColor(c.logprob), ) : renderMarkdown ? (
display: "inline", // @ts-ignore
}} <Markdown markdown={getMessageText(chat)} />
> ) : (
{c.token} <div className="message-content">
</div> {
)) // only show when content is string or list of message
: getMessageText(chat)) // this check is used to avoid rendering tool call
} chat.content &&
</div> (chat.logprobs && renderColor
)} ? chat.logprobs.content
<hr className="mt-2" /> .filter((c) => c.token)
<TTSPlay chat={chat} /> .map((c) => (
<div className="w-full flex justify-between"> <div
style={{
backgroundColor: logprobToColor(c.logprob),
display: "inline",
}}
>
{c.token}
</div>
))
: getMessageText(chat))
}
</div>
)}
</div>
<div className="chat-footer opacity-50 flex gap-x-2">
<DeleteIcon /> <DeleteIcon />
<button onClick={() => setShowEdit(true)}>🖋</button> <button onClick={() => setShowEdit(true)}>Edit</button>
<CopyIcon textToCopy={getMessageText(chat)} />
{chatStore.tts_api && chatStore.tts_key && ( {chatStore.tts_api && chatStore.tts_key && (
<TTSButton <TTSButton
chatStore={chatStore} chatStore={chatStore}
@@ -157,7 +181,7 @@ export default function Message(props: Props) {
setChatStore={setChatStore} setChatStore={setChatStore}
/> />
)} )}
<CopyIcon textToCopy={getMessageText(chat)} /> <TTSPlay chat={chat} />
</div> </div>
</div> </div>
{showEdit && ( {showEdit && (
@@ -170,11 +194,15 @@ export default function Message(props: Props) {
)} )}
{showCopiedHint && <CopiedHint />} {showCopiedHint && <CopiedHint />}
{chatStore.develop_mode && ( {chatStore.develop_mode && (
<div> <div
<span className="dark:text-white">token</span> className={`gap-1 chat-end flex ${
chat.role === "assistant" ? "justify-start" : "justify-end"
}`}
>
<span className="">token</span>
<input <input
value={chat.token} value={chat.token}
className="w-20" 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(); props.update_total_tokens();
@@ -186,7 +214,7 @@ export default function Message(props: Props) {
chatStore.history.splice(messageIndex, 1); chatStore.history.splice(messageIndex, 1);
chatStore.postBeginIndex = Math.max( chatStore.postBeginIndex = Math.max(
chatStore.postBeginIndex - 1, chatStore.postBeginIndex - 1,
0 0,
); );
//chatStore.totalTokens = //chatStore.totalTokens =
chatStore.totalTokens = 0; chatStore.totalTokens = 0;
@@ -199,7 +227,7 @@ export default function Message(props: Props) {
setChatStore({ ...chatStore }); setChatStore({ ...chatStore });
}} }}
> >
<XMarkIcon className="w-4 h-4" />
</button> </button>
<span <span
onClick={(event: any) => { onClick={(event: any) => {
@@ -207,17 +235,17 @@ export default function Message(props: Props) {
setChatStore({ ...chatStore }); setChatStore({ ...chatStore });
}} }}
> >
<label className="dark:text-white">{Tr("example")}</label> <label className="">{Tr("example")}</label>
<input type="checkbox" checked={chat.example} /> <input type="checkbox" checked={chat.example} />
</span> </span>
<span <span
onClick={(event: any) => setRenderWorkdown(!renderMarkdown)} onClick={(event: any) => setRenderWorkdown(!renderMarkdown)}
> >
<label className="dark:text-white">{Tr("render")}</label> <label className="">{Tr("render")}</label>
<input type="checkbox" checked={renderMarkdown} /> <input type="checkbox" checked={renderMarkdown} />
</span> </span>
<span onClick={(event: any) => setRenderColor(!renderColor)}> <span onClick={(event: any) => setRenderColor(!renderColor)}>
<label className="dark:text-white">{Tr("color")}</label> <label className="">{Tr("color")}</label>
<input type="checkbox" checked={renderColor} /> <input type="checkbox" checked={renderColor} />
</span> </span>
</div> </div>

View File

@@ -1,4 +1,4 @@
import { ChatStoreMessage } from "./app"; import { ChatStoreMessage } from "@/app";
interface Props { interface Props {
chat: ChatStoreMessage; chat: ChatStoreMessage;
@@ -13,7 +13,7 @@ export function MessageDetail({ chat, renderMarkdown }: Props) {
{chat.content.map((mdt) => {chat.content.map((mdt) =>
mdt.type === "text" ? ( mdt.type === "text" ? (
chat.hide ? ( chat.hide ? (
mdt.text?.split("\n")[0].slice(0, 16) + "... (deleted)" mdt.text?.split("\n")[0].slice(0, 16) + " ..."
) : renderMarkdown ? ( ) : renderMarkdown ? (
// @ts-ignore // @ts-ignore
<Markdown markdown={mdt.text} /> <Markdown markdown={mdt.text} />
@@ -22,13 +22,13 @@ export function MessageDetail({ chat, renderMarkdown }: Props) {
) )
) : ( ) : (
<img <img
className="cursor-pointer max-w-xs max-h-32 p-1" className="my-2 rounded-md max-w-64 max-h-64"
src={mdt.image_url?.url} src={mdt.image_url?.url}
onClick={() => { onClick={() => {
window.open(mdt.image_url?.url, "_blank"); window.open(mdt.image_url?.url, "_blank");
}} }}
/> />
) ),
)} )}
</div> </div>
); );

View File

@@ -1,12 +1,10 @@
import { ChatStoreMessage } from "./app"; import { ChatStoreMessage } from "@/app";
import { getMessageText } from "./chatgpt"; import { getMessageText } from "@/chatgpt";
interface Props { interface Props {
chat: ChatStoreMessage; chat: ChatStoreMessage;
} }
export function MessageHide({ chat }: Props) { export function MessageHide({ chat }: Props) {
return ( return <div>{getMessageText(chat).split("\n")[0].slice(0, 18)} ...</div>;
<div>{getMessageText(chat).split("\n")[0].slice(0, 18)} ... (deleted)</div>
);
} }

View File

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

View File

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

View File

@@ -7,6 +7,26 @@ interface Model {
} }
const models: Record<string, Model> = { const models: Record<string, Model> = {
"gpt-4o": {
maxToken: 128000,
price: { prompt: 0.005 / 1000, completion: 0.015 / 1000 },
},
"gpt-4o-2024-08-06": {
maxToken: 128000,
price: { prompt: 0.0025 / 1000, completion: 0.01 / 1000 },
},
"gpt-4o-2024-05-13": {
maxToken: 128000,
price: { prompt: 0.005 / 1000, completion: 0.015 / 1000 },
},
"gpt-4o-mini": {
maxToken: 128000,
price: { prompt: 0.15 / 1000 / 1000, completion: 0.6 / 1000 / 1000 },
},
"gpt-4o-mini-2024-07-18": {
maxToken: 128000,
price: { prompt: 0.15 / 1000 / 1000, completion: 0.6 / 1000 / 1000 },
},
"gpt-3.5-turbo-0125": { "gpt-3.5-turbo-0125": {
maxToken: 16385, maxToken: 16385,
price: { prompt: 0.0005 / 1000, completion: 0.0015 / 1000 }, price: { prompt: 0.0005 / 1000, completion: 0.0015 / 1000 },
@@ -23,19 +43,11 @@ const models: Record<string, Model> = {
maxToken: 16385, maxToken: 16385,
price: { prompt: 0.003 / 1000, completion: 0.004 / 1000 }, price: { prompt: 0.003 / 1000, completion: 0.004 / 1000 },
}, },
"gpt-3.5-turbo-0613": { "gpt-4-turbo": {
maxToken: 4096, maxToken: 128000,
price: { prompt: 0.0015 / 1000, completion: 0.002 / 1000 }, price: { prompt: 0.01 / 1000, completion: 0.03 / 1000 },
}, },
"gpt-3.5-turbo-16k-0613": { "gpt-4-turbo-2024-04-09": {
maxToken: 16385,
price: { prompt: 0.003 / 1000, completion: 0.004 / 1000 },
},
"gpt-3.5-turbo-0301": {
maxToken: 4096,
price: { prompt: 0.0015 / 1000, completion: 0.002 / 1000 },
},
"gpt-4-0125-preview": {
maxToken: 128000, maxToken: 128000,
price: { prompt: 0.01 / 1000, completion: 0.03 / 1000 }, price: { prompt: 0.01 / 1000, completion: 0.03 / 1000 },
}, },
@@ -43,6 +55,10 @@ const models: Record<string, Model> = {
maxToken: 128000, maxToken: 128000,
price: { prompt: 0.01 / 1000, completion: 0.03 / 1000 }, price: { prompt: 0.01 / 1000, completion: 0.03 / 1000 },
}, },
"gpt-4-0125-preview": {
maxToken: 128000,
price: { prompt: 0.01 / 1000, completion: 0.03 / 1000 },
},
"gpt-4-1106-preview": { "gpt-4-1106-preview": {
maxToken: 128000, maxToken: 128000,
price: { prompt: 0.01 / 1000, completion: 0.03 / 1000 }, price: { prompt: 0.01 / 1000, completion: 0.03 / 1000 },
@@ -59,20 +75,12 @@ const models: Record<string, Model> = {
maxToken: 8192, maxToken: 8192,
price: { prompt: 0.03 / 1000, completion: 0.06 / 1000 }, price: { prompt: 0.03 / 1000, completion: 0.06 / 1000 },
}, },
"gpt-4-0613": {
maxToken: 8192,
price: { prompt: 0.03 / 1000, completion: 0.06 / 1000 },
},
"gpt-4-32k": { "gpt-4-32k": {
maxToken: 8192, maxToken: 8192,
price: { prompt: 0.06 / 1000, completion: 0.12 / 1000 }, price: { prompt: 0.06 / 1000, completion: 0.12 / 1000 },
}, },
"gpt-4-32k-0613": {
maxToken: 8192,
price: { prompt: 0.06 / 1000, completion: 0.12 / 1000 },
},
}; };
export const defaultModel = "gpt-3.5-turbo-0125"; export const defaultModel = "gpt-4o-mini";
export default models; export default models;

180
src/search.tsx Normal file
View File

@@ -0,0 +1,180 @@
import { IDBPDatabase } from "idb";
import { StateUpdater, useRef, useState, Dispatch } from "preact/hooks";
import { ChatStore } from "@/app";
interface ChatStoreSearchResult {
key: IDBValidKey;
cs: ChatStore;
query: string;
preview: string;
}
export default function Search(props: {
db: Promise<IDBPDatabase<ChatStore>>;
setSelectedChatIndex: Dispatch<StateUpdater<number>>;
chatStore: ChatStore;
setShow: (show: boolean) => void;
}) {
const [searchResult, setSearchResult] = useState<ChatStoreSearchResult[]>([]);
const [searching, setSearching] = useState<boolean>(false);
const [searchingNow, setSearchingNow] = useState<number>(0);
const [pageIndex, setPageIndex] = useState<number>(0);
const searchAbortRef = useRef<AbortController | null>(null);
return (
<div
onClick={() => props.setShow(false)}
className="left-0 top-0 overflow-scroll flex justify-center absolute w-screen h-full bg-black bg-opacity-50 z-10"
>
<div
onClick={(event: any) => {
event.stopPropagation();
}}
className="m-2 p-2 bg-base-300 rounded-lg h-fit w-2/3 z-20"
>
<div className="flex justify-between">
<span className="m-1 p-1 font-bold">Search</span>
<button
className="m-1 p-1 btn btn-sm btn-secondary"
onClick={() => props.setShow(false)}
>
Close
</button>
</div>
<div>
<input
autoFocus
className="input input-bordered w-full border"
type="text"
placeholder="Type Something..."
onInput={async (event: any) => {
const query = event.target.value.trim().toLowerCase();
if (!query) {
setSearchResult([]);
return;
}
// abort previous search
if (searchAbortRef.current) {
searchAbortRef.current.abort();
}
// Create a new AbortController for the new operation
const abortController = new AbortController();
searchAbortRef.current = abortController;
const signal = abortController.signal;
setSearching(true);
const db = await props.db;
const resultKeys = await db.getAllKeys("chatgpt-api-web");
const result: ChatStoreSearchResult[] = [];
for (const key of resultKeys) {
// abort the operation if the signal is set
if (signal.aborted) {
return;
}
const now = Math.floor(
(result.length / resultKeys.length) * 100,
);
if (now !== searchingNow) setSearchingNow(now);
const value: ChatStore = await db.get("chatgpt-api-web", key);
const content = value.contents_for_index
.join(" ")
.toLowerCase();
if (content.includes(query)) {
const beginIndex: number = content.indexOf(query);
const preview = content.slice(
Math.max(0, beginIndex - 100),
Math.min(content.length, beginIndex + 239),
);
result.push({
key,
cs: value,
query: query,
preview: preview,
});
}
}
// sort by key desc
result.sort((a, b) => {
if (a.key < b.key) {
return 1;
}
if (a.key > b.key) {
return -1;
}
return 0;
});
console.log(result);
setPageIndex(0);
setSearchResult(result);
setSearching(false);
}}
/>
</div>
{searching && <div>Searching {searchingNow}%...</div>}
<div>
{searchResult
.slice(pageIndex * 10, (pageIndex + 1) * 10)
.map((result: ChatStoreSearchResult) => {
return (
<div
className="flex justify-start p-1 m-1 rounded border bg-base-200 cursor-pointer"
key={result.key}
onClick={() => {
props.setSelectedChatIndex(parseInt(result.key.toString()));
props.setShow(false);
}}
>
<div className="m-1 p-1 font-bold">{result.key}</div>
<div className="m-1 p-1">{result.preview}</div>
</div>
);
})}
</div>
{searchResult.length > 0 && (
<div className="flex justify-center my-2">
<div className="join">
<button
className="join-item btn btn-sm"
disabled={pageIndex === 0}
onClick={() => {
if (pageIndex === 0) {
return;
}
setPageIndex(pageIndex - 1);
}}
>
«
</button>
<button className="join-item btn btn-sm">
Page {pageIndex + 1} /{" "}
{Math.floor(searchResult.length / 10) + 1}
</button>
<button
className="join-item btn btn-sm"
disabled={pageIndex === Math.floor(searchResult.length / 10)}
onClick={() => {
if (pageIndex === Math.floor(searchResult.length / 10)) {
return;
}
setPageIndex(pageIndex + 1);
}}
>
»
</button>
</div>
</div>
)}
</div>
</div>
);
}

View File

@@ -1,5 +1,5 @@
import { TemplateAPI } from "./app"; import { TemplateAPI } from "@/app";
import { Tr } from "./translate"; import { Tr } from "@/translate";
interface Props { interface Props {
tmps: TemplateAPI[]; tmps: TemplateAPI[];
@@ -17,7 +17,7 @@ export function SetAPIsTemplate({
}: Props) { }: Props) {
return ( return (
<button <button
className="p-1 m-1 rounded bg-blue-300" className="btn btn-primary btn-sm mt-3"
onClick={() => { onClick={() => {
const name = prompt(`Give this **${label}** template a name:`); const name = prompt(`Give this **${label}** template a name:`);
if (!name) { if (!name) {

File diff suppressed because it is too large Load Diff

View File

@@ -3,7 +3,7 @@ export const autoHeight = (target: any) => {
// max 70% of screen height // max 70% of screen height
target.style.height = `${Math.min( target.style.height = `${Math.min(
target.scrollHeight, target.scrollHeight,
window.innerHeight * 0.7 window.innerHeight * 0.7,
)}px`; )}px`;
console.log("set auto height", target.style.height); console.log("set auto height", target.style.height);
}; };

View File

@@ -1,5 +1,5 @@
import { createContext } from "preact"; import { createContext } from "preact";
import MAP_zh_CN from "./zh_CN"; import MAP_zh_CN from "@/translate/zh_CN";
interface LangOption { interface LangOption {
name: string; name: string;

View File

@@ -1,6 +1,8 @@
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 { Message, getMessageText } from "./chatgpt"; import { ChatStore, ChatStoreMessage, addTotalCost } from "@/app";
import { Message, getMessageText } from "@/chatgpt";
interface TTSProps { interface TTSProps {
chatStore: ChatStore; chatStore: ChatStore;
@@ -78,7 +80,11 @@ export default function TTSButton(props: TTSProps) {
}); });
}} }}
> >
{generating ? "🤔" : "🔈"} {generating ? (
<span className="loading loading-dots loading-xs"></span>
) : (
<SpeakerWaveIcon className="h-4 w-4" />
)}
</button> </button>
); );
} }

View File

@@ -1,8 +1,42 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"], content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
daisyui: {
themes: ["light",
"dark",
"cupcake",
"bumblebee",
"emerald",
"corporate",
"synthwave",
"retro",
"cyberpunk",
"valentine",
"halloween",
"garden",
"forest",
"aqua",
"lofi",
"pastel",
"fantasy",
"wireframe",
"black",
"luxury",
"dracula",
"cmyk",
"autumn",
"business",
"acid",
"lemonade",
"night",
"coffee",
"winter",
"dim",
"nord",
"sunset",],
},
theme: { theme: {
extend: {}, extend: {},
}, },
plugins: [], plugins: [require('daisyui')],
}; };

View File

@@ -1,5 +1,9 @@
{ {
"compilerOptions": { "compilerOptions": {
"baseUrl": "src",
"paths": {
"@/*": ["*"]
},
"target": "ESNext", "target": "ESNext",
"useDefineForClassFields": true, "useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"], "lib": ["DOM", "DOM.Iterable", "ESNext"],

View File

@@ -1,8 +1,14 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import preact from '@preact/preset-vite' import preact from '@preact/preset-vite'
import path from 'path'
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [preact()], plugins: [preact()],
base: './', base: './',
resolve: {
alias: {
'@': path.resolve(__dirname, 'src')
}
}
}) })

983
yarn.lock

File diff suppressed because it is too large Load Diff