feat: implement dark mode (#27)

* feat: implement dark mode

* feat: update data table dark mode style

* chore: update
This commit is contained in:
boojack
2023-04-12 11:05:25 +08:00
committed by GitHub
parent 2783bca24d
commit c4de3b0d6f
28 changed files with 243 additions and 85 deletions

View File

@ -161,10 +161,12 @@ const ConnectionSidebar = () => {
onClose={() => layoutStore.toggleSidebar(false)} onClose={() => layoutStore.toggleSidebar(false)}
> >
<div className="w-80 h-full overflow-y-hidden flex flex-row justify-start items-start"> <div className="w-80 h-full overflow-y-hidden flex flex-row justify-start items-start">
<div className="w-16 h-full bg-gray-200 pl-2 py-4 pt-6 flex flex-col justify-between items-center"> <div className="w-16 h-full bg-gray-200 dark:bg-zinc-600 pl-2 py-4 pt-6 flex flex-col justify-between items-center">
<div className="w-full flex flex-col justify-start items-start"> <div className="w-full flex flex-col justify-start items-start">
<button <button
className={`w-full h-14 rounded-l-lg p-2 mt-1 group ${currentConnectionCtx === undefined && "bg-gray-100 shadow"}`} className={`w-full h-14 rounded-l-lg p-2 mt-1 group ${
currentConnectionCtx === undefined && "bg-gray-100 dark:bg-zinc-700 shadow"
}`}
onClick={() => connectionStore.setCurrentConnectionCtx(undefined)} onClick={() => connectionStore.setCurrentConnectionCtx(undefined)}
> >
<img src="/chat-logo-bot.webp" className="w-7 h-auto mx-auto" alt="" /> <img src="/chat-logo-bot.webp" className="w-7 h-auto mx-auto" alt="" />
@ -173,7 +175,7 @@ const ConnectionSidebar = () => {
<button <button
key={connection.id} key={connection.id}
className={`relative w-full h-14 rounded-l-lg p-2 mt-2 group ${ className={`relative w-full h-14 rounded-l-lg p-2 mt-2 group ${
currentConnectionCtx?.connection.id === connection.id && "bg-gray-100 shadow" currentConnectionCtx?.connection.id === connection.id && "bg-gray-100 dark:bg-zinc-700 shadow"
}`} }`}
onClick={() => handleConnectionSelect(connection)} onClick={() => handleConnectionSelect(connection)}
> >
@ -184,14 +186,14 @@ const ConnectionSidebar = () => {
handleEditConnection(connection); handleEditConnection(connection);
}} }}
> >
<Icon.FiEdit3 className="w-3.5 h-auto" /> <Icon.FiEdit3 className="w-3.5 h-auto dark:text-gray-300" />
</span> </span>
<EngineIcon engine={connection.engineType} className="w-auto h-full mx-auto" /> <EngineIcon engine={connection.engineType} className="w-auto h-full mx-auto dark:text-gray-300" />
</button> </button>
))} ))}
<Tooltip title={t("connection.new")} side="right"> <Tooltip title={t("connection.new")} side="right">
<button <button
className="w-10 h-10 mt-4 ml-2 p-2 bg-gray-100 rounded-full text-gray-500 cursor-pointer" className="w-10 h-10 mt-4 ml-2 p-2 bg-gray-100 dark:bg-zinc-700 rounded-full text-gray-500 cursor-pointer"
onClick={() => toggleCreateConnectionModal(true)} onClick={() => toggleCreateConnectionModal(true)}
> >
<Icon.AiOutlinePlus className="w-auto h-full mx-auto" /> <Icon.AiOutlinePlus className="w-auto h-full mx-auto" />
@ -202,27 +204,27 @@ const ConnectionSidebar = () => {
<LocaleSwitch /> <LocaleSwitch />
<Tooltip title={t("common.setting")} side="right"> <Tooltip title={t("common.setting")} side="right">
<button <button
className=" w-10 h-10 p-1 rounded-full flex flex-row justify-center items-center hover:bg-gray-100" className=" w-10 h-10 p-1 rounded-full flex flex-row justify-center items-center hover:bg-gray-100 dark:hover:bg-zinc-700"
data-tip={t("common.setting")} data-tip={t("common.setting")}
onClick={() => toggleSettingModal(true)} onClick={() => toggleSettingModal(true)}
> >
<Icon.IoMdSettings className="text-gray-600 w-6 h-auto" /> <Icon.IoMdSettings className="text-gray-600 dark:text-gray-300 w-6 h-auto" />
</button> </button>
</Tooltip> </Tooltip>
</div> </div>
</div> </div>
<div className="relative p-4 pb-0 w-64 h-full overflow-y-auto flex flex-col justify-start items-start bg-gray-100"> <div className="relative p-4 pb-0 w-64 h-full overflow-y-auto flex flex-col justify-start items-start bg-gray-100 dark:bg-zinc-700">
<img className="px-4 shrink-0" src="/chat-logo.webp" alt="" /> <img className="px-4 shrink-0" src="/chat-logo.webp" alt="" />
<div className="w-full grow"> <div className="w-full grow">
{isRequestingDatabase && ( {isRequestingDatabase && (
<div className="w-full h-12 flex flex-row justify-start items-center px-4 sticky top-0 border z-1 mb-4 mt-2 rounded-lg text-sm text-gray-600"> <div className="w-full h-12 flex flex-row justify-start items-center px-4 sticky top-0 border z-1 mb-4 mt-2 rounded-lg text-sm text-gray-600 dark:text-gray-400">
<Icon.BiLoaderAlt className="w-4 h-auto animate-spin mr-1" /> {t("common.loading")} <Icon.BiLoaderAlt className="w-4 h-auto animate-spin mr-1" /> {t("common.loading")}
</div> </div>
)} )}
{databaseList.length > 0 && ( {databaseList.length > 0 && (
<div className="w-full sticky top-0 z-1 my-4"> <div className="w-full sticky top-0 z-1 my-4">
<Select <Select
className="w-full bg-white px-4 py-3" className="w-full px-4 py-3"
value={currentConnectionCtx?.database?.name} value={currentConnectionCtx?.database?.name}
itemList={databaseList.map((database) => { itemList={databaseList.map((database) => {
return { return {
@ -238,8 +240,8 @@ const ConnectionSidebar = () => {
{conversationList.map((conversation) => ( {conversationList.map((conversation) => (
<div <div
key={conversation.id} key={conversation.id}
className={`w-full mt-2 first:mt-4 py-3 pl-4 pr-2 rounded-lg flex flex-row justify-start items-center cursor-pointer border border-transparent group hover:bg-gray-50 ${ className={`w-full mt-2 first:mt-4 py-3 pl-4 pr-2 rounded-lg flex flex-row justify-start items-center cursor-pointer dark:text-gray-300 border border-transparent group hover:bg-gray-50 dark:hover:bg-zinc-800 ${
conversation.id === conversationStore.currentConversation?.id && "!bg-white border-gray-200 font-medium" conversation.id === conversationStore.currentConversation?.id && "bg-white dark:bg-zinc-800 border-gray-200 font-medium"
}`} }`}
onClick={() => handleConversationSelect(conversation)} onClick={() => handleConversationSelect(conversation)}
> >
@ -268,17 +270,17 @@ const ConnectionSidebar = () => {
</div> </div>
))} ))}
<button <button
className="w-full my-4 py-3 px-4 border rounded-lg flex flex-row justify-center items-center text-gray-500 hover:text-gray-700 hover:bg-gray-50" className="w-full my-4 py-3 px-4 border dark:border-zinc-800 rounded-lg flex flex-row justify-center items-center text-gray-500 dark:text-gray-400 hover:text-gray-700 hover:bg-gray-50 dark:hover:bg-zinc-800"
onClick={handleCreateConversation} onClick={handleCreateConversation}
> >
<Icon.AiOutlinePlus className="w-5 h-auto mr-1" /> <Icon.AiOutlinePlus className="w-5 h-auto mr-1" />
{t("conversation.new-chat")} {t("conversation.new-chat")}
</button> </button>
</div> </div>
<div className="sticky bottom-0 w-full flex justify-center bg-gray-100 backdrop-blur bg-opacity-60 pb-6 py-2"> <div className="sticky bottom-0 w-full flex justify-center bg-gray-100 dark:bg-zinc-700 backdrop-blur bg-opacity-60 pb-6 py-2">
<a <a
href="https://discord.gg/z6kakemDjm" href="https://discord.gg/z6kakemDjm"
className="text-indigo-600 text-sm font-medium flex flex-row justify-center items-center hover:underline" className="text-indigo-600 dark:text-indigo-400 text-sm font-medium flex flex-row justify-center items-center hover:underline"
target="_blank" target="_blank"
> >
<Icon.BsDiscord className="w-4 h-auto mr-1" /> <Icon.BsDiscord className="w-4 h-auto mr-1" />

View File

@ -22,11 +22,11 @@ const Header = (props: Props) => {
<div <div
className={`${ className={`${
className || "" className || ""
} w-full flex flex-row justify-between items-center lg:grid lg:grid-cols-3 py-1 border-b z-1 transition-all duration-300`} } w-full flex flex-row justify-between items-center lg:grid lg:grid-cols-3 py-1 border-b dark:border-zinc-700 z-1 transition-all duration-300`}
> >
<div className="ml-2 flex justify-start items-center"> <div className="ml-2 flex justify-start items-center">
<button <button
className="w-8 h-8 p-1 mr-1 block lg:hidden rounded-md cursor-pointer hover:bg-gray-100" className="w-8 h-8 p-1 mr-1 block lg:hidden rounded-md cursor-pointer hover:bg-gray-100 dark:hover:bg-zinc-700"
onClick={() => layoutStore.toggleSidebar()} onClick={() => layoutStore.toggleSidebar()}
> >
<Icon.IoIosMenu className="text-gray-600 w-full h-auto" /> <Icon.IoIosMenu className="text-gray-600 w-full h-auto" />
@ -38,7 +38,7 @@ const Header = (props: Props) => {
<div className="mr-2 sm:mr-3 relative flex flex-row justify-end items-center"> <div className="mr-2 sm:mr-3 relative flex flex-row justify-end items-center">
<a <a
href="https://www.bytebase.com?source=sqlchat" href="https://www.bytebase.com?source=sqlchat"
className="flex flex-row justify-center items-center h-10 px-3 py-1 rounded-md whitespace-nowrap hover:bg-gray-100" className="flex flex-row justify-center items-center h-10 px-3 py-1 rounded-md whitespace-nowrap hover:bg-gray-100 dark:hover:bg-zinc-700"
target="_blank" target="_blank"
> >
<img className="h-5 sm:h-6 w-auto" src="/craft-by-bytebase.webp" alt="" /> <img className="h-5 sm:h-6 w-auto" src="/craft-by-bytebase.webp" alt="" />

View File

@ -73,7 +73,7 @@ const MessageTextarea = (props: Props) => {
}; };
return ( return (
<div className="w-full h-auto flex flex-row justify-between items-end border rounded-lg mb-2 px-2 py-1 relative shadow bg-white"> <div className="w-full h-auto flex flex-row justify-between items-end border dark:border-zinc-700 rounded-lg mb-2 px-2 py-1 relative shadow bg-white dark:bg-zinc-800">
<TextareaAutosize <TextareaAutosize
ref={textareaRef} ref={textareaRef}
className="w-full h-full outline-none border-none bg-transparent leading-6 py-2 px-2 resize-none hide-scrollbar" className="w-full h-full outline-none border-none bg-transparent leading-6 py-2 px-2 resize-none hide-scrollbar"
@ -87,7 +87,7 @@ const MessageTextarea = (props: Props) => {
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
/> />
<button <button
className="w-8 p-1 -translate-y-1 cursor-pointer rounded-md hover:shadow hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-60" className="w-8 p-1 -translate-y-1 cursor-pointer rounded-md hover:shadow hover:bg-gray-100 dark:hover:bg-zinc-700 disabled:cursor-not-allowed disabled:opacity-60"
disabled={disabled} disabled={disabled}
onClick={handleSend} onClick={handleSend}
> >

View File

@ -1,7 +1,6 @@
import { Menu, MenuItem } from "@mui/material"; import { Menu, MenuItem } from "@mui/material";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ReactElement, useState } from "react"; import { ReactElement, useState } from "react";
import { ThreeDots } from "react-loader-spinner";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
@ -11,6 +10,7 @@ import { Message } from "@/types";
import Icon from "../Icon"; import Icon from "../Icon";
import { CodeBlock } from "../CodeBlock"; import { CodeBlock } from "../CodeBlock";
import EngineIcon from "../EngineIcon"; import EngineIcon from "../EngineIcon";
import ThreeDotsLoader from "./ThreeDotsLoader";
interface Props { interface Props {
message: Message; message: Message;
@ -56,10 +56,10 @@ const MessageView = (props: Props) => {
> >
{isCurrentUser ? ( {isCurrentUser ? (
<> <>
<div className="w-auto max-w-full bg-indigo-600 text-white px-4 py-2 rounded-lg whitespace-pre-wrap break-all"> <div className="w-auto max-w-full bg-indigo-600 text-white dark:text-gray-200 px-4 py-2 rounded-lg whitespace-pre-wrap break-all">
{message.content} {message.content}
</div> </div>
<div className="w-10 h-10 p-1 border rounded-full flex justify-center items-center ml-2 shrink-0"> <div className="w-10 h-10 p-1 border dark:border-zinc-700 rounded-full flex justify-center items-center ml-2 shrink-0">
<Icon.AiOutlineUser className="w-6 h-6" /> <Icon.AiOutlineUser className="w-6 h-6" />
</div> </div>
</> </>
@ -67,20 +67,20 @@ const MessageView = (props: Props) => {
<> <>
<div className="flex justify-center items-center mr-2 shrink-0"> <div className="flex justify-center items-center mr-2 shrink-0">
{connection ? ( {connection ? (
<EngineIcon className="w-10 h-auto p-1 border rounded-full" engine={connection.engineType} /> <EngineIcon className="w-10 h-auto p-1 border dark:border-zinc-700 rounded-full" engine={connection.engineType} />
) : ( ) : (
<img className="w-10 h-auto p-1" src="/chat-logo-bot.webp" alt="" /> <img className="w-10 h-auto p-1" src="/chat-logo-bot.webp" alt="" />
)} )}
</div> </div>
{message.status === "LOADING" && message.content === "" ? ( {message.status === "LOADING" && message.content === "" ? (
<div className="mt-0.5 w-12 bg-gray-100 px-4 py-2 rounded-lg"> <div className="mt-0.5 w-12 bg-gray-100 dark:bg-zinc-700 px-4 py-2 rounded-lg">
<ThreeDots wrapperClass="opacity-80" width="24" height="24" color="" /> <ThreeDotsLoader />
</div> </div>
) : ( ) : (
<> <>
<div className="w-auto max-w-[calc(100%-4rem)] flex flex-col justify-start items-start"> <div className="w-auto max-w-[calc(100%-4rem)] flex flex-col justify-start items-start">
<ReactMarkdown <ReactMarkdown
className={`w-auto max-w-full bg-gray-100 px-4 py-2 rounded-lg prose prose-neutral ${ className={`w-auto max-w-full bg-gray-100 dark:bg-zinc-700 px-4 py-2 rounded-lg prose prose-neutral dark:prose-invert ${
message.status === "FAILED" && "border border-red-400 bg-red-100 text-red-500" message.status === "FAILED" && "border border-red-400 bg-red-100 text-red-500"
}`} }`}
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}

View File

@ -0,0 +1,30 @@
import { useSettingStore } from "@/store";
import { useEffect, useState } from "react";
import { ThreeDots } from "react-loader-spinner";
const ThreeDotsLoader = () => {
const settingStore = useSettingStore();
const [color, setColor] = useState("gray");
useEffect(() => {
const theme = settingStore.setting.theme;
let appearance = theme;
if (theme === "system") {
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
appearance = "dark";
} else {
appearance = "light";
}
}
if (appearance === "dark") {
setColor("white");
} else {
setColor("gray");
}
}, [settingStore.setting.theme]);
return <ThreeDots wrapperClass="dark:opacity-60" width="24" height="24" color={color} />;
};
export default ThreeDotsLoader;

View File

@ -213,9 +213,9 @@ const ConversationView = () => {
ref={conversationViewRef} ref={conversationViewRef}
className={`${ className={`${
layoutStore.showSidebar && "sm:pl-80" layoutStore.showSidebar && "sm:pl-80"
} relative w-full h-full max-h-full flex flex-col justify-start items-start overflow-y-auto bg-white transition-all duration-300`} } relative w-full h-full max-h-full flex flex-col justify-start items-start overflow-y-auto bg-white dark:bg-zinc-800 transition-all duration-300`}
> >
<div className="sticky top-0 z-1 bg-white w-full flex flex-col justify-start items-start"> <div className="sticky top-0 z-1 bg-white dark:bg-zinc-800 w-full flex flex-col justify-start items-start">
<DataStorageBanner /> <DataStorageBanner />
<Header className={showHeaderShadow ? "shadow" : ""} /> <Header className={showHeaderShadow ? "shadow" : ""} />
</div> </div>
@ -226,7 +226,7 @@ const ConversationView = () => {
messageList.map((message) => <MessageView key={message.id} message={message} />) messageList.map((message) => <MessageView key={message.id} message={message} />)
)} )}
</div> </div>
<div className="sticky bottom-0 w-full max-w-4xl py-2 px-4 sm:px-8 mx-auto bg-white bg-opacity-80 backdrop-blur"> <div className="sticky bottom-0 w-full max-w-4xl py-2 px-4 sm:px-8 mx-auto bg-white dark:bg-zinc-800 bg-opacity-80 backdrop-blur">
<MessageTextarea disabled={lastMessage?.status === "LOADING"} sendMessage={sendMessageToCurrentConversation} /> <MessageTextarea disabled={lastMessage?.status === "LOADING"} sendMessage={sendMessageToCurrentConversation} />
</div> </div>
</div> </div>

View File

@ -208,7 +208,7 @@ const CreateConnectionModal = (props: Props) => {
<> <>
<Dialog title={isEditing ? "Edit Connection" : "Create Connection"} onClose={close}> <Dialog title={isEditing ? "Edit Connection" : "Create Connection"} onClose={close}>
<div className="w-full flex flex-col justify-start items-start space-y-3 pt-4"> <div className="w-full flex flex-col justify-start items-start space-y-3 pt-4">
<DataStorageBanner className="rounded-lg bg-white border py-2 !justify-start" alwaysShow={true} /> <DataStorageBanner className="rounded-lg bg-white border dark:border-zinc-700 py-2 !justify-start" alwaysShow={true} />
<div className="w-full flex flex-col"> <div className="w-full flex flex-col">
<label className="block text-sm font-medium text-gray-700 mb-1">Database Type</label> <label className="block text-sm font-medium text-gray-700 mb-1">Database Type</label>
<Select <Select

View File

@ -17,7 +17,7 @@ const DataStorageBanner = (props: Props) => {
<div <div
className={`${!show && "!hidden"} ${ className={`${!show && "!hidden"} ${
className || "" className || ""
} relative w-full flex flex-row justify-start sm:justify-center items-center px-4 py-1 bg-gray-100`} } relative w-full flex flex-row justify-start sm:justify-center items-center px-4 py-1 bg-gray-100 dark:bg-zinc-700`}
> >
<span className="text-sm leading-6 pr-4"> <span className="text-sm leading-6 pr-4">
<Icon.IoInformationCircleOutline className="inline-block h-5 w-auto -mt-0.5 mr-0.5 opacity-80" /> <Icon.IoInformationCircleOutline className="inline-block h-5 w-auto -mt-0.5 mr-0.5 opacity-80" />

View File

@ -53,7 +53,7 @@ const EmptyView = (props: Props) => {
{examples.map((example) => ( {examples.map((example) => (
<div <div
key={example} key={example}
className="w-full rounded-lg px-4 py-3 text-sm mb-4 cursor-pointer bg-gray-50 hover:bg-gray-100" className="w-full rounded-lg px-4 py-3 text-sm mb-4 cursor-pointer bg-gray-50 dark:bg-zinc-700 hover:opacity-80"
onClick={() => handleExampleClick(example)} onClick={() => handleExampleClick(example)}
> >
{`"${example}"`} {`"${example}"`}
@ -63,14 +63,20 @@ const EmptyView = (props: Props) => {
<div className="w-full flex flex-col justify-start items-center"> <div className="w-full flex flex-col justify-start items-center">
<Icon.BsLightning className="w-8 h-auto opacity-80" /> <Icon.BsLightning className="w-8 h-auto opacity-80" />
<span className="mt-2 mb-4">Capabilities</span> <span className="mt-2 mb-4">Capabilities</span>
<div className="w-full bg-gray-50 rounded-lg px-4 py-3 text-sm mb-4">Remembers what user said earlier in the conversation</div> <div className="w-full bg-gray-50 dark:bg-zinc-700 rounded-lg px-4 py-3 text-sm mb-4">
<div className="w-full bg-gray-50 rounded-lg px-4 py-3 text-sm mb-4">Allows user to provide follow-up corrections</div> Remembers what user said earlier in the conversation
</div>
<div className="w-full bg-gray-50 dark:bg-zinc-700 rounded-lg px-4 py-3 text-sm mb-4">
Allows user to provide follow-up corrections
</div>
</div> </div>
<div className="w-full hidden sm:flex flex-col justify-start items-center"> <div className="w-full hidden sm:flex flex-col justify-start items-center">
<Icon.BsEmojiNeutral className="w-8 h-auto opacity-80" /> <Icon.BsEmojiNeutral className="w-8 h-auto opacity-80" />
<span className="mt-2 mb-4">Limitations</span> <span className="mt-2 mb-4">Limitations</span>
<div className="w-full bg-gray-50 rounded-lg px-4 py-3 text-sm mb-4">May occasionally generate incorrect information</div> <div className="w-full bg-gray-50 dark:bg-zinc-700 rounded-lg px-4 py-3 text-sm mb-4">
<div className="w-full bg-gray-50 rounded-lg px-4 py-3 text-sm mb-4"> May occasionally generate incorrect information
</div>
<div className="w-full bg-gray-50 dark:bg-zinc-700 rounded-lg px-4 py-3 text-sm mb-4">
May occasionally produce harmful instructions or biased content May occasionally produce harmful instructions or biased content
</div> </div>
</div> </div>

View File

@ -14,8 +14,11 @@ const LocaleSwitch = () => {
}; };
return ( return (
<button className="w-10 h-10 p-1 rounded-full flex flex-row justify-center items-center hover:bg-gray-100" onClick={handleLocaleChange}> <button
<Icon.IoLanguage className="text-gray-600 w-6 h-auto" /> className="w-10 h-10 p-1 rounded-full flex flex-row justify-center items-center hover:bg-gray-100 dark:hover:bg-zinc-700"
onClick={handleLocaleChange}
>
<Icon.IoLanguage className="text-gray-600 dark:text-gray-300 w-6 h-auto" />
</button> </button>
); );
}; };

View File

@ -1,25 +0,0 @@
import { ThreeDots } from "react-loader-spinner";
import { useConnectionStore } from "@/store";
import EngineIcon from "./EngineIcon";
const MessageLoader = () => {
const connectionStore = useConnectionStore();
const connection = connectionStore.currentConnectionCtx?.connection;
return (
<div className={`w-full max-w-full flex flex-row justify-start items-start my-4 pr-8 sm:pr-24`}>
<div className="flex justify-center items-center mr-2 shrink-0">
{connection ? (
<EngineIcon className="w-10 h-auto p-1 border rounded-full" engine={connection.engineType} />
) : (
<img className="w-10 h-auto p-1" src="/chat-logo-bot.webp" alt="" />
)}
</div>
<div className="mt-0.5 w-12 bg-gray-100 px-4 py-2 rounded-lg">
<ThreeDots wrapperClass="opacity-80" width="24" height="24" color="" />
</div>
</div>
);
};
export default MessageLoader;

View File

@ -28,7 +28,7 @@ const OpenAIApiConfigView = () => {
return ( return (
<> <>
<h3 className="pl-4 text-sm text-gray-500">{t("setting.openai-api-configuration.self")}</h3> <h3 className="pl-4 text-sm text-gray-500">{t("setting.openai-api-configuration.self")}</h3>
<div className="w-full border border-gray-200 p-4 rounded-lg"> <div className="w-full border border-gray-200 dark:border-zinc-700 p-4 rounded-lg">
<div className="flex flex-col"> <div className="flex flex-col">
<label className="mb-1">Key</label> <label className="mb-1">Key</label>
<TextField <TextField

View File

@ -84,7 +84,7 @@ const QueryDrawer = () => {
return ( return (
<Drawer open={queryStore.showDrawer} anchor="right" className="w-full" onClose={close}> <Drawer open={queryStore.showDrawer} anchor="right" className="w-full" onClose={close}>
<div className="w-screen sm:w-[calc(60vw)] lg:w-[calc(50vw)] 2xl:w-[calc(40vw)] max-w-full flex flex-col justify-start items-start p-4"> <div className="dark:text-gray-300 w-screen sm:w-[calc(60vw)] lg:w-[calc(50vw)] 2xl:w-[calc(40vw)] max-w-full flex flex-col justify-start items-start p-4">
<button className="w-8 h-8 p-1 bg-zinc-600 text-gray-100 rounded-full hover:opacity-80" onClick={close}> <button className="w-8 h-8 p-1 bg-zinc-600 text-gray-100 rounded-full hover:opacity-80" onClick={close}>
<Icon.IoMdClose className="w-full h-auto" /> <Icon.IoMdClose className="w-full h-auto" />
</button> </button>
@ -101,7 +101,7 @@ const QueryDrawer = () => {
<EngineIcon className="w-6 h-auto" engine={context.connection.engineType} /> <EngineIcon className="w-6 h-auto" engine={context.connection.engineType} />
<span>{context.database?.name}</span> <span>{context.database?.name}</span>
</div> </div>
<div className="w-full h-auto mt-4 px-2 flex flex-row justify-between items-end border rounded-lg overflow-clip"> <div className="w-full h-auto mt-4 px-2 flex flex-row justify-between items-end border dark:border-zinc-700 rounded-lg overflow-clip">
<TextareaAutosize <TextareaAutosize
className="w-full h-full outline-none border-none bg-transparent leading-6 pl-2 py-2 resize-none hide-scrollbar text-sm font-mono break-all" className="w-full h-full outline-none border-none bg-transparent leading-6 pl-2 py-2 resize-none hide-scrollbar text-sm font-mono break-all"
value={statement} value={statement}
@ -112,7 +112,7 @@ const QueryDrawer = () => {
onChange={(e) => setStatement(e.target.value)} onChange={(e) => setStatement(e.target.value)}
/> />
<button <button
className="w-8 p-1 -translate-y-1 cursor-pointer rounded-md hover:shadow hover:bg-gray-100 disabled:cursor-not-allowed disabled:opacity-60" className="w-8 p-1 -translate-y-1 cursor-pointer rounded-md hover:shadow hover:bg-gray-100 dark:hover:bg-zinc-700 disabled:cursor-not-allowed disabled:opacity-60"
onClick={() => executeStatement(statement)} onClick={() => executeStatement(statement)}
> >
<Icon.IoPlay className="w-full h-auto text-indigo-600" /> <Icon.IoPlay className="w-full h-auto text-indigo-600" />
@ -131,7 +131,14 @@ const QueryDrawer = () => {
</div> </div>
) : ( ) : (
<div className="w-full"> <div className="w-full">
<DataTable className="w-full border !rounded-lg" columns={columns} data={rawResults} fixedHeader pagination responsive /> <DataTable
className="w-full border !rounded-lg dark:border-zinc-700"
columns={columns}
data={rawResults}
fixedHeader
pagination
responsive
/>
</div> </div>
)} )}
</div> </div>

View File

@ -4,6 +4,7 @@ import Icon from "./Icon";
import WeChatQRCodeView from "./WeChatQRCodeView"; import WeChatQRCodeView from "./WeChatQRCodeView";
import ClearDataButton from "./ClearDataButton"; import ClearDataButton from "./ClearDataButton";
import LocaleSelector from "./LocaleSelector"; import LocaleSelector from "./LocaleSelector";
import ThemeSelector from "./ThemeSelector";
import OpenAIApiConfigView from "./OpenAIApiConfigView"; import OpenAIApiConfigView from "./OpenAIApiConfigView";
interface Props { interface Props {
@ -30,17 +31,21 @@ const SettingModal = (props: Props) => {
</div> </div>
<h3 className="pl-4 text-sm text-gray-500">{t("setting.basic.self")}</h3> <h3 className="pl-4 text-sm text-gray-500">{t("setting.basic.self")}</h3>
<div className="w-full border border-gray-200 p-4 rounded-lg"> <div className="w-full border border-gray-200 dark:border-zinc-700 p-4 rounded-lg space-y-2">
<div className="w-full flex flex-row justify-between items-center gap-2"> <div className="w-full flex flex-row justify-between items-center gap-2">
<span>{t("setting.basic.language")}</span> <span>{t("setting.basic.language")}</span>
<LocaleSelector /> <LocaleSelector />
</div> </div>
<div className="w-full flex flex-row justify-between items-center gap-2">
<span>Theme</span>
<ThemeSelector />
</div>
</div> </div>
<OpenAIApiConfigView /> <OpenAIApiConfigView />
<h3 className="pl-4 text-sm text-gray-500">{t("setting.data.self")}</h3> <h3 className="pl-4 text-sm text-gray-500">{t("setting.data.self")}</h3>
<div className="w-full border border-red-200 p-4 rounded-lg"> <div className="w-full border border-red-200 dark:border-zinc-700 p-4 rounded-lg">
<div className="w-full flex flex-row justify-between items-center gap-2"> <div className="w-full flex flex-row justify-between items-center gap-2">
<span>{t("setting.data.clear-all-data")}</span> <span>{t("setting.data.clear-all-data")}</span>
<ClearDataButton /> <ClearDataButton />

View File

@ -0,0 +1,36 @@
import { useSettingStore } from "@/store";
import { Theme } from "@/types";
import Select from "./kit/Select";
interface ThemeItem {
value: Theme;
label: string;
}
const themeItemList: ThemeItem[] = [
{
value: "system",
label: "System",
},
{
value: "light",
label: "Light",
},
{
value: "dark",
label: "Dark",
},
];
const ThemeSelector = () => {
const settingStore = useSettingStore();
const theme = settingStore.setting.theme;
const handleThemeChange = (theme: Theme) => {
settingStore.setTheme(theme);
};
return <Select className="w-28" value={theme} itemList={themeItemList} onValueChange={handleThemeChange} />;
};
export default ThemeSelector;

View File

@ -0,0 +1,23 @@
import { useSettingStore } from "@/store";
import Icon from "./Icon";
const ThemeSwitch = () => {
const settingStore = useSettingStore();
const theme = settingStore.setting.theme;
const handleThemeChange = () => {
if (theme === "light") {
settingStore.setLocale("zh");
} else {
settingStore.setLocale("en");
}
};
return (
<button className="w-10 h-10 p-1 rounded-full flex flex-row justify-center items-center hover:bg-gray-100" onClick={handleThemeChange}>
<Icon.IoSunny className="text-gray-600 w-6 h-auto" />
</button>
);
};
export default ThemeSwitch;

View File

@ -15,10 +15,10 @@ const Dialog = (props: Props) => {
<DialogUI.Root open={true}> <DialogUI.Root open={true}>
<DialogUI.Portal> <DialogUI.Portal>
<DialogUI.Overlay className="fixed inset-0 bg-black bg-opacity-60 z-[999]" /> <DialogUI.Overlay className="fixed inset-0 bg-black bg-opacity-60 z-[999]" />
<DialogUI.Content className="bg-white rounded-xl p-4 px-5 fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] z-[999]"> <DialogUI.Content className="bg-white dark:bg-zinc-800 rounded-xl p-4 px-5 fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[450px] translate-x-[-50%] translate-y-[-50%] z-[999]">
<DialogUI.Title className="text-lg text-black font-medium mb-2">{title}</DialogUI.Title> <DialogUI.Title className="text-lg text-black dark:text-gray-300 font-medium mb-2">{title}</DialogUI.Title>
<DialogUI.Close <DialogUI.Close
className="absolute top-3 right-3 outline-none w-8 h-8 p-1 bg-zinc-600 rounded-full text-white hover:opacity-80" className="absolute top-3 right-3 outline-none w-8 h-8 p-1 bg-zinc-600 rounded-full text-gray-300 hover:opacity-80"
aria-label="Close" aria-label="Close"
onClick={onClose} onClick={onClose}
> >

View File

@ -18,7 +18,11 @@ const Select = (props: Props) => {
return ( return (
<SelectUI.Root value={value} onValueChange={onValueChange}> <SelectUI.Root value={value} onValueChange={onValueChange}>
<SelectUI.Trigger className={`${className || ""} flex flex-row justify-between items-center border px-3 py-2 rounded-lg`}> <SelectUI.Trigger
className={`${
className || ""
} flex flex-row justify-between items-center dark:text-gray-300 bg-white dark:bg-zinc-700 border dark:border-zinc-800 px-3 py-2 rounded-lg`}
>
<SelectUI.Value placeholder={placeholder} /> <SelectUI.Value placeholder={placeholder} />
<SelectUI.Icon className="ml-1 w-5 h-auto shrink-0"> <SelectUI.Icon className="ml-1 w-5 h-auto shrink-0">
<Icon.BiChevronDown className="w-full h-auto opacity-60" /> <Icon.BiChevronDown className="w-full h-auto opacity-60" />
@ -32,7 +36,7 @@ const Select = (props: Props) => {
}} }}
position="popper" position="popper"
> >
<SelectUI.Viewport className="bg-white border shadow p-1 rounded-lg"> <SelectUI.Viewport className="bg-white dark:bg-zinc-700 border dark:border-zinc-800 shadow p-1 rounded-lg">
<SelectUI.Group> <SelectUI.Group>
{placeholder && <SelectUI.Label className="w-full px-3 mt-2 mb-1 text-sm text-gray-400">{placeholder}</SelectUI.Label>} {placeholder && <SelectUI.Label className="w-full px-3 mt-2 mb-1 text-sm text-gray-400">{placeholder}</SelectUI.Label>}
{itemList.map((item) => ( {itemList.map((item) => (
@ -60,7 +64,7 @@ const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(({ children, clas
<SelectUI.Item <SelectUI.Item
className={`${ className={`${
className || "" className || ""
} w-full px-3 py-2 rounded-lg flex flex-row justify-between items-center cursor-pointer hover:bg-gray-100`} } w-full px-3 py-2 rounded-lg flex flex-row justify-between items-center cursor-pointer hover:bg-gray-100 dark:hover:bg-zinc-800`}
{...props} {...props}
ref={forwardedRef} ref={forwardedRef}
> >

View File

@ -11,7 +11,7 @@ const TextField = (props: Props) => {
return ( return (
<input <input
className={`${className || ""} w-full border px-3 py-2 rounded-lg`} className={`${className || ""} w-full border px-3 py-2 rounded-lg dark:border-zinc-700 dark:bg-zinc-800 outline-none `}
type="text" type="text"
disabled={disabled} disabled={disabled}
placeholder={placeholder} placeholder={placeholder}

View File

@ -14,17 +14,56 @@ import "@/locales/i18n";
import "@/styles/tailwind.css"; import "@/styles/tailwind.css";
import "@/styles/global.css"; import "@/styles/global.css";
import "@/styles/data-table.css"; import "@/styles/data-table.css";
import "@/styles/mui.css";
function MyApp({ Component, pageProps }: AppProps) { function MyApp({ Component, pageProps }: AppProps) {
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const settingStore = useSettingStore(); const settingStore = useSettingStore();
useEffect(() => {
const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleColorSchemeChange = (e: MediaQueryListEvent) => {
if (settingStore.getState().setting.theme === "system") {
const theme = e.matches ? "dark" : "light";
document.documentElement.classList.remove("dark");
document.documentElement.classList.remove("light");
document.documentElement.classList.add(theme);
}
};
try {
if (darkMediaQuery.addEventListener) {
darkMediaQuery.addEventListener("change", handleColorSchemeChange);
} else {
darkMediaQuery.addListener(handleColorSchemeChange);
}
} catch (error) {
console.error("failed to initial color scheme listener", error);
}
}, []);
useEffect(() => { useEffect(() => {
const locale = settingStore.setting.locale; const locale = settingStore.setting.locale;
i18n.changeLanguage(locale); i18n.changeLanguage(locale);
document.documentElement.setAttribute("lang", locale); document.documentElement.setAttribute("lang", locale);
}, [settingStore.setting.locale]); }, [settingStore.setting.locale]);
useEffect(() => {
const theme = settingStore.setting.theme;
let currentAppearance = theme;
if (theme === "system") {
if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) {
currentAppearance = "dark";
} else {
currentAppearance = "light";
}
}
document.documentElement.classList.remove("dark");
document.documentElement.classList.remove("light");
document.documentElement.classList.add(currentAppearance);
}, [settingStore.setting.theme]);
return ( return (
<> <>
<Component {...pageProps} /> <Component {...pageProps} />

View File

@ -33,7 +33,7 @@ const IndexPage: NextPage = () => {
<h1 className="sr-only">SQL Chat</h1> <h1 className="sr-only">SQL Chat</h1>
<main className="w-full h-full flex flex-row"> <main className="w-full h-full flex flex-row dark:bg-zinc-800">
<ConnectionSidebar /> <ConnectionSidebar />
<ConversationView /> <ConversationView />
<QueryDrawer /> <QueryDrawer />

View File

@ -6,6 +6,7 @@ import { Setting } from "@/types";
const getDefaultSetting = (): Setting => { const getDefaultSetting = (): Setting => {
return { return {
locale: "en", locale: "en",
theme: "system",
openAIApiConfig: { openAIApiConfig: {
key: "", key: "",
endpoint: "", endpoint: "",
@ -15,15 +16,17 @@ const getDefaultSetting = (): Setting => {
interface SettingState { interface SettingState {
setting: Setting; setting: Setting;
getState: () => SettingState;
setLocale: (locale: Setting["locale"]) => void; setLocale: (locale: Setting["locale"]) => void;
setTheme: (theme: Setting["theme"]) => void;
setOpenAIApiConfig: (openAIApiConfig: Setting["openAIApiConfig"]) => void; setOpenAIApiConfig: (openAIApiConfig: Setting["openAIApiConfig"]) => void;
} }
export const useSettingStore = create<SettingState>()( export const useSettingStore = create<SettingState>()(
persist( persist(
(set, get) => ({ (set, get) => ({
getState: () => get(),
setting: getDefaultSetting(), setting: getDefaultSetting(),
getState: () => get(),
setLocale: (locale: Setting["locale"]) => { setLocale: (locale: Setting["locale"]) => {
set({ set({
setting: { setting: {
@ -32,6 +35,14 @@ export const useSettingStore = create<SettingState>()(
}, },
}); });
}, },
setTheme: (theme: Setting["theme"]) => {
set({
setting: {
...get().setting,
theme,
},
});
},
setOpenAIApiConfig: (openAIApiConfig: Setting["openAIApiConfig"]) => { setOpenAIApiConfig: (openAIApiConfig: Setting["openAIApiConfig"]) => {
set({ set({
setting: { setting: {

View File

@ -1,3 +1,14 @@
.rdt_Pagination { .rdt_Pagination {
border-top: none !important; @apply !border-t-0 dark:bg-zinc-800 dark:text-gray-300;
}
.rdt_Pagination button {
@apply dark:bg-zinc-800 dark:text-gray-300 dark:fill-gray-300;
}
.rdt_Table,
.rdt_TableHead,
.rdt_TableHeadRow,
.rdt_TableRow {
@apply dark:bg-zinc-800 dark:text-gray-300;
} }

View File

@ -3,6 +3,5 @@ body,
body > div:first-child, body > div:first-child,
div#__next, div#__next,
div#__next > div { div#__next > div {
width: 100%; @apply w-full h-full dark:text-gray-300;
height: 100%;
} }

3
src/styles/mui.css Normal file
View File

@ -0,0 +1,3 @@
.MuiPaper-root {
@apply dark:bg-zinc-800;
}

View File

@ -19,7 +19,7 @@
} }
.btn-outline { .btn-outline {
@apply border-none hover:bg-gray-100; @apply border-none hover:bg-gray-100 dark:hover:bg-zinc-800;
} }
.btn-error { .btn-error {

View File

@ -1,5 +1,7 @@
export type Locale = "en" | "zh"; export type Locale = "en" | "zh";
export type Theme = "light" | "dark" | "system";
export interface OpenAIApiConfig { export interface OpenAIApiConfig {
key: string; key: string;
endpoint: string; endpoint: string;
@ -7,5 +9,6 @@ export interface OpenAIApiConfig {
export interface Setting { export interface Setting {
locale: Locale; locale: Locale;
theme: Theme;
openAIApiConfig: OpenAIApiConfig; openAIApiConfig: OpenAIApiConfig;
} }

View File

@ -1,6 +1,7 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
module.exports = { module.exports = {
content: ["./src/pages/**/*.{js,ts,jsx,tsx}", "./src/components/**/*.{js,ts,jsx,tsx}"], content: ["./src/pages/**/*.{js,ts,jsx,tsx}", "./src/components/**/*.{js,ts,jsx,tsx}"],
darkMode: "class",
theme: { theme: {
extend: { extend: {
zIndex: { zIndex: {