reconstitution ui with daisyui
This commit is contained in:
@@ -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
2268
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
31
src/app.tsx
31
src/app.tsx
@@ -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,12 +435,18 @@ export function App() {
|
|||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<button
|
<button
|
||||||
className="rounded bg-rose-400 p-1 my-1 w-full"
|
className="btn btn-warning btn-sm p-1 my-1 w-full"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (!confirm("Are you sure you want to delete this chat history?"))
|
if (
|
||||||
|
!confirm("Are you sure you want to delete this chat history?")
|
||||||
|
)
|
||||||
return;
|
return;
|
||||||
console.log("remove item", `${STORAGE_NAME}-${selectedChatIndex}`);
|
console.log(
|
||||||
|
"remove item",
|
||||||
|
`${STORAGE_NAME}-${selectedChatIndex}`
|
||||||
|
);
|
||||||
(await db).delete(STORAGE_NAME, selectedChatIndex);
|
(await db).delete(STORAGE_NAME, selectedChatIndex);
|
||||||
const newAllChatStoreIndexes = await (
|
const newAllChatStoreIndexes = await (
|
||||||
await db
|
await db
|
||||||
@@ -461,7 +469,7 @@ export function App() {
|
|||||||
</button>
|
</button>
|
||||||
{chatStore.develop_mode && (
|
{chatStore.develop_mode && (
|
||||||
<button
|
<button
|
||||||
className="rounded bg-rose-800 p-1 my-1 w-full text-white"
|
className="btn btn-sm btn-warning p-1 my-1 w-full"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (
|
if (
|
||||||
!confirm(
|
!confirm(
|
||||||
@@ -480,6 +488,7 @@ export function App() {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<ChatBOX
|
<ChatBOX
|
||||||
db={db}
|
db={db}
|
||||||
chatStore={chatStore}
|
chatStore={chatStore}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ 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 } from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
export interface TemplateChatStore extends ChatStore {
|
export interface TemplateChatStore extends ChatStore {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -434,7 +435,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}
|
||||||
@@ -464,11 +465,11 @@ export default function ChatBOX(props: {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className="relative cursor-pointer rounded bg-cyan-300 dark:text-white p-1 dark:bg-cyan-800"
|
className="relative cursor-pointer rounded p-2 bg-base-200"
|
||||||
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,7 +477,7 @@ export default function ChatBOX(props: {
|
|||||||
setShowSearch(true);
|
setShowSearch(true);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
🔍
|
<MagnifyingGlassIcon class="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
<div>
|
<div>
|
||||||
<button className="underline">
|
<button className="underline">
|
||||||
@@ -514,12 +515,12 @@ export default function ChatBOX(props: {
|
|||||||
</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 +582,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 +597,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
|
||||||
@@ -729,7 +730,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-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 +750,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-warning disabled:line-through disabled:bg-neural m-2 p-2"
|
||||||
disabled={showGenerating}
|
disabled={showGenerating}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await complete();
|
await complete();
|
||||||
@@ -852,15 +853,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 +893,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-xs grow m-1 p-1 w-0"
|
||||||
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 +911,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 +1027,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 +1051,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 +1062,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);
|
||||||
|
|||||||
19
src/main.tsx
19
src/main.tsx
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
482
src/settings.tsx
482
src/settings.tsx
@@ -16,6 +16,14 @@ import { SetAPIsTemplate } from "./setAPIsTemplate";
|
|||||||
import { autoHeight } from "./textarea";
|
import { autoHeight } from "./textarea";
|
||||||
import getDefaultParams from "./getDefaultParam";
|
import getDefaultParams from "./getDefaultParam";
|
||||||
|
|
||||||
|
import {
|
||||||
|
InformationCircleIcon,
|
||||||
|
CheckIcon,
|
||||||
|
NoSymbolIcon,
|
||||||
|
} from "@heroicons/react/24/outline";
|
||||||
|
|
||||||
|
import { themeChange } from "theme-change";
|
||||||
|
|
||||||
const TTS_VOICES: string[] = [
|
const TTS_VOICES: string[] = [
|
||||||
"alloy",
|
"alloy",
|
||||||
"echo",
|
"echo",
|
||||||
@@ -29,14 +37,6 @@ const TTS_FORMAT: string[] = ["mp3", "opus", "aac", "flac"];
|
|||||||
const Help = (props: { children: any; help: string }) => {
|
const Help = (props: { children: any; help: string }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<button
|
|
||||||
className="absolute"
|
|
||||||
onClick={() => {
|
|
||||||
alert(props.help);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
❓
|
|
||||||
</button>
|
|
||||||
<p className="flex justify-between">{props.children}</p>
|
<p className="flex justify-between">{props.children}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -103,12 +103,25 @@ const LongInput = (props: {
|
|||||||
chatStore: ChatStore;
|
chatStore: ChatStore;
|
||||||
setChatStore: (cs: ChatStore) => void;
|
setChatStore: (cs: ChatStore) => void;
|
||||||
field: "systemMessageContent" | "toolsString";
|
field: "systemMessageContent" | "toolsString";
|
||||||
|
label: string;
|
||||||
help: string;
|
help: string;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<Help help={props.help}>
|
<label class="form-control">
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text">{props.label}</span>
|
||||||
|
<span class="label-text-alt">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
alert(props.help);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<InformationCircleIcon className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
className="m-2 p-2 border rounded focus w-full"
|
className="textarea textarea-bordered h-24 w-full"
|
||||||
value={props.chatStore[props.field]}
|
value={props.chatStore[props.field]}
|
||||||
onChange={(event: any) => {
|
onChange={(event: any) => {
|
||||||
props.chatStore[props.field] = event.target.value;
|
props.chatStore[props.field] = event.target.value;
|
||||||
@@ -119,7 +132,7 @@ const LongInput = (props: {
|
|||||||
autoHeight(event.target);
|
autoHeight(event.target);
|
||||||
}}
|
}}
|
||||||
></textarea>
|
></textarea>
|
||||||
</Help>
|
</label>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -142,7 +155,6 @@ const Input = (props: {
|
|||||||
<Help help={props.help}>
|
<Help help={props.help}>
|
||||||
<label className="m-2 p-2">{props.field}</label>
|
<label className="m-2 p-2">{props.field}</label>
|
||||||
<button
|
<button
|
||||||
className="p-2"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setHideInput(!hideInput);
|
setHideInput(!hideInput);
|
||||||
console.log("clicked", hideInput);
|
console.log("clicked", hideInput);
|
||||||
@@ -343,6 +355,7 @@ export default (props: {
|
|||||||
const { langCode, setLangCode } = useContext(langCodeContext);
|
const { langCode, setLangCode } = useContext(langCodeContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
themeChange(false);
|
||||||
const handleKeyPress = (event: any) => {
|
const handleKeyPress = (event: any) => {
|
||||||
if (event.keyCode === 27) {
|
if (event.keyCode === 27) {
|
||||||
// keyCode for ESC key is 27
|
// keyCode for ESC key is 27
|
||||||
@@ -360,17 +373,133 @@ export default (props: {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={() => props.setShow(false)}
|
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"
|
className="left-0 top-0 overflow-scroll flex justify-center absolute mt-6 w-screen h-full z-10"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
onClick={(event: any) => {
|
onClick={(event: any) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className="m-2 p-2 bg-white rounded-lg h-fit lg:w-2/3 z-20"
|
className="px-6 pt-2 pb-4 rounded-lg h-fit lg:w-2/3 z-20 shadow-2xl bg-base-200"
|
||||||
>
|
>
|
||||||
<h3 className="text-xl text-center flex justify-between">
|
<div className="flex justify-between items-center">
|
||||||
|
<h3 className="text-xl">
|
||||||
<span>{Tr("Settings")}</span>
|
<span>{Tr("Settings")}</span>
|
||||||
<select>
|
</h3>
|
||||||
|
<button
|
||||||
|
className="btn"
|
||||||
|
onClick={() => {
|
||||||
|
props.setShow(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Tr("Close")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="pt-2" />
|
||||||
|
<div role="tablist" class="tabs tabs-bordered pt-2 w-full">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="setting_tab"
|
||||||
|
role="tab"
|
||||||
|
class="tab"
|
||||||
|
aria-label="Session"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
role="tabpanel"
|
||||||
|
class="tab-content bg-base-100 border-base-300 rounded-box p-6"
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
{Tr("Total cost in this session")} $
|
||||||
|
{props.chatStore.cost.toFixed(4)}
|
||||||
|
</p>
|
||||||
|
<LongInput
|
||||||
|
label="System Prompt"
|
||||||
|
field="systemMessageContent"
|
||||||
|
help="系统消息,用于指示ChatGPT的角色和一些前置条件,例如“你是一个有帮助的人工智能助理”,或者“你是一个专业英语翻译,把我的话全部翻译成英语”,详情参考 OPEAN AI API 文档"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<LongInput
|
||||||
|
label="Tools String"
|
||||||
|
field="toolsString"
|
||||||
|
help="function call tools, should be valid json format in list"
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
<span className="pt-1">
|
||||||
|
JSON Check:{" "}
|
||||||
|
{isVailedJSON(props.chatStore.toolsString) ? (
|
||||||
|
<CheckIcon className="inline w-4 h-4" />
|
||||||
|
) : (
|
||||||
|
<NoSymbolIcon className="inline w-4 h-4" />
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<div className="box">
|
||||||
|
<div className="flex justify-evenly flex-wrap">
|
||||||
|
{props.chatStore.toolsString.trim() && (
|
||||||
|
<button
|
||||||
|
className="btn"
|
||||||
|
onClick={() => {
|
||||||
|
const name = prompt(
|
||||||
|
`Give this **Tools** template a name:`
|
||||||
|
);
|
||||||
|
if (!name) {
|
||||||
|
alert("No template name specified");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newToolsTmp: TemplateTools = {
|
||||||
|
name,
|
||||||
|
toolsString: props.chatStore.toolsString,
|
||||||
|
};
|
||||||
|
props.templateTools.push(newToolsTmp);
|
||||||
|
props.setTemplateTools([...props.templateTools]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Tr(`Save Tools`)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="setting_tab"
|
||||||
|
role="tab"
|
||||||
|
class="tab"
|
||||||
|
aria-label="System"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
role="tabpanel"
|
||||||
|
class="tab-content bg-base-100 border-base-300 rounded-box p-6"
|
||||||
|
>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<p>
|
||||||
|
{Tr("Accumulated cost in all sessions")} ${totalCost.toFixed(4)}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
clearTotalCost();
|
||||||
|
setTotalCost(getTotalCost());
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Tr("Reset")}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<label class="form-control w-full max-w-xs">
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text">Theme Switch</span>
|
||||||
|
</div>
|
||||||
|
<select data-choose-theme class="select select-bordered">
|
||||||
|
<option value="light">Light</option>
|
||||||
|
<option value="dark">Dark</option>
|
||||||
|
<option value="cyberpunk">Cyberpunk</option>
|
||||||
|
<option value="black">Black</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="form-control w-full max-w-xs">
|
||||||
|
<div class="label">
|
||||||
|
<span class="label-text">Language</span>
|
||||||
|
</div>
|
||||||
|
<select class="select select-bordered">
|
||||||
{Object.keys(LANG_OPTIONS).map((opt) => (
|
{Object.keys(LANG_OPTIONS).map((opt) => (
|
||||||
<option
|
<option
|
||||||
value={opt}
|
value={opt}
|
||||||
@@ -384,11 +513,10 @@ export default (props: {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</h3>
|
</label>
|
||||||
<hr />
|
<div class="join pt-2">
|
||||||
<div className="flex justify-between">
|
|
||||||
<button
|
<button
|
||||||
className="p-2 m-2 rounded bg-purple-600 text-white"
|
class="btn join-item"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(link);
|
navigator.clipboard.writeText(link);
|
||||||
alert(tr(`Copied link:`, langCode) + `${link}`);
|
alert(tr(`Copied link:`, langCode) + `${link}`);
|
||||||
@@ -397,9 +525,11 @@ export default (props: {
|
|||||||
{Tr("Copy Setting Link")}
|
{Tr("Copy Setting Link")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="p-2 m-2 rounded bg-rose-600 text-white"
|
class="btn join-item"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!confirm(tr("Are you sure to clear all history?", langCode)))
|
if (
|
||||||
|
!confirm(tr("Are you sure to clear all history?", langCode))
|
||||||
|
)
|
||||||
return;
|
return;
|
||||||
props.chatStore.history = props.chatStore.history.filter(
|
props.chatStore.history = props.chatStore.history.filter(
|
||||||
(msg) => msg.example && !msg.hide
|
(msg) => msg.example && !msg.hide
|
||||||
@@ -411,33 +541,125 @@ export default (props: {
|
|||||||
{Tr("Clear History")}
|
{Tr("Clear History")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="p-2 m-2 rounded bg-cyan-600 text-white"
|
class="btn join-item"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.setShow(false);
|
let dataStr =
|
||||||
|
"data:text/json;charset=utf-8," +
|
||||||
|
encodeURIComponent(
|
||||||
|
JSON.stringify(props.chatStore, null, "\t")
|
||||||
|
);
|
||||||
|
let downloadAnchorNode = document.createElement("a");
|
||||||
|
downloadAnchorNode.setAttribute("href", dataStr);
|
||||||
|
downloadAnchorNode.setAttribute(
|
||||||
|
"download",
|
||||||
|
`chatgpt-api-web-${props.selectedChatStoreIndex}.json`
|
||||||
|
);
|
||||||
|
document.body.appendChild(downloadAnchorNode); // required for firefox
|
||||||
|
downloadAnchorNode.click();
|
||||||
|
downloadAnchorNode.remove();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Tr("Close")}
|
{Tr("Export")}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn join-item"
|
||||||
|
onClick={() => {
|
||||||
|
const name = prompt(
|
||||||
|
tr("Give this template a name:", langCode)
|
||||||
|
);
|
||||||
|
if (!name) {
|
||||||
|
alert(tr("No template name specified", langCode));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const tmp: ChatStore = structuredClone(props.chatStore);
|
||||||
|
tmp.history = tmp.history.filter((h) => h.example);
|
||||||
|
// clear api because it is stored in the API template
|
||||||
|
tmp.apiEndpoint = "";
|
||||||
|
tmp.apiKey = "";
|
||||||
|
tmp.whisper_api = "";
|
||||||
|
tmp.whisper_key = "";
|
||||||
|
tmp.tts_api = "";
|
||||||
|
tmp.tts_key = "";
|
||||||
|
tmp.image_gen_api = "";
|
||||||
|
tmp.image_gen_key = "";
|
||||||
|
// @ts-ignore
|
||||||
|
tmp.name = name;
|
||||||
|
props.templates.push(tmp as TemplateChatStore);
|
||||||
|
props.setTemplates([...props.templates]);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Tr("As template")}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn join-item"
|
||||||
|
onClick={() => {
|
||||||
|
if (
|
||||||
|
!confirm(
|
||||||
|
tr(
|
||||||
|
"This will OVERWRITE the current chat history! Continue?",
|
||||||
|
langCode
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
console.log("importFileRef", importFileRef);
|
||||||
|
importFileRef.current.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Import
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
className="hidden"
|
||||||
|
ref={importFileRef}
|
||||||
|
type="file"
|
||||||
|
onChange={() => {
|
||||||
|
const file = importFileRef.current.files[0];
|
||||||
|
console.log("file to import", file);
|
||||||
|
if (!file || file.type !== "application/json") {
|
||||||
|
alert(tr("Please select a json file", langCode));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
console.log("import content", reader.result);
|
||||||
|
if (!reader) {
|
||||||
|
alert(tr("Empty file", langCode));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const newChatStore: ChatStore = JSON.parse(
|
||||||
|
reader.result as string
|
||||||
|
);
|
||||||
|
if (!newChatStore.chatgpt_api_web_version) {
|
||||||
|
throw tr(
|
||||||
|
"This is not an exported chatgpt-api-web chatstore file. The key 'chatgpt_api_web_version' is missing!",
|
||||||
|
langCode
|
||||||
|
);
|
||||||
|
}
|
||||||
|
props.setChatStore({ ...newChatStore });
|
||||||
|
} catch (e) {
|
||||||
|
alert(
|
||||||
|
tr(`Import error on parsing json:`, langCode) + `${e}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsText(file);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<p className="m-2 p-2">
|
</div>
|
||||||
{Tr("Total cost in this session")} ${props.chatStore.cost.toFixed(4)}
|
|
||||||
</p>
|
<input
|
||||||
<hr />
|
type="radio"
|
||||||
<div className="box">
|
name="setting_tab"
|
||||||
<LongInput
|
role="tab"
|
||||||
field="systemMessageContent"
|
class="tab"
|
||||||
help="系统消息,用于指示ChatGPT的角色和一些前置条件,例如“你是一个有帮助的人工智能助理”,或者“你是一个专业英语翻译,把我的话全部翻译成英语”,详情参考 OPEAN AI API 文档"
|
aria-label="Chat"
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
<span>
|
|
||||||
Valied JSON:{" "}
|
|
||||||
{isVailedJSON(props.chatStore.toolsString) ? "🆗" : "❌"}
|
|
||||||
</span>
|
|
||||||
<LongInput
|
|
||||||
field="toolsString"
|
|
||||||
help="function call tools, should be valied json format in list"
|
|
||||||
{...props}
|
|
||||||
/>
|
/>
|
||||||
|
<div
|
||||||
|
role="tabpanel"
|
||||||
|
class="tab-content bg-base-100 border-base-300 rounded-box p-6"
|
||||||
|
>
|
||||||
<div className="relative border-slate-300 border rounded">
|
<div className="relative border-slate-300 border rounded">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<strong className="p-1 m-1">Chat API</strong>
|
<strong className="p-1 m-1">Chat API</strong>
|
||||||
@@ -533,7 +755,19 @@ export default (props: {
|
|||||||
readOnly={false}
|
readOnly={false}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="setting_tab"
|
||||||
|
role="tab"
|
||||||
|
class="tab"
|
||||||
|
aria-label="TTS"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
role="tabpanel"
|
||||||
|
class="tab-content bg-base-100 border-base-300 rounded-box p-6"
|
||||||
|
>
|
||||||
<div className="relative border-slate-300 border rounded">
|
<div className="relative border-slate-300 border rounded">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<strong className="p-1 m-1">Whisper API</strong>
|
<strong className="p-1 m-1">Whisper API</strong>
|
||||||
@@ -616,7 +850,18 @@ export default (props: {
|
|||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</Help>
|
</Help>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="setting_tab"
|
||||||
|
role="tab"
|
||||||
|
class="tab"
|
||||||
|
aria-label="Image Gen"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
role="tabpanel"
|
||||||
|
class="tab-content bg-base-100 border-base-300 rounded-box p-6"
|
||||||
|
>
|
||||||
<div className="relative border-slate-300 border rounded">
|
<div className="relative border-slate-300 border rounded">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<strong className="p-1 m-1">Image Gen API</strong>
|
<strong className="p-1 m-1">Image Gen API</strong>
|
||||||
@@ -640,155 +885,15 @@ export default (props: {
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex justify-between">
|
|
||||||
<p className="m-2 p-2">
|
|
||||||
{Tr("Accumulated cost in all sessions")} ${totalCost.toFixed(4)}
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
className="p-2 m-2 rounded bg-emerald-500"
|
|
||||||
onClick={() => {
|
|
||||||
clearTotalCost();
|
|
||||||
setTotalCost(getTotalCost());
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Tr("Reset")}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-evenly flex-wrap">
|
|
||||||
{props.chatStore.toolsString.trim() && (
|
|
||||||
<button
|
|
||||||
className="p-2 m-2 rounded bg-blue-300"
|
|
||||||
onClick={() => {
|
|
||||||
const name = prompt(`Give this **Tools** template a name:`);
|
|
||||||
if (!name) {
|
|
||||||
alert("No template name specified");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const newToolsTmp: TemplateTools = {
|
|
||||||
name,
|
|
||||||
toolsString: props.chatStore.toolsString,
|
|
||||||
};
|
|
||||||
props.templateTools.push(newToolsTmp);
|
|
||||||
props.setTemplateTools([...props.templateTools]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Tr(`Save Tools`)}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<p className="flex justify-evenly">
|
<div className="pt-4 pb-2">
|
||||||
<button
|
<p className="text-center">
|
||||||
className="p-2 m-2 rounded bg-amber-500"
|
|
||||||
onClick={() => {
|
|
||||||
let dataStr =
|
|
||||||
"data:text/json;charset=utf-8," +
|
|
||||||
encodeURIComponent(
|
|
||||||
JSON.stringify(props.chatStore, null, "\t")
|
|
||||||
);
|
|
||||||
let downloadAnchorNode = document.createElement("a");
|
|
||||||
downloadAnchorNode.setAttribute("href", dataStr);
|
|
||||||
downloadAnchorNode.setAttribute(
|
|
||||||
"download",
|
|
||||||
`chatgpt-api-web-${props.selectedChatStoreIndex}.json`
|
|
||||||
);
|
|
||||||
document.body.appendChild(downloadAnchorNode); // required for firefox
|
|
||||||
downloadAnchorNode.click();
|
|
||||||
downloadAnchorNode.remove();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Tr("Export")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="p-2 m-2 rounded bg-amber-500"
|
|
||||||
onClick={() => {
|
|
||||||
const name = prompt(tr("Give this template a name:", langCode));
|
|
||||||
if (!name) {
|
|
||||||
alert(tr("No template name specified", langCode));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const tmp: ChatStore = structuredClone(props.chatStore);
|
|
||||||
tmp.history = tmp.history.filter((h) => h.example);
|
|
||||||
// clear api because it is stored in the API template
|
|
||||||
tmp.apiEndpoint = "";
|
|
||||||
tmp.apiKey = "";
|
|
||||||
tmp.whisper_api = "";
|
|
||||||
tmp.whisper_key = "";
|
|
||||||
tmp.tts_api = "";
|
|
||||||
tmp.tts_key = "";
|
|
||||||
tmp.image_gen_api = "";
|
|
||||||
tmp.image_gen_key = "";
|
|
||||||
// @ts-ignore
|
|
||||||
tmp.name = name;
|
|
||||||
props.templates.push(tmp as TemplateChatStore);
|
|
||||||
props.setTemplates([...props.templates]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{Tr("As template")}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
className="p-2 m-2 rounded bg-amber-500"
|
|
||||||
onClick={() => {
|
|
||||||
if (
|
|
||||||
!confirm(
|
|
||||||
tr(
|
|
||||||
"This will OVERWRITE the current chat history! Continue?",
|
|
||||||
langCode
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return;
|
|
||||||
console.log("importFileRef", importFileRef);
|
|
||||||
importFileRef.current.click();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Import
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
className="hidden"
|
|
||||||
ref={importFileRef}
|
|
||||||
type="file"
|
|
||||||
onChange={() => {
|
|
||||||
const file = importFileRef.current.files[0];
|
|
||||||
console.log("file to import", file);
|
|
||||||
if (!file || file.type !== "application/json") {
|
|
||||||
alert(tr("Please select a json file", langCode));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => {
|
|
||||||
console.log("import content", reader.result);
|
|
||||||
if (!reader) {
|
|
||||||
alert(tr("Empty file", langCode));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const newChatStore: ChatStore = JSON.parse(
|
|
||||||
reader.result as string
|
|
||||||
);
|
|
||||||
if (!newChatStore.chatgpt_api_web_version) {
|
|
||||||
throw tr(
|
|
||||||
"This is not an exported chatgpt-api-web chatstore file. The key 'chatgpt_api_web_version' is missing!",
|
|
||||||
langCode
|
|
||||||
);
|
|
||||||
}
|
|
||||||
props.setChatStore({ ...newChatStore });
|
|
||||||
} catch (e) {
|
|
||||||
alert(
|
|
||||||
tr(`Import error on parsing json:`, langCode) + `${e}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
<p className="text-center m-2 p-2">
|
|
||||||
chatgpt-api-web ChatStore {Tr("Version")}{" "}
|
chatgpt-api-web ChatStore {Tr("Version")}{" "}
|
||||||
{props.chatStore.chatgpt_api_web_version}
|
{props.chatStore.chatgpt_api_web_version}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p className="text-center">
|
||||||
⚠{Tr("Documents and source code are avaliable here")}:{" "}
|
{Tr("Documents and source code are avaliable here")}:{" "}
|
||||||
<a
|
<a
|
||||||
className="underline"
|
className="underline"
|
||||||
href="https://github.com/heimoshuiyu/chatgpt-api-web"
|
href="https://github.com/heimoshuiyu/chatgpt-api-web"
|
||||||
@@ -798,7 +903,6 @@ export default (props: {
|
|||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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')],
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user