Spaces:
Running
Running
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> | |
); | |
}; | |