Merge pull request #2 from ecwu/master

Refactor UI Design with DaisyUI
This commit is contained in:
2024-07-18 09:14:06 +08:00
committed by GitHub
19 changed files with 3700 additions and 1068 deletions

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

2268
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@heroicons/react": "^2.1.5",
"@types/ungap__structured-clone": "^0.3.1", "@types/ungap__structured-clone": "^0.3.1",
"@ungap/structured-clone": "^1.2.0", "@ungap/structured-clone": "^1.2.0",
"autoprefixer": "^10.4.16", "autoprefixer": "^10.4.16",
@@ -21,6 +22,8 @@
}, },
"devDependencies": { "devDependencies": {
"@preact/preset-vite": "^2.6.0", "@preact/preset-vite": "^2.6.0",
"daisyui": "^4.12.10",
"theme-change": "^2.5.0",
"typescript": "^5.2.2", "typescript": "^5.2.2",
"vite": "^4.5.0" "vite": "^4.5.0"
} }

View File

@@ -41,15 +41,15 @@ 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();
}} }}
> >
<div className="flex justify-between p-1"> <div className="flex justify-between items-center p-1">
<span>Add Images</span> <h3>Add Images</h3>
<button <button
className="m-1 p-1 border-2 bg-red-400" className="btn btn-sm btn-neutral"
onClick={() => { onClick={() => {
setShowAddImage(false); setShowAddImage(false);
}} }}
@@ -57,10 +57,9 @@ export function AddImage({
Done Done
</button> </button>
</div> </div>
<hr /> <span class="">
<span>
<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-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) {
@@ -81,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");
@@ -122,23 +121,24 @@ export function AddImage({
<input type="checkbox" checked={enableHighResolution} /> <input type="checkbox" checked={enableHighResolution} />
</span> </span>
</span> </span>
<div class="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
class="select select-sm select-bordered"
value={imageGenModel} value={imageGenModel}
onChange={(e: any) => { onChange={(e: any) => {
setImageGenModel(e.target.value); setImageGenModel(e.target.value);
@@ -148,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
class="input input-sm input-bordered"
value={imageGenN} value={imageGenN}
type="number" type="number"
min={1} min={1}
@@ -158,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
class="select select-sm select-bordered"
value={imageGenQuality} value={imageGenQuality}
onChange={(e: any) => setImageGEnQuality(e.target.value)} onChange={(e: any) => setImageGEnQuality(e.target.value)}
> >
@@ -168,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
class="select select-sm select-bordered"
value={imageGenResponseFormat} value={imageGenResponseFormat}
onChange={(e: any) => setImageGenResponseFormat(e.target.value)} onChange={(e: any) => setImageGenResponseFormat(e.target.value)}
> >
@@ -178,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
class="select select-sm select-bordered"
value={imageGenSize} value={imageGenSize}
onChange={(e: any) => setImageGenSize(e.target.value)} onChange={(e: any) => setImageGenSize(e.target.value)}
> >
@@ -191,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
class="select select-sm select-bordered"
value={imageGenStyle} value={imageGenStyle}
onChange={(e: any) => setImageGenStyle(e.target.value)} onChange={(e: any) => setImageGenStyle(e.target.value)}
> >
@@ -201,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

@@ -228,7 +228,9 @@ export function App() {
if (oldVersion < 11) { if (oldVersion < 11) {
if (oldVersion < 11 && oldVersion >= 1) { if (oldVersion < 11 && oldVersion >= 1) {
alert("Start upgrading storage, just a sec... (Click OK to continue)"); alert(
"Start upgrading storage, just a sec... (Click OK to continue)"
);
} }
if ( if (
transaction transaction
@@ -401,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="w-full 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 class="pt-2">
{(allChatStoreIndexes as number[]) {(allChatStoreIndexes as number[])
.slice() .slice()
.reverse() .reverse()
@@ -419,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);
@@ -433,52 +435,59 @@ 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} db={db}

View File

@@ -14,6 +14,7 @@ import {
TemplateAPI, TemplateAPI,
TemplateTools, TemplateTools,
addTotalCost, addTotalCost,
getTotalCost,
} from "./app"; } from "./app";
import ChatGPT, { import ChatGPT, {
calculate_token_length, calculate_token_length,
@@ -34,6 +35,16 @@ import { ListToolsTempaltes } from "./listToolsTemplates";
import { autoHeight } from "./textarea"; import { autoHeight } from "./textarea";
import Search from "./search"; import Search from "./search";
import { IDBPDatabase } from "idb"; import { IDBPDatabase } from "idb";
import {
MagnifyingGlassIcon,
CubeIcon,
BanknotesIcon,
DocumentTextIcon,
ChatBubbleLeftEllipsisIcon,
ScissorsIcon,
SwatchIcon,
SparklesIcon,
} from "@heroicons/react/24/outline";
export interface TemplateChatStore extends ChatStore { export interface TemplateChatStore extends ChatStore {
name: string; name: string;
@@ -434,7 +445,7 @@ export default function ChatBOX(props: {
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">
{showSettings && ( {showSettings && (
<Settings <Settings
chatStore={chatStore} chatStore={chatStore}
@@ -463,12 +474,124 @@ export default function ChatBOX(props: {
setShow={setShowSearch} setShow={setShowSearch}
/> />
)} )}
<div <div class="navbar bg-base-100">
className="relative cursor-pointer rounded bg-cyan-300 dark:text-white p-1 dark:bg-cyan-800" <div class="navbar-start">
<div class="dropdown">
<div tabindex={0} role="button" class="btn btn-ghost btn-circle">
<svg
xmlns="http://www.w3.org/2000/svg"
class="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}
class="menu menu-sm dropdown-content bg-base-100 rounded-box z-[1] mt-3 w-52 p-2 shadow"
>
<li>
<p>
<ChatBubbleLeftEllipsisIcon class="h-4 w-4" />
Tokens: {chatStore.totalTokens}/{chatStore.maxTokens}
</p>
</li>
<li>
<p>
<ScissorsIcon class="h-4 w-4" />
Cut:
{chatStore.postBeginIndex}/
{chatStore.history.filter(({ hide }) => !hide).length}
</p>
</li>
<li>
<p>
<BanknotesIcon class="h-4 w-4" />
Cost: ${chatStore.cost.toFixed(4)}
</p>
</li>
</ul>
</div>
</div>
<div class="navbar-center">
<div class="indicator">
<span class="indicator-item badge badge-primary">
{chatStore.streamMode ? Tr("STREAM") : Tr("FETCH")}
</span>
<a class="btn btn-ghost text-xl">
<SparklesIcon class="h-4 w-4" />
{chatStore.model}
</a>
</div>
</div>
<div class="navbar-end">
<button
class="btn btn-ghost btn-circle"
onClick={(event) => {
// stop propagation to parent
event.stopPropagation();
setShowSearch(true);
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="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
class="btn btn-ghost btn-circle"
onClick={() => setShowSettings(true)}
>
<div class="indicator">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="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 class="badge badge-xs badge-primary indicator-item"></span>
</div>
</button>
</div>
</div>
{/* <div
className="relative cursor-pointer rounded p-2"
onClick={() => setShowSettings(true)} onClick={() => setShowSettings(true)}
> >
<button <button
className="absolute right-1 bg-gray-300 rounded p-1 m-1" className="absolute right-1 rounded p-1 m-1"
onClick={(event) => { onClick={(event) => {
// stop propagation to parent // stop propagation to parent
event.stopPropagation(); event.stopPropagation();
@@ -476,50 +599,53 @@ export default function ChatBOX(props: {
setShowSearch(true); setShowSearch(true);
}} }}
> >
🔍 <MagnifyingGlassIcon class="w-5 h-5" />
</button> </button>
<div> <div class="hidden lg:inline-grid"></div>
<button className="underline"> <div class="lg:hidden">
{chatStore.systemMessageContent.length > 16 <div>
? chatStore.systemMessageContent.slice(0, 16) + ".." <button className="underline">
: chatStore.systemMessageContent} {chatStore.systemMessageContent.length > 16
</button>{" "} ? chatStore.systemMessageContent.slice(0, 16) + ".."
<button className="underline"> : chatStore.systemMessageContent}
{chatStore.streamMode ? Tr("STREAM") : Tr("FETCH")} </button>{" "}
</button>{" "} <button className="underline">
{chatStore.toolsString.trim() && ( {chatStore.streamMode ? Tr("STREAM") : Tr("FETCH")}
<button className="underline">TOOL</button> </button>{" "}
)} {chatStore.toolsString.trim() && (
</div> <button className="underline">TOOL</button>
<div className="text-xs"> )}
<span className="underline">{chatStore.model}</span>{" "} </div>
<span> <div className="text-xs">
Tokens:{" "} <span class="underline">{chatStore.model}</span>{" "}
<span className="underline"> <span>
{chatStore.totalTokens}/{chatStore.maxTokens} Tokens:{" "}
</span> <span class="underline">
</span>{" "} {chatStore.totalTokens}/{chatStore.maxTokens}
<span> </span>
{Tr("Cut")}:{" "}
<span className="underline">
{chatStore.postBeginIndex}/
{chatStore.history.filter(({ hide }) => !hide).length}
</span>{" "} </span>{" "}
</span>{" "} <span>
<span> {Tr("Cut")}:{" "}
{Tr("Cost")}:{" "} <span class="underline">
<span className="underline">${chatStore.cost.toFixed(4)}</span> {chatStore.postBeginIndex}/
</span> {chatStore.history.filter(({ hide }) => !hide).length}
</span>{" "}
</span>{" "}
<span>
{Tr("Cost")}:{" "}
<span className="underline">${chatStore.cost.toFixed(4)}</span>
</span>
</div>
</div> </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>
)} )}
@@ -581,7 +707,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
@@ -596,7 +722,7 @@ export default function ChatBOX(props: {
{Tr("Reset Current")} {Tr("Reset Current")}
</button> </button>
</h2> </h2>
<hr className="my-2" /> <div class="divider"></div>
<div className="flex flex-wrap"> <div className="flex flex-wrap">
{templates.map((t, index) => ( {templates.map((t, index) => (
<div <div
@@ -712,6 +838,13 @@ export default function ChatBOX(props: {
<br /> <br />
</p> </p>
)} )}
<div class="chat chat-start">
<div class="chat-header">Prompt</div>
<div class="chat-bubble chat-bubble-accent">
{chatStore.systemMessageContent}
</div>
</div>
{chatStore.history.map((_, messageIndex) => ( {chatStore.history.map((_, messageIndex) => (
<Message <Message
chatStore={chatStore} chatStore={chatStore}
@@ -721,7 +854,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>
@@ -729,7 +862,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;
@@ -749,7 +882,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();
@@ -852,15 +985,16 @@ export default function ChatBOX(props: {
<input type="checkbox" checked={follow} /> <input type="checkbox" checked={follow} />
</span> </span>
)} )}
<div className="flex justify-between"> <div className="flex justify-between">
<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:line-through disabled:text-white disabled:bg-neutral m-1 p-1"
disabled={showGenerating || !chatStore.apiKey} disabled={showGenerating || !chatStore.apiKey}
onClick={() => { onClick={() => {
setShowAddImage(!showAddImage); setShowAddImage(!showAddImage);
}} }}
> >
Img Image
</button> </button>
{showAddImage && ( {showAddImage && (
<AddImage <AddImage
@@ -891,11 +1025,11 @@ 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"
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);
@@ -909,10 +1043,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}
@@ -1027,7 +1159,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({
@@ -1051,7 +1183,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);
@@ -1062,7 +1194,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);
@@ -1110,7 +1242,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

@@ -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
class="link"
onClick={() => { onClick={() => {
const name = prompt(`Give **${label}** template a name`); const name = prompt(`Give **${label}** template a name`);
if (!name) { if (!name) {
@@ -55,9 +55,10 @@ export function ListAPIs({
setTmps(structuredClone(tmps)); setTmps(structuredClone(tmps));
}} }}
> >
🖋 Edit
</button> </button>
<button <button
class="link"
onClick={() => { onClick={() => {
if ( if (
!confirm( !confirm(
@@ -70,7 +71,7 @@ export function ListAPIs({
setTmps(structuredClone(tmps)); setTmps(structuredClone(tmps));
}} }}
> >
Delete
</button> </button>
</span> </span>
</div> </div>

View File

@@ -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
class="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
class="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

@@ -3,25 +3,28 @@ import { App } from "./app";
import { useState, useEffect } from "preact/hooks"; import { useState, useEffect } from "preact/hooks";
import { Tr, langCodeContext, LANG_OPTIONS } from "./translate"; import { Tr, langCodeContext, LANG_OPTIONS } from "./translate";
import { themeChange } from "theme-change";
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

@@ -10,6 +10,7 @@ import { MessageToolCall } from "./messageToolCall";
import { MessageToolResp } from "./messageToolResp"; import { MessageToolResp } from "./messageToolResp";
import { EditMessage } from "./editMessage"; import { EditMessage } from "./editMessage";
import logprobToColor from "./logprob"; import logprobToColor from "./logprob";
import { XMarkIcon } from "@heroicons/react/24/outline";
export const isVailedJSON = (str: string): boolean => { export const isVailedJSON = (str: string): boolean => {
try { try {
@@ -51,17 +52,26 @@ export default function Message(props: Props) {
setChatStore({ ...chatStore }); setChatStore({ ...chatStore });
}} }}
> >
🗑 Delete
</button> </button>
); );
const CopiedHint = () => ( const CopiedHint = () => (
<span <div role="alert" class="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!")} class="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 +88,7 @@ export default function Message(props: Props) {
copyToClipboard(textToCopy); copyToClipboard(textToCopy);
}} }}
> >
📋 Copy
</button> </button>
</> </>
); );
@@ -92,8 +102,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>
@@ -105,51 +115,64 @@ export default function Message(props: Props) {
> >
<div> <div>
<div <div
className={`w-fit p-2 rounded my-2 ${ className={`chat min-w-16 w-fit 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 ${
) : 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 class="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 +180,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 +193,11 @@ export default function Message(props: Props) {
)} )}
{showCopiedHint && <CopiedHint />} {showCopiedHint && <CopiedHint />}
{chatStore.develop_mode && ( {chatStore.develop_mode && (
<div> <div class="flex items-center gap-1">
<span className="dark:text-white">token</span> <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();
@@ -199,7 +222,7 @@ export default function Message(props: Props) {
setChatStore({ ...chatStore }); setChatStore({ ...chatStore });
}} }}
> >
<XMarkIcon class="w-4 h-4" />
</button> </button>
<span <span
onClick={(event: any) => { onClick={(event: any) => {
@@ -207,17 +230,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

@@ -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,7 +22,7 @@ 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");

View File

@@ -6,7 +6,5 @@ interface Props {
} }
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

@@ -30,23 +30,23 @@ export default function Search(props: {
onClick={(event: any) => { onClick={(event: any) => {
event.stopPropagation(); event.stopPropagation();
}} }}
className="m-2 p-2 bg-white rounded-lg h-fit w-2/3 z-20" className="m-2 p-2 bg-base-300 rounded-lg h-fit w-2/3 z-20"
> >
<div className="flex justify-between"> <div className="flex justify-between">
<span className="m-1 p-1 font-bold">Search</span> <span className="m-1 p-1 font-bold">Search</span>
<button <button
className="m-1 p-1 bg-cyan-400 rounded" className="m-1 p-1 btn btn-sm btn-secondary"
onClick={() => props.setShow(false)} onClick={() => props.setShow(false)}
> >
Close Close
</button> </button>
</div> </div>
<hr />
<div> <div>
<input <input
autoFocus autoFocus
className="m-1 p-1 w-full border" className="input input-bordered w-full w-full border"
type="text" type="text"
placeholder="Type Something..."
onInput={async (event: any) => { onInput={async (event: any) => {
const query = event.target.value.trim().toLowerCase(); const query = event.target.value.trim().toLowerCase();
if (!query) { if (!query) {
@@ -119,44 +119,14 @@ export default function Search(props: {
/> />
</div> </div>
{searching && <div>Searching {searchingNow}%...</div>} {searching && <div>Searching {searchingNow}%...</div>}
{searchResult.length > 0 && (
<div className="flex justify-between">
<button
className="m-1 p-1 rounded bg-green-300 disabled:bg-slate-400"
disabled={pageIndex === 0}
onClick={() => {
if (pageIndex === 0) {
return;
}
setPageIndex(pageIndex - 1);
}}
>
Last page
</button>
<div className="m-1 p-1">
Page: {pageIndex + 1} / {Math.floor(searchResult.length / 10) + 1}
</div>
<button
className="m-1 p-1 rounded bg-green-300 disabled:bg-slate-400"
disabled={pageIndex === Math.floor(searchResult.length / 10)}
onClick={() => {
if (pageIndex === Math.floor(searchResult.length / 10)) {
return;
}
setPageIndex(pageIndex + 1);
}}
>
Next page
</button>
</div>
)}
<div> <div>
{searchResult {searchResult
.slice(pageIndex * 10, (pageIndex + 1) * 10) .slice(pageIndex * 10, (pageIndex + 1) * 10)
.map((result: ChatStoreSearchResult) => { .map((result: ChatStoreSearchResult) => {
return ( return (
<div <div
className="flex justify-start p-1 m-1 rounded border bg-slate-300 cursor-pointer" className="flex justify-start p-1 m-1 rounded border bg-base-200 cursor-pointer"
key={result.key} key={result.key}
onClick={() => { onClick={() => {
props.setSelectedChatIndex(parseInt(result.key.toString())); props.setSelectedChatIndex(parseInt(result.key.toString()));
@@ -169,6 +139,40 @@ export default function Search(props: {
); );
})} })}
</div> </div>
{searchResult.length > 0 && (
<div className="flex justify-center my-2">
<div class="join">
<button
class="join-item btn btn-sm"
disabled={pageIndex === 0}
onClick={() => {
if (pageIndex === 0) {
return;
}
setPageIndex(pageIndex - 1);
}}
>
«
</button>
<button class="join-item btn btn-sm">
Page {pageIndex + 1} /{" "}
{Math.floor(searchResult.length / 10) + 1}
</button>
<button
class="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>
</div> </div>
); );

View File

@@ -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

@@ -1,6 +1,7 @@
import { useMemo, useState } from "preact/hooks"; import { useMemo, useState } from "preact/hooks";
import { ChatStore, ChatStoreMessage, addTotalCost } from "./app"; import { ChatStore, ChatStoreMessage, addTotalCost } from "./app";
import { Message, getMessageText } from "./chatgpt"; import { Message, getMessageText } from "./chatgpt";
import { SpeakerWaveIcon } from "@heroicons/react/24/outline";
interface TTSProps { interface TTSProps {
chatStore: ChatStore; chatStore: ChatStore;
@@ -78,7 +79,11 @@ export default function TTSButton(props: TTSProps) {
}); });
}} }}
> >
{generating ? "🤔" : "🔈"} {generating ? (
<span class="loading loading-dots loading-xs"></span>
) : (
<SpeakerWaveIcon class="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')],
}; };

511
yarn.lock

File diff suppressed because it is too large Load Diff