mirror of
https://github.com/sqlchat/sqlchat.git
synced 2025-07-24 23:34:28 +08:00
feat: implement dark mode (#27)
* feat: implement dark mode * feat: update data table dark mode style * chore: update
This commit is contained in:
@ -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" />
|
||||
|
@ -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="" />
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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]}
|
||||
|
30
src/components/ConversationView/ThreeDotsLoader.tsx
Normal file
30
src/components/ConversationView/ThreeDotsLoader.tsx
Normal 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;
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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" />
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
@ -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;
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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 />
|
||||
|
36
src/components/ThemeSelector.tsx
Normal file
36
src/components/ThemeSelector.tsx
Normal 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;
|
23
src/components/ThemeSwitch.tsx
Normal file
23
src/components/ThemeSwitch.tsx
Normal 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;
|
@ -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}
|
||||
>
|
||||
|
@ -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}
|
||||
>
|
||||
|
@ -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}
|
||||
|
@ -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} />
|
||||
|
@ -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 />
|
||||
|
@ -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: {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
3
src/styles/mui.css
Normal file
@ -0,0 +1,3 @@
|
||||
.MuiPaper-root {
|
||||
@apply dark:bg-zinc-800;
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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: {
|
||||
|
Reference in New Issue
Block a user