Skip to content

Commit c95a5c8

Browse files
committed
feat: add theme toggle functionality and enhance settings UI
1 parent f1cc838 commit c95a5c8

File tree

7 files changed

+170
-32
lines changed

7 files changed

+170
-32
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { Moon, Sun } from "lucide-react"
2+
3+
import { Button } from "@/components/ui/button"
4+
import {
5+
DropdownMenu,
6+
DropdownMenuContent,
7+
DropdownMenuItem,
8+
DropdownMenuTrigger,
9+
} from "@/components/ui/dropdown-menu"
10+
import { useTheme } from "@/components/theme-provider"
11+
12+
export function ModeToggle() {
13+
const { setTheme } = useTheme()
14+
15+
return (
16+
<DropdownMenu>
17+
<DropdownMenuTrigger asChild>
18+
<Button variant="outline" size="icon">
19+
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
20+
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
21+
<span className="sr-only">Toggle theme</span>
22+
</Button>
23+
</DropdownMenuTrigger>
24+
<DropdownMenuContent align="end">
25+
<DropdownMenuItem onClick={() => setTheme("light")}>
26+
Light
27+
</DropdownMenuItem>
28+
<DropdownMenuItem onClick={() => setTheme("dark")}>
29+
Dark
30+
</DropdownMenuItem>
31+
<DropdownMenuItem onClick={() => setTheme("system")}>
32+
System
33+
</DropdownMenuItem>
34+
</DropdownMenuContent>
35+
</DropdownMenu>
36+
)
37+
}

frontend/src/components/settings/general-settings.tsx

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,34 @@
1+
import { useTheme } from "@/components/theme-provider"
2+
import { Label } from "@/components/ui/label"
3+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
4+
15
export function GeneralSettings() {
6+
const { theme, setTheme } = useTheme()
7+
28
return (
39
<div className="p-8 max-w-2xl space-y-6">
410
<div>
511
<h3 className="text-lg font-medium">通用设置</h3>
6-
<p className="text-sm text-muted-foreground">暂无通用设置项。</p>
12+
<p className="text-sm text-muted-foreground">配置应用程序的通用选项。</p>
13+
</div>
14+
15+
<div className="space-y-4">
16+
<div className="space-y-2">
17+
<Label htmlFor="theme">主题</Label>
18+
<Select value={theme} onValueChange={setTheme}>
19+
<SelectTrigger id="theme" className="w-50">
20+
<SelectValue placeholder="选择主题" />
21+
</SelectTrigger>
22+
<SelectContent>
23+
<SelectItem value="light">浅色</SelectItem>
24+
<SelectItem value="dark">深色</SelectItem>
25+
<SelectItem value="system">跟随系统</SelectItem>
26+
</SelectContent>
27+
</Select>
28+
<p className="text-sm text-muted-foreground">
29+
选择应用程序的主题外观
30+
</p>
31+
</div>
732
</div>
833
</div>
934
);

frontend/src/components/settings/model-providers-settings.tsx

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -151,42 +151,42 @@ export function ModelProvidersSettings() {
151151
</Button>
152152
</div>
153153

154-
<div className="space-y-4">
154+
<div className="space-y-4 mb-10">
155155
{loading ? (
156156
<div className="flex justify-center py-8">
157157
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
158158
</div>
159159
) : (
160160
providers.map((provider) => (
161-
<Card key={provider.id} className="relative group hover:shadow-md transition-shadow">
162-
<CardHeader className="pb-3">
163-
<CardTitle className="text-base flex justify-between items-center">
164-
{provider.name}
165-
<span className="text-xs font-normal text-muted-foreground px-2 py-1 bg-muted rounded-full">
161+
<Card key={provider.id} className="relative group hover:shadow-sm transition-all hover:border-primary/20 py-0">
162+
<div className="p-4 pb-3">
163+
<div className="flex justify-between items-center mb-2">
164+
<h4 className="text-sm font-semibold">{provider.name}</h4>
165+
<span className="text-xs font-medium text-muted-foreground px-2 py-0.5 bg-muted rounded-md">
166166
{provider.type}
167167
</span>
168-
</CardTitle>
169-
</CardHeader>
170-
<CardContent>
171-
<div className="text-sm text-muted-foreground truncate">
172-
BaseURL: {provider.baseUrl || "Default"}
173168
</div>
174-
<div className="text-sm text-muted-foreground truncate">
175-
API Key: ••••••••
169+
<div className="space-y-1 text-xs text-muted-foreground">
170+
<div className="truncate">
171+
<span className="font-medium">BaseURL:</span> {provider.baseUrl || "Default"}
172+
</div>
173+
<div className="truncate">
174+
<span className="font-medium">API Key:</span> ••••••••
175+
</div>
176176
</div>
177177

178-
<div className="absolute bottom-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1 bg-background/80 rounded-md">
179-
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => handleListModels(provider.id)}>
180-
<List className="h-4 w-4" />
178+
<div className="absolute bottom-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity flex gap-0.5">
179+
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => handleListModels(provider.id)}>
180+
<List className="h-3.5 w-3.5" />
181181
</Button>
182-
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => handleOpenDialog(provider)}>
183-
<Pencil className="h-4 w-4" />
182+
<Button variant="ghost" size="icon" className="h-7 w-7" onClick={() => handleOpenDialog(provider)}>
183+
<Pencil className="h-3.5 w-3.5" />
184184
</Button>
185-
<Button variant="ghost" size="icon" className="h-8 w-8 text-destructive" onClick={() => handleDelete(provider.id)}>
186-
<Trash2 className="h-4 w-4" />
185+
<Button variant="ghost" size="icon" className="h-7 w-7 text-destructive" onClick={() => handleDelete(provider.id)}>
186+
<Trash2 className="h-3.5 w-3.5" />
187187
</Button>
188188
</div>
189-
</CardContent>
189+
</div>
190190
</Card>
191191
))
192192
)}

frontend/src/components/settings/settings-dialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
2424
<Dialog open={open} onOpenChange={onOpenChange}>
2525
<DialogContent className="min-w-200 h-150 overflow-hidden p-0 gap-0">
2626
<DialogTitle className="hidden">Settings</DialogTitle>
27-
<Tabs defaultValue="models" orientation="vertical" className="flex h-full w-full">
27+
<Tabs defaultValue="general" orientation="vertical" className="flex h-full w-full">
2828
{/* Sidebar */}
2929
<div className="w-60 bg-muted/30 border-r h-full flex flex-col shrink-0">
3030
<div className="p-6 pb-4">
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { createContext, useContext, useEffect, useState } from "react"
2+
3+
type Theme = "dark" | "light" | "system"
4+
5+
type ThemeProviderProps = {
6+
children: React.ReactNode
7+
defaultTheme?: Theme
8+
storageKey?: string
9+
}
10+
11+
type ThemeProviderState = {
12+
theme: Theme
13+
setTheme: (theme: Theme) => void
14+
}
15+
16+
const initialState: ThemeProviderState = {
17+
theme: "system",
18+
setTheme: () => null,
19+
}
20+
21+
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
22+
23+
export function ThemeProvider({
24+
children,
25+
defaultTheme = "system",
26+
storageKey = "vite-ui-theme",
27+
...props
28+
}: ThemeProviderProps) {
29+
const [theme, setTheme] = useState<Theme>(
30+
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
31+
)
32+
33+
useEffect(() => {
34+
const root = window.document.documentElement
35+
36+
root.classList.remove("light", "dark")
37+
38+
if (theme === "system") {
39+
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
40+
.matches
41+
? "dark"
42+
: "light"
43+
44+
root.classList.add(systemTheme)
45+
return
46+
}
47+
48+
root.classList.add(theme)
49+
}, [theme])
50+
51+
const value = {
52+
theme,
53+
setTheme: (theme: Theme) => {
54+
localStorage.setItem(storageKey, theme)
55+
setTheme(theme)
56+
},
57+
}
58+
59+
return (
60+
<ThemeProviderContext.Provider {...props} value={value}>
61+
{children}
62+
</ThemeProviderContext.Provider>
63+
)
64+
}
65+
66+
export const useTheme = () => {
67+
const context = useContext(ThemeProviderContext)
68+
69+
if (context === undefined)
70+
throw new Error("useTheme must be used within a ThemeProvider")
71+
72+
return context
73+
}

frontend/src/components/ui/sidebar.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -575,7 +575,7 @@ function SidebarMenuBadge({
575575
data-slot="sidebar-menu-badge"
576576
data-sidebar="menu-badge"
577577
className={cn(
578-
"text-sidebar-foreground peer-hover/menu-button:text-sidebar-accent-foreground peer-data-active/menu-button:text-sidebar-accent-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 rounded-md px-1 text-xs font-medium peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 flex items-center justify-center tabular-nums select-none group-data-[collapsible=icon]:hidden",
578+
"text-sidebar-foreground peer-hover/menu-button:text-sidebar-accent-foreground peer-data-active/menu-button:text-sidebar-accent-foreground pointer-events-none absolute right-1 flex h-5 min-w-5 rounded-md px-1 text-xs font-medium peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 items-center justify-center tabular-nums select-none group-data-[collapsible=icon]:hidden",
579579
className
580580
)}
581581
{...props}

frontend/src/main.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
import { StrictMode } from "react"
2-
import { createRoot } from "react-dom/client"
1+
import { StrictMode } from "react";
2+
import { createRoot } from "react-dom/client";
33

4-
import "./index.css"
5-
import App from "./App.tsx"
6-
import { Toaster } from "./components/ui/sonner.tsx"
4+
import "./index.css";
5+
import App from "./App.tsx";
6+
import { Toaster } from "./components/ui/sonner.tsx";
7+
import { ThemeProvider } from "./components/theme-provider.tsx";
78

89
createRoot(document.getElementById("root")!).render(
910
<StrictMode>
10-
<App />
11-
<Toaster position="top-center" />
11+
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
12+
<App />
13+
<Toaster position="top-center" />
14+
</ThemeProvider>
1215
</StrictMode>
13-
)
16+
);

0 commit comments

Comments
 (0)