Spaces:
Sleeping
Sleeping
| import { useState, useRef, useEffect } from "react"; | |
| import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; | |
| import { Button } from "@/components/ui/button"; | |
| import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; | |
| import { Label } from "@/components/ui/label"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Separator } from "@/components/ui/separator"; | |
| import { toast } from "@/components/ui/sonner"; | |
| import { storage, STORAGE_KEYS } from "@/lib/storage"; | |
| import { X, Settings, Moon, Sun, Globe, Shield, Database, Cloud } from "lucide-react"; | |
| interface SettingsModalProps { | |
| open: boolean; | |
| onOpenChange: (open: boolean) => void; | |
| } | |
| const THEME_OPTIONS = [ | |
| { value: "light", label: "Light", icon: Sun }, | |
| { value: "dark", label: "Dark", icon: Moon }, | |
| { value: "system", label: "System", icon: Globe }, | |
| ]; | |
| export const SettingsModal = ({ open, onOpenChange }: SettingsModalProps) => { | |
| // Load settings from storage on mount | |
| const [theme, setTheme] = useState(() => { | |
| return storage.get<string>(STORAGE_KEYS.THEME) || (window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"); | |
| }); | |
| const [apiEndpoint, setApiEndpoint] = useState(() => | |
| storage.get<string>(STORAGE_KEYS.API_ENDPOINT) || "https://insight-ai-api.hf.space" | |
| ); | |
| const fileInputRef = useRef<HTMLInputElement>(null); | |
| // Sync settings from storage when modal opens | |
| useEffect(() => { | |
| if (open) { | |
| setTheme(storage.get<string>(STORAGE_KEYS.THEME)); | |
| setApiEndpoint(storage.get<string>(STORAGE_KEYS.API_ENDPOINT)); | |
| } | |
| }, [open]); | |
| const handleThemeChange = (newTheme: string) => { | |
| setTheme(newTheme); | |
| const root = window.document.documentElement; | |
| if (newTheme === "dark") { | |
| root.classList.add("dark"); | |
| } else if (newTheme === "light") { | |
| root.classList.remove("dark"); | |
| } else { | |
| // System theme | |
| if (window.matchMedia("(prefers-color-scheme: dark)").matches) { | |
| root.classList.add("dark"); | |
| } else { | |
| root.classList.remove("dark"); | |
| } | |
| } | |
| storage.set(STORAGE_KEYS.THEME, newTheme); | |
| toast.success("Theme updated successfully"); | |
| }; | |
| const handleSaveEndpoint = () => { | |
| storage.set(STORAGE_KEYS.API_ENDPOINT, apiEndpoint); | |
| toast.success("API endpoint saved successfully"); | |
| }; | |
| const handleClearChats = () => { | |
| storage.set(STORAGE_KEYS.CHATS, []); | |
| toast.success("Chat history cleared successfully"); | |
| window.location.reload(); | |
| }; | |
| const handleClearSources = () => { | |
| storage.set(STORAGE_KEYS.SOURCES, []); | |
| toast.success("Sources cleared successfully"); | |
| }; | |
| const handleResetSettings = () => { | |
| // Reset all keys to their default values using storage lib | |
| storage.resetToDefaults(); | |
| window.location.reload(); | |
| toast.success("Settings reset to defaults"); | |
| }; | |
| const handleExportStorage = () => { | |
| const data = storage.export(); | |
| const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement("a"); | |
| a.href = url; | |
| a.download = "insight-storage-export.json"; | |
| a.click(); | |
| URL.revokeObjectURL(url); | |
| toast.success("Storage exported successfully"); | |
| }; | |
| const handleImportStorage = (event: React.ChangeEvent<HTMLInputElement>) => { | |
| const file = event.target.files?.[0]; | |
| if (!file) return; | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| try { | |
| const result = e.target?.result as string; | |
| const data = JSON.parse(result); | |
| if (storage.import(data)) { | |
| toast.success("Storage imported successfully. Please refresh the page."); | |
| } else { | |
| toast.error("Failed to import storage."); | |
| } | |
| } catch { | |
| toast.error("Invalid file format."); | |
| } | |
| }; | |
| reader.readAsText(file); | |
| event.target.value = ""; | |
| }; | |
| return ( | |
| <Dialog open={open} onOpenChange={onOpenChange}> | |
| <DialogContent className="w-full max-w-3xl"> | |
| <DialogHeader> | |
| <DialogTitle className="flex items-center text-xl"> | |
| <Settings className="mr-2 h-5 w-5" /> | |
| Settings | |
| </DialogTitle> | |
| </DialogHeader> | |
| <Separator /> | |
| <div className="max-h-[80dvh] md:max-h-[75vh] overflow-y-auto grid gap-6"> | |
| <div className="space-y-4"> | |
| <h3 className="text-sm font-medium flex items-center"> | |
| <Moon className="mr-2 h-4 w-4" /> | |
| Appearance | |
| </h3> | |
| <RadioGroup | |
| value={theme} | |
| onValueChange={handleThemeChange} | |
| className="grid grid-cols-3 gap-2" | |
| > | |
| {THEME_OPTIONS.map(({ value, label, icon: Icon }) => ( | |
| <div key={value} className="flex flex-col items-center space-y-2"> | |
| <Label | |
| htmlFor={value} | |
| className={ | |
| `flex flex-col items-center justify-center w-full h-20 border rounded-md cursor-pointer transition-colors | |
| ${theme === value | |
| ? "bg-financial-accent/30 border-financial-accent/30 ring-2 ring-financial-accent" | |
| : "hover:bg-financial-accent/30 hover:border-financial-accent/30"} | |
| ` | |
| } | |
| > | |
| <Icon className="h-8 w-8 mb-2" /> | |
| {label} | |
| </Label> | |
| <RadioGroupItem value={value} id={value} className="sr-only" /> | |
| </div> | |
| ))} | |
| </RadioGroup> | |
| </div> | |
| <Separator /> | |
| <div className="space-y-4"> | |
| <h3 className="text-sm font-medium flex items-center"> | |
| <Database className="mr-2 h-4 w-4" /> | |
| API Configuration | |
| </h3> | |
| <div className="space-y-2"> | |
| <Label htmlFor="api-endpoint">API Endpoint</Label> | |
| <div className="flex space-x-2"> | |
| <Input | |
| id="api-endpoint" | |
| value={apiEndpoint} | |
| onChange={(e) => setApiEndpoint(e.target.value)} | |
| placeholder="Ex: http://localhost:8000" | |
| /> | |
| <Button onClick={handleSaveEndpoint} variant="outline"> | |
| <Cloud className="h-4 w-4 mr-2" /> | |
| Save | |
| </Button> | |
| </div> | |
| </div> | |
| </div> | |
| <Separator /> | |
| <div className="space-y-4"> | |
| <h3 className="text-sm font-medium flex items-center"> | |
| <Shield className="mr-2 h-4 w-4" /> | |
| Data Management | |
| </h3> | |
| <div className="flex flex-wrap gap-3"> | |
| <Button | |
| variant="outline" | |
| onClick={handleClearChats} | |
| > | |
| Clear Chat History | |
| </Button> | |
| <Button | |
| variant="outline" | |
| onClick={handleClearSources} | |
| > | |
| Clear Sources | |
| </Button> | |
| <Button | |
| variant="destructive" | |
| onClick={handleResetSettings} | |
| > | |
| Reset All Settings | |
| </Button> | |
| </div> | |
| </div> | |
| <Separator /> | |
| <div className="space-y-4"> | |
| <h3 className="text-sm font-medium flex items-center"> | |
| <Cloud className="mr-2 h-4 w-4" /> | |
| Backup & Restore | |
| </h3> | |
| <div className="flex flex-wrap gap-3"> | |
| <Button | |
| variant="outline" | |
| onClick={handleExportStorage} | |
| > | |
| Export Data | |
| </Button> | |
| <Button | |
| variant="outline" | |
| onClick={() => fileInputRef.current?.click()} | |
| > | |
| Import Data | |
| </Button> | |
| <input | |
| ref={fileInputRef} | |
| type="file" | |
| accept="application/json" | |
| style={{ display: "none" }} | |
| onChange={handleImportStorage} | |
| /> | |
| </div> | |
| <div className="text-xs text-muted-foreground"> | |
| Export will download your data as a JSON file. Import will overwrite your current data. | |
| </div> | |
| </div> | |
| </div> | |
| </DialogContent> | |
| </Dialog> | |
| ); | |
| }; | |