Refactor Settings component and integrate shadcn UI elements for improved user experience

This commit is contained in:
ecwu
2024-12-20 16:06:01 +08:00
parent 7ecdae8f1d
commit bac65994b0
6 changed files with 1095 additions and 843 deletions

View File

@@ -0,0 +1,13 @@
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";

View File

@@ -4,6 +4,7 @@ import { useState, useEffect } from "preact/hooks";
import { App } from "@/pages/App"; import { App } from "@/pages/App";
import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate"; import { Tr, langCodeContext, LANG_OPTIONS } from "@/translate";
import { SidebarProvider } from "@/components/ui/sidebar"; import { SidebarProvider } from "@/components/ui/sidebar";
import { Toaster } from "@/components/ui/toaster";
function Base() { function Base() {
const [langCode, _setLangCode] = useState("en-US"); const [langCode, _setLangCode] = useState("en-US");
@@ -49,6 +50,7 @@ function Base() {
<langCodeContext.Provider value={{ langCode, setLangCode }}> <langCodeContext.Provider value={{ langCode, setLangCode }}>
<SidebarProvider> <SidebarProvider>
<App /> <App />
<Toaster />
</SidebarProvider> </SidebarProvider>
</langCodeContext.Provider> </langCodeContext.Provider>
); );

View File

@@ -12,10 +12,42 @@ import { newChatStore } from "@/types/newChatstore";
import { STORAGE_NAME, STORAGE_NAME_SELECTED } from "@/const"; import { STORAGE_NAME, STORAGE_NAME_SELECTED } from "@/const";
import { upgrade } from "@/indexedDB/upgrade"; import { upgrade } from "@/indexedDB/upgrade";
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarRail,
SidebarInset,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { Button } from "@/components/ui/button";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { useToast } from "@/hooks/use-toast";
import { Separator } from "@/components/ui/separator";
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(() => {
@@ -27,6 +59,8 @@ export function App() {
upgrade, upgrade,
}); });
const { toast } = useToast();
const getChatStoreByIndex = async (index: number): Promise<ChatStore> => { const getChatStoreByIndex = async (index: number): Promise<ChatStore> => {
const ret: ChatStore = await (await db).get(STORAGE_NAME, index); const ret: ChatStore = await (await db).get(STORAGE_NAME, index);
if (ret === null || ret === undefined) return newChatStore({}); if (ret === null || ret === undefined) return newChatStore({});
@@ -54,7 +88,7 @@ export function App() {
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)
@@ -69,7 +103,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)
@@ -82,7 +116,7 @@ export function App() {
// update total tokens // update total tokens
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)
@@ -101,7 +135,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) => {
@@ -114,7 +148,6 @@ export function App() {
}; };
const handleDEL = async () => { const handleDEL = async () => {
if (!confirm("Are you sure you want to delete this chat history?")) 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 (await db).getAllKeys(STORAGE_NAME); const newAllChatStoreIndexes = await (await db).getAllKeys(STORAGE_NAME);
@@ -129,6 +162,11 @@ export function App() {
console.log("next is", next); console.log("next is", next);
setSelectedChatIndex(next as number); setSelectedChatIndex(next as number);
setAllChatStoreIndexes(newAllChatStoreIndexes); setAllChatStoreIndexes(newAllChatStoreIndexes);
toast({
title: "Chat history deleted",
description: `Chat history ${selectedChatIndex} has been deleted.`,
});
}; };
const handleCLS = async () => { const handleCLS = async () => {
@@ -176,53 +214,79 @@ export function App() {
}, []); }, []);
return ( return (
<div className="flex text-sm h-full"> <>
<div className="flex flex-col h-full p-2 bg-primary"> <Sidebar>
<div className="grow overflow-scroll"> <SidebarHeader>
<button <Button onClick={handleNewChatStore}>
className="btn btn-sm btn-info p-1 my-1 w-full" <span>{Tr("New")}</span>
onClick={handleNewChatStore} </Button>
> </SidebarHeader>
{Tr("NEW")} <SidebarContent>
</button> <SidebarGroup>
<ul className="pt-2"> <SidebarGroupLabel>Conversation</SidebarGroupLabel>
<SidebarGroupContent>
<SidebarMenu>
{(allChatStoreIndexes as number[]) {(allChatStoreIndexes as number[])
.slice() .slice()
.reverse() .reverse()
.map((i) => { .map((i) => {
// reverse // reverse
return ( return (
<li> <SidebarMenuItem
<button key={i}
className={`w-full my-1 p-1 btn btn-sm ${
i === selectedChatIndex ? "btn-accent" : "btn-secondary"
}`}
onClick={() => setSelectedChatIndex(i)} onClick={() => setSelectedChatIndex(i)}
> >
{i} <SidebarMenuButton
</button> asChild
</li> isActive={i === selectedChatIndex}
>
<span>{i}</span>
</SidebarMenuButton>
</SidebarMenuItem>
); );
})} })}
</ul> </SidebarMenu>
</div> </SidebarGroupContent>
<div> </SidebarGroup>
<button </SidebarContent>
className="btn btn-warning btn-sm p-1 my-1 w-full" <SidebarFooter>
onClick={async () => handleDEL()} <AlertDialog>
> <AlertDialogTrigger asChild>
{Tr("DEL")} <Button variant="destructive">{Tr("DEL")}</Button>
</button> </AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete the
chat history.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleDEL}>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{chatStore.develop_mode && ( {chatStore.develop_mode && (
<button <Button onClick={handleCLS} variant="destructive">
className="btn btn-sm btn-warning p-1 my-1 w-full" <span>{Tr("CLS")}</span>
onClick={async () => handleCLS()} </Button>
>
{Tr("CLS")}
</button>
)} )}
</SidebarFooter>
<SidebarRail />
</Sidebar>
<SidebarInset>
<header className="flex h-16 shrink-0 items-center gap-2 border-b">
<div className="flex items-center gap-2 px-3">
<SidebarTrigger />
<Separator orientation="vertical" className="mr-2 h-4" />
<h1 className="text-lg font-bold">MikuChat</h1>
</div> </div>
</div> </header>
<ChatBOX <ChatBOX
db={db} db={db}
chatStore={chatStore} chatStore={chatStore}
@@ -230,6 +294,7 @@ export function App() {
selectedChatIndex={selectedChatIndex} selectedChatIndex={selectedChatIndex}
setSelectedChatIndex={setSelectedChatIndex} setSelectedChatIndex={setSelectedChatIndex}
/> />
</div> </SidebarInset>
</>
); );
} }

View File

@@ -131,7 +131,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) {
@@ -146,7 +146,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);
@@ -295,7 +295,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")) {
@@ -365,33 +365,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));
@@ -400,35 +400,35 @@ 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);
}; };
@@ -574,7 +574,7 @@ 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>

View File

@@ -1,5 +1,6 @@
import { TemplateAPI } from "@/types/chatstore"; import { TemplateAPI } from "@/types/chatstore";
import { Tr } from "@/translate"; import { Tr } from "@/translate";
import { Button } from "./components/ui/button";
interface Props { interface Props {
tmps: TemplateAPI[]; tmps: TemplateAPI[];
@@ -16,8 +17,10 @@ export function SetAPIsTemplate({
label, label,
}: Props) { }: Props) {
return ( return (
<button <Button
className="btn btn-primary btn-sm mt-3" variant="default"
size="sm"
className="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) {
@@ -34,6 +37,6 @@ export function SetAPIsTemplate({
}} }}
> >
{Tr(`Save ${label}`)} {Tr(`Save ${label}`)}
</button> </Button>
); );
} }

File diff suppressed because it is too large Load Diff