Spaces:
Running
Running
| import React, { useState } from "react"; | |
| import { | |
| Dialog, | |
| DialogContent, | |
| DialogDescription, | |
| DialogHeader, | |
| DialogTitle, | |
| } from "@/components/ui/dialog"; | |
| import { Card, CardContent } from "@/components/ui/card"; | |
| import { Button } from "@/components/ui/button"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { Separator } from "@/components/ui/separator"; | |
| import { ScrollArea } from "@/components/ui/scroll-area"; | |
| import { | |
| Settings, | |
| Palette, | |
| Monitor, | |
| Sun, | |
| Moon, | |
| Smartphone, | |
| Check, | |
| GitBranch, | |
| Layers, | |
| Brain, | |
| Zap, | |
| Clock, | |
| DollarSign, | |
| } from "lucide-react"; | |
| import { useKGDisplayMode } from "@/context/KGDisplayModeContext"; | |
| import { useModelPreferences } from "@/hooks/useModelPreferences"; | |
| import { AVAILABLE_MODELS, ModelConfig } from "@/lib/models"; | |
| interface SettingsModalProps { | |
| open: boolean; | |
| onOpenChange: (open: boolean) => void; | |
| } | |
| type Theme = "light" | "dark" | "system"; | |
| export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { | |
| const [activeSection, setActiveSection] = useState<string>("appearance"); | |
| const [currentTheme, setCurrentTheme] = useState<Theme>("system"); | |
| const { mode: kgDisplayMode, setMode: setKGDisplayMode } = useKGDisplayMode(); | |
| const { | |
| selectedModel, | |
| currentModelConfig, | |
| updateModelPreference, | |
| isLoading, | |
| } = useModelPreferences(); | |
| const handleThemeChange = (theme: Theme) => { | |
| setCurrentTheme(theme); | |
| // Apply theme to document | |
| const root = document.documentElement; | |
| if (theme === "dark") { | |
| root.classList.add("dark"); | |
| } else if (theme === "light") { | |
| root.classList.remove("dark"); | |
| } else { | |
| // System theme - detect user preference | |
| const prefersDark = window.matchMedia( | |
| "(prefers-color-scheme: dark)" | |
| ).matches; | |
| if (prefersDark) { | |
| root.classList.add("dark"); | |
| } else { | |
| root.classList.remove("dark"); | |
| } | |
| } | |
| // Save to localStorage | |
| localStorage.setItem("theme", theme); | |
| }; | |
| React.useEffect(() => { | |
| // Load saved theme on mount | |
| const savedTheme = localStorage.getItem("theme") as Theme; | |
| if (savedTheme) { | |
| setCurrentTheme(savedTheme); | |
| handleThemeChange(savedTheme); | |
| } | |
| }, []); | |
| const themeOptions = [ | |
| { | |
| id: "light" as Theme, | |
| name: "Light", | |
| description: "Light mode for bright environments", | |
| icon: Sun, | |
| }, | |
| { | |
| id: "dark" as Theme, | |
| name: "Dark", | |
| description: "Dark mode for low-light environments", | |
| icon: Moon, | |
| }, | |
| { | |
| id: "system" as Theme, | |
| name: "System", | |
| description: "Follows your system preference", | |
| icon: Monitor, | |
| }, | |
| ]; | |
| const sidebarSections = [ | |
| { | |
| id: "appearance", | |
| name: "Appearance", | |
| icon: Palette, | |
| }, | |
| { | |
| id: "models", | |
| name: "Models", | |
| icon: Brain, | |
| }, | |
| { | |
| id: "general", | |
| name: "General", | |
| icon: Settings, | |
| }, | |
| ]; | |
| const getCostLevelColor = (costLevel: string) => { | |
| switch (costLevel) { | |
| case "low": | |
| return "text-green-600"; | |
| case "medium": | |
| return "text-yellow-600"; | |
| case "high": | |
| return "text-red-600"; | |
| default: | |
| return "text-gray-600"; | |
| } | |
| }; | |
| const getSpeedIcon = (speed: string) => { | |
| switch (speed) { | |
| case "fast": | |
| return Zap; | |
| case "medium": | |
| return Clock; | |
| case "slow": | |
| return Brain; | |
| default: | |
| return Clock; | |
| } | |
| }; | |
| const renderModelCard = (model: ModelConfig) => { | |
| const SpeedIcon = getSpeedIcon(model.speed); | |
| const isSelected = selectedModel === model.id; | |
| return ( | |
| <Card | |
| key={model.id} | |
| className={`cursor-pointer transition-all duration-200 hover:shadow-md ${ | |
| isSelected | |
| ? "ring-2 ring-primary border-primary bg-primary/5" | |
| : "border-border hover:border-primary/50" | |
| }`} | |
| onClick={() => updateModelPreference(model.id)} | |
| > | |
| <CardContent className="p-4"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-3"> | |
| <div className="p-2 rounded-lg bg-muted"> | |
| <Brain className="h-4 w-4" /> | |
| </div> | |
| <div className="flex-1"> | |
| <div className="flex items-center gap-2"> | |
| <h4 className="font-medium">{model.name}</h4> | |
| {model.recommended && ( | |
| <Badge variant="secondary" className="text-xs"> | |
| Recommended | |
| </Badge> | |
| )} | |
| </div> | |
| <p className="text-sm text-muted-foreground mt-1"> | |
| {model.description} | |
| </p> | |
| <div className="flex items-center gap-4 mt-2"> | |
| <div className="flex items-center gap-1"> | |
| <SpeedIcon className="h-3 w-3 text-muted-foreground" /> | |
| <span className="text-xs text-muted-foreground capitalize"> | |
| {model.speed} | |
| </span> | |
| </div> | |
| <div className="flex items-center gap-1"> | |
| <DollarSign | |
| className={`h-3 w-3 ${getCostLevelColor( | |
| model.costLevel | |
| )}`} | |
| /> | |
| <span | |
| className={`text-xs capitalize ${getCostLevelColor( | |
| model.costLevel | |
| )}`} | |
| > | |
| {model.costLevel} cost | |
| </span> | |
| </div> | |
| <span className="text-xs text-muted-foreground"> | |
| {model.contextWindow} context | |
| </span> | |
| </div> | |
| </div> | |
| </div> | |
| {isSelected && ( | |
| <div className="p-1 rounded-full bg-primary"> | |
| <Check className="h-3 w-3 text-primary-foreground" /> | |
| </div> | |
| )} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ); | |
| }; | |
| const renderModelsSection = () => ( | |
| <div className="space-y-6"> | |
| {isLoading ? ( | |
| <div className="flex items-center justify-center p-8"> | |
| <div className="w-6 h-6 border-2 border-primary border-t-transparent rounded-full animate-spin mr-3" /> | |
| Loading model preferences... | |
| </div> | |
| ) : ( | |
| <> | |
| {/* Current Model Indicator */} | |
| {currentModelConfig && ( | |
| <div className="bg-primary/10 border border-primary/20 p-4 rounded-lg"> | |
| <div className="flex items-center gap-3 mb-2"> | |
| <div className="p-2 rounded-lg bg-primary/20"> | |
| <Check className="h-4 w-4 text-primary" /> | |
| </div> | |
| <div> | |
| <h4 className="font-medium text-primary"> | |
| Currently Selected | |
| </h4> | |
| <p className="text-sm text-muted-foreground"> | |
| Active for all new graph generations | |
| </p> | |
| </div> | |
| </div> | |
| <div className="ml-11"> | |
| <p className="font-medium">{currentModelConfig.name}</p> | |
| <p className="text-sm text-muted-foreground"> | |
| {currentModelConfig.description} | |
| </p> | |
| </div> | |
| </div> | |
| )} | |
| <div> | |
| <h3 className="text-lg font-semibold mb-4">Standard Models</h3> | |
| <div className="space-y-3"> | |
| {AVAILABLE_MODELS.standard.map(renderModelCard)} | |
| </div> | |
| </div> | |
| <Separator /> | |
| <div> | |
| <h3 className="text-lg font-semibold mb-4">Reasoning Models</h3> | |
| <p className="text-sm text-muted-foreground mb-4"> | |
| Specialized models for complex analysis and multi-step reasoning | |
| tasks. | |
| </p> | |
| <div className="space-y-3"> | |
| {AVAILABLE_MODELS.reasoning.map(renderModelCard)} | |
| </div> | |
| </div> | |
| <Separator /> | |
| <div className="bg-muted/30 p-4 rounded-lg"> | |
| <div className="flex items-center gap-2 mb-2"> | |
| <Brain className="h-4 w-4 text-muted-foreground" /> | |
| <span className="text-sm font-medium">Model Selection</span> | |
| </div> | |
| <p className="text-xs text-muted-foreground"> | |
| Your selected model will be used for all knowledge graph | |
| extractions. Changes apply to new generations only. | |
| </p> | |
| </div> | |
| </> | |
| )} | |
| </div> | |
| ); | |
| const renderAppearanceSection = () => ( | |
| <div className="space-y-6"> | |
| <div> | |
| <h3 className="text-lg font-semibold mb-4">Theme Selection</h3> | |
| <div className="grid grid-cols-1 gap-3"> | |
| {themeOptions.map((option) => ( | |
| <Card | |
| key={option.id} | |
| className={`cursor-pointer transition-all hover:shadow-md ${ | |
| currentTheme === option.id | |
| ? "ring-2 ring-primary bg-primary/5" | |
| : "hover:bg-muted/50" | |
| }`} | |
| onClick={() => handleThemeChange(option.id)} | |
| > | |
| <CardContent className="p-4"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-3"> | |
| <div className="p-2 rounded-lg bg-muted"> | |
| <option.icon className="h-4 w-4" /> | |
| </div> | |
| <div> | |
| <h4 className="font-medium">{option.name}</h4> | |
| <p className="text-sm text-muted-foreground"> | |
| {option.description} | |
| </p> | |
| </div> | |
| </div> | |
| {currentTheme === option.id && ( | |
| <div className="p-1 rounded-full bg-primary"> | |
| <Check className="h-3 w-3 text-primary-foreground" /> | |
| </div> | |
| )} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ))} | |
| </div> | |
| </div> | |
| <Separator /> | |
| <div> | |
| <h3 className="text-lg font-semibold mb-4">Color Customization</h3> | |
| <div className="bg-muted/30 p-4 rounded-lg"> | |
| <div className="flex items-center gap-2 mb-2"> | |
| <Smartphone className="h-4 w-4 text-muted-foreground" /> | |
| <span className="text-sm font-medium">Coming Soon</span> | |
| <Badge variant="secondary" className="text-xs"> | |
| Future Update | |
| </Badge> | |
| </div> | |
| <p className="text-sm text-muted-foreground"> | |
| Custom color themes and accent colors will be available in a future | |
| update. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| const renderGeneralSection = () => ( | |
| <div className="space-y-6"> | |
| <div> | |
| <h3 className="text-lg font-semibold mb-4">Knowledge Graph Display</h3> | |
| <div className="grid grid-cols-1 gap-3"> | |
| {[ | |
| { | |
| id: "multiple" as const, | |
| name: "Multiple Knowledge Graphs", | |
| description: | |
| "Show all knowledge graphs for research and comparison", | |
| icon: Layers, | |
| }, | |
| { | |
| id: "single" as const, | |
| name: "Single Knowledge Graph", | |
| description: | |
| "Show only the most recent/best knowledge graph per trace", | |
| icon: GitBranch, | |
| }, | |
| ].map((option) => ( | |
| <Card | |
| key={option.id} | |
| className={`cursor-pointer transition-all hover:shadow-md ${ | |
| kgDisplayMode === option.id | |
| ? "ring-2 ring-primary bg-primary/5" | |
| : "hover:bg-muted/50" | |
| }`} | |
| onClick={() => setKGDisplayMode(option.id)} | |
| > | |
| <CardContent className="p-4"> | |
| <div className="flex items-center justify-between"> | |
| <div className="flex items-center gap-3"> | |
| <div className="p-2 rounded-lg bg-muted"> | |
| <option.icon className="h-4 w-4" /> | |
| </div> | |
| <div> | |
| <h4 className="font-medium">{option.name}</h4> | |
| <p className="text-sm text-muted-foreground"> | |
| {option.description} | |
| </p> | |
| </div> | |
| </div> | |
| {kgDisplayMode === option.id && ( | |
| <div className="p-1 rounded-full bg-primary"> | |
| <Check className="h-3 w-3 text-primary-foreground" /> | |
| </div> | |
| )} | |
| </div> | |
| </CardContent> | |
| </Card> | |
| ))} | |
| </div> | |
| </div> | |
| <Separator /> | |
| <div> | |
| <h3 className="text-lg font-semibold mb-4">Additional Settings</h3> | |
| <div className="bg-muted/30 p-4 rounded-lg"> | |
| <div className="flex items-center gap-2 mb-2"> | |
| <Settings className="h-4 w-4 text-muted-foreground" /> | |
| <span className="text-sm font-medium">More Options</span> | |
| <Badge variant="secondary" className="text-xs"> | |
| Coming Soon | |
| </Badge> | |
| </div> | |
| <p className="text-sm text-muted-foreground"> | |
| More configuration options including language preferences, default | |
| views, and notification settings will be available soon. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| const renderSection = () => { | |
| switch (activeSection) { | |
| case "appearance": | |
| return renderAppearanceSection(); | |
| case "models": | |
| return renderModelsSection(); | |
| case "general": | |
| return renderGeneralSection(); | |
| default: | |
| return renderAppearanceSection(); | |
| } | |
| }; | |
| return ( | |
| <Dialog open={open} onOpenChange={onOpenChange}> | |
| <DialogContent className="max-w-4xl max-h-[90vh] p-0"> | |
| <DialogHeader className="p-6 pb-0"> | |
| <DialogTitle className="text-2xl font-bold flex items-center gap-2"> | |
| <Settings className="h-6 w-6" /> | |
| Settings & Preferences | |
| </DialogTitle> | |
| <DialogDescription className="text-base"> | |
| Customize your AgentGraph experience | |
| </DialogDescription> | |
| </DialogHeader> | |
| <div className="flex flex-1 min-h-0"> | |
| {/* Navigation Sidebar */} | |
| <div className="w-64 border-r bg-muted/20 p-4"> | |
| <nav className="space-y-2"> | |
| {sidebarSections.map((section) => ( | |
| <Button | |
| key={section.id} | |
| variant={activeSection === section.id ? "secondary" : "ghost"} | |
| className="w-full justify-start gap-2 h-auto p-3" | |
| onClick={() => setActiveSection(section.id)} | |
| > | |
| <section.icon className="h-4 w-4" /> | |
| <span className="text-sm font-medium">{section.name}</span> | |
| </Button> | |
| ))} | |
| </nav> | |
| </div> | |
| {/* Content Area */} | |
| <div className="flex-1 min-w-0"> | |
| <ScrollArea className="h-[70vh] p-6">{renderSection()}</ScrollArea> | |
| </div> | |
| </div> | |
| <Separator /> | |
| <div className="p-6 pt-4 flex justify-between items-center"> | |
| <div className="text-sm text-muted-foreground"> | |
| Changes are saved automatically | |
| </div> | |
| <Button onClick={() => onOpenChange(false)}>Done</Button> | |
| </div> | |
| </DialogContent> | |
| </Dialog> | |
| ); | |
| } | |