feat: Add light/dark mode support with backend persistence
This commit is contained in:
@@ -2,6 +2,7 @@ import ReactMarkdown, { type Components } from "react-markdown"
|
||||
import remarkGfm from "remark-gfm"
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
interface MarkdownContentProps {
|
||||
content: string
|
||||
@@ -27,7 +28,7 @@ export function MarkdownContent({ content }: MarkdownContentProps) {
|
||||
</SyntaxHighlighter>
|
||||
)
|
||||
}
|
||||
return <code className={className}>{children}</code>
|
||||
return <code className={cn("bg-muted px-1.5 py-0.5 rounded font-mono text-[0.8em]", className)}>{children}</code>
|
||||
},
|
||||
table({ children }) {
|
||||
return (
|
||||
|
||||
@@ -55,7 +55,7 @@ export function MessageBubble({ message }: MessageBubbleProps) {
|
||||
<Button
|
||||
key={`stream-${index}`}
|
||||
variant="ghost"
|
||||
className="relative group p-0 h-auto w-full max-w-2xl mx-auto overflow-hidden hover:bg-transparent flex justify-center border bg-white"
|
||||
className="relative group p-0 h-auto w-full max-w-2xl mx-auto overflow-hidden hover:bg-transparent flex justify-center border bg-white dark:bg-muted/20"
|
||||
onClick={() => setSelectedPlot(src)}
|
||||
>
|
||||
<img
|
||||
@@ -75,7 +75,7 @@ export function MessageBubble({ message }: MessageBubbleProps) {
|
||||
<Button
|
||||
key={`history-${index}`}
|
||||
variant="ghost"
|
||||
className="relative group p-0 h-auto w-full max-w-2xl mx-auto overflow-hidden hover:bg-transparent flex justify-center border bg-white"
|
||||
className="relative group p-0 h-auto w-full max-w-2xl mx-auto overflow-hidden hover:bg-transparent flex justify-center border bg-white dark:bg-muted/20"
|
||||
onClick={() => setSelectedPlot(src)}
|
||||
>
|
||||
<img
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import * as React from "react"
|
||||
import { SidebarProvider, SidebarInset, SidebarTrigger } from "@/components/ui/sidebar"
|
||||
import { HistorySidebar, type Conversation } from "./HistorySidebar"
|
||||
import { ThemeToggle } from "./ThemeToggle"
|
||||
|
||||
interface MainLayoutProps {
|
||||
children: React.ReactNode
|
||||
@@ -33,9 +34,12 @@ export function MainLayout({
|
||||
onDelete={onDelete}
|
||||
/>
|
||||
<SidebarInset className="flex flex-col flex-1 h-full overflow-hidden">
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 border-b px-4" role="navigation">
|
||||
<SidebarTrigger />
|
||||
<div className="font-semibold">Chat</div>
|
||||
<header className="flex h-16 shrink-0 items-center justify-between border-b px-4" role="navigation">
|
||||
<div className="flex items-center gap-2">
|
||||
<SidebarTrigger />
|
||||
<div className="font-semibold">Chat</div>
|
||||
</div>
|
||||
<ThemeToggle />
|
||||
</header>
|
||||
<main className="flex-1 flex flex-col p-6 overflow-hidden bg-muted/10">
|
||||
{children}
|
||||
|
||||
20
frontend/src/components/layout/ThemeToggle.tsx
Normal file
20
frontend/src/components/layout/ThemeToggle.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Moon, Sun } from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { useTheme } from "@/components/theme-provider"
|
||||
|
||||
export function ThemeToggle() {
|
||||
const { theme, toggleTheme } = useTheme()
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={toggleTheme}
|
||||
title={`Switch to ${theme === "light" ? "dark" : "light"} mode`}
|
||||
>
|
||||
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
55
frontend/src/components/theme-provider.tsx
Normal file
55
frontend/src/components/theme-provider.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { createContext, useContext, useEffect, useState } from "react"
|
||||
import { AuthService } from "@/services/auth"
|
||||
|
||||
type Theme = "light" | "dark"
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme
|
||||
setTheme: (theme: Theme) => void
|
||||
toggleTheme: () => void
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined)
|
||||
|
||||
export function ThemeProvider({
|
||||
children,
|
||||
initialTheme = "light",
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
initialTheme?: Theme
|
||||
}) {
|
||||
const [theme, setThemeState] = useState<Theme>(initialTheme)
|
||||
|
||||
useEffect(() => {
|
||||
const root = window.document.documentElement
|
||||
root.classList.remove("light", "dark")
|
||||
root.classList.add(theme)
|
||||
}, [theme])
|
||||
|
||||
const setTheme = async (newTheme: Theme) => {
|
||||
setThemeState(newTheme)
|
||||
try {
|
||||
await AuthService.updateTheme(newTheme)
|
||||
} catch (error) {
|
||||
console.error("Failed to sync theme to backend:", error)
|
||||
}
|
||||
}
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(theme === "light" ? "dark" : "light")
|
||||
}
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
export const useTheme = () => {
|
||||
const context = useContext(ThemeContext)
|
||||
if (context === undefined) {
|
||||
throw new Error("useTheme must be used within a ThemeProvider")
|
||||
}
|
||||
return context
|
||||
}
|
||||
Reference in New Issue
Block a user