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)}
>
<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">
<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)}
>
<img src="/chat-logo-bot.webp" className="w-7 h-auto mx-auto" alt="" />
@ -173,7 +175,7 @@ const ConnectionSidebar = () => {
<button
key={connection.id}
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)}
>
@ -184,14 +186,14 @@ const ConnectionSidebar = () => {
handleEditConnection(connection);
}}
>
<Icon.FiEdit3 className="w-3.5 h-auto" />
<Icon.FiEdit3 className="w-3.5 h-auto dark:text-gray-300" />
</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>
))}
<Tooltip title={t("connection.new")} side="right">
<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)}
>
<Icon.AiOutlinePlus className="w-auto h-full mx-auto" />
@ -202,27 +204,27 @@ const ConnectionSidebar = () => {
<LocaleSwitch />
<Tooltip title={t("common.setting")} side="right">
<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")}
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>
</Tooltip>
</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="" />
<div className="w-full grow">
{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")}
</div>
)}
{databaseList.length > 0 && (
<div className="w-full sticky top-0 z-1 my-4">
<Select
className="w-full bg-white px-4 py-3"
className="w-full px-4 py-3"
value={currentConnectionCtx?.database?.name}
itemList={databaseList.map((database) => {
return {
@ -238,8 +240,8 @@ const ConnectionSidebar = () => {
{conversationList.map((conversation) => (
<div
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 ${
conversation.id === conversationStore.currentConversation?.id && "!bg-white border-gray-200 font-medium"
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 dark:bg-zinc-800 border-gray-200 font-medium"
}`}
onClick={() => handleConversationSelect(conversation)}
>
@ -268,17 +270,17 @@ const ConnectionSidebar = () => {
</div>
))}
<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}
>
<Icon.AiOutlinePlus className="w-5 h-auto mr-1" />
{t("conversation.new-chat")}
</button>
</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
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"
>
<Icon.BsDiscord className="w-4 h-auto mr-1" />

View File

@ -22,11 +22,11 @@ const Header = (props: Props) => {
<div
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">
<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()}
>
<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">
<a
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"
>
<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 (
<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
ref={textareaRef}
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}
/>
<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}
onClick={handleSend}
>

View File

@ -1,7 +1,6 @@
import { Menu, MenuItem } from "@mui/material";
import dayjs from "dayjs";
import { ReactElement, useState } from "react";
import { ThreeDots } from "react-loader-spinner";
import { useTranslation } from "react-i18next";
import { toast } from "react-hot-toast";
import ReactMarkdown from "react-markdown";
@ -11,6 +10,7 @@ import { Message } from "@/types";
import Icon from "../Icon";
import { CodeBlock } from "../CodeBlock";
import EngineIcon from "../EngineIcon";
import ThreeDotsLoader from "./ThreeDotsLoader";
interface Props {
message: Message;
@ -56,10 +56,10 @@ const MessageView = (props: Props) => {
>
{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}
</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" />
</div>
</>
@ -67,20 +67,20 @@ const MessageView = (props: Props) => {
<>
<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} />
<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="" />
)}
</div>
{message.status === "LOADING" && message.content === "" ? (
<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 className="mt-0.5 w-12 bg-gray-100 dark:bg-zinc-700 px-4 py-2 rounded-lg">
<ThreeDotsLoader />
</div>
) : (
<>
<div className="w-auto max-w-[calc(100%-4rem)] flex flex-col justify-start items-start">
<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"
}`}
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}
className={`${
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 />
<Header className={showHeaderShadow ? "shadow" : ""} />
</div>
@ -226,7 +226,7 @@ const ConversationView = () => {
messageList.map((message) => <MessageView key={message.id} message={message} />)
)}
</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} />
</div>
</div>

View File

@ -208,7 +208,7 @@ const CreateConnectionModal = (props: Props) => {
<>
<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">
<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">
<label className="block text-sm font-medium text-gray-700 mb-1">Database Type</label>
<Select

View File

@ -17,7 +17,7 @@ const DataStorageBanner = (props: Props) => {
<div
className={`${!show && "!hidden"} ${
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">
<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) => (
<div
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)}
>
{`"${example}"`}
@ -63,14 +63,20 @@ const EmptyView = (props: Props) => {
<div className="w-full flex flex-col justify-start items-center">
<Icon.BsLightning className="w-8 h-auto opacity-80" />
<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 rounded-lg px-4 py-3 text-sm mb-4">Allows user to provide follow-up corrections</div>
<div className="w-full bg-gray-50 dark:bg-zinc-700 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">
Allows user to provide follow-up corrections
</div>
</div>
<div className="w-full hidden sm:flex flex-col justify-start items-center">
<Icon.BsEmojiNeutral className="w-8 h-auto opacity-80" />
<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 rounded-lg px-4 py-3 text-sm mb-4">
<div className="w-full bg-gray-50 dark:bg-zinc-700 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
</div>
</div>

View File

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

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 (
<>
<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">
<label className="mb-1">Key</label>
<TextField

View File

@ -84,7 +84,7 @@ const QueryDrawer = () => {
return (
<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}>
<Icon.IoMdClose className="w-full h-auto" />
</button>
@ -101,7 +101,7 @@ const QueryDrawer = () => {
<EngineIcon className="w-6 h-auto" engine={context.connection.engineType} />
<span>{context.database?.name}</span>
</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
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}
@ -112,7 +112,7 @@ const QueryDrawer = () => {
onChange={(e) => setStatement(e.target.value)}
/>
<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)}
>
<Icon.IoPlay className="w-full h-auto text-indigo-600" />
@ -131,7 +131,14 @@ const QueryDrawer = () => {
</div>
) : (
<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>

View File

@ -4,6 +4,7 @@ import Icon from "./Icon";
import WeChatQRCodeView from "./WeChatQRCodeView";
import ClearDataButton from "./ClearDataButton";
import LocaleSelector from "./LocaleSelector";
import ThemeSelector from "./ThemeSelector";
import OpenAIApiConfigView from "./OpenAIApiConfigView";
interface Props {
@ -30,17 +31,21 @@ const SettingModal = (props: Props) => {
</div>
<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">
<span>{t("setting.basic.language")}</span>
<LocaleSelector />
</div>
<div className="w-full flex flex-row justify-between items-center gap-2">
<span>Theme</span>
<ThemeSelector />
</div>
</div>
<OpenAIApiConfigView />
<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">
<span>{t("setting.data.clear-all-data")}</span>
<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.Portal>
<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.Title className="text-lg text-black font-medium mb-2">{title}</DialogUI.Title>
<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 dark:text-gray-300 font-medium mb-2">{title}</DialogUI.Title>
<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"
onClick={onClose}
>

View File

@ -18,7 +18,11 @@ const Select = (props: Props) => {
return (
<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.Icon className="ml-1 w-5 h-auto shrink-0">
<Icon.BiChevronDown className="w-full h-auto opacity-60" />
@ -32,7 +36,7 @@ const Select = (props: Props) => {
}}
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>
{placeholder && <SelectUI.Label className="w-full px-3 mt-2 mb-1 text-sm text-gray-400">{placeholder}</SelectUI.Label>}
{itemList.map((item) => (
@ -60,7 +64,7 @@ const SelectItem = forwardRef<HTMLDivElement, SelectItemProps>(({ children, clas
<SelectUI.Item
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}
ref={forwardedRef}
>

View File

@ -11,7 +11,7 @@ const TextField = (props: Props) => {
return (
<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"
disabled={disabled}
placeholder={placeholder}

View File

@ -14,17 +14,56 @@ import "@/locales/i18n";
import "@/styles/tailwind.css";
import "@/styles/global.css";
import "@/styles/data-table.css";
import "@/styles/mui.css";
function MyApp({ Component, pageProps }: AppProps) {
const { i18n } = useTranslation();
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(() => {
const locale = settingStore.setting.locale;
i18n.changeLanguage(locale);
document.documentElement.setAttribute("lang", 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 (
<>
<Component {...pageProps} />

View File

@ -33,7 +33,7 @@ const IndexPage: NextPage = () => {
<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 />
<ConversationView />
<QueryDrawer />

View File

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

View File

@ -1,3 +1,14 @@
.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,
div#__next,
div#__next > div {
width: 100%;
height: 100%;
@apply w-full h-full dark:text-gray-300;
}

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 {
@apply border-none hover:bg-gray-100;
@apply border-none hover:bg-gray-100 dark:hover:bg-zinc-800;
}
.btn-error {

View File

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

View File

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