Buckets:
techprotrade/LuunaOS / NEXUS_AI.pro /pixel-perfect-pixels-main /src /components /chat /ModelSwitcher.tsx
| import { useState, useEffect } from "react"; | |
| import { motion } from "framer-motion"; | |
| import { Cpu, Cloud, Circle, Search as SearchIcon } from "lucide-react"; | |
| import { | |
| DropdownMenu, | |
| DropdownMenuContent, | |
| DropdownMenuItem, | |
| DropdownMenuTrigger, | |
| DropdownMenuSeparator, | |
| DropdownMenuLabel, | |
| } from "@/components/ui/dropdown-menu"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { cn } from "@/lib/utils"; | |
| export type ModelType = "local" | "gemini-flash" | "perplexity-sonar"; | |
| interface ModelOption { | |
| id: ModelType; | |
| name: string; | |
| description: string; | |
| icon: typeof Cpu; | |
| } | |
| const models: ModelOption[] = [ | |
| { id: "gemini-flash", name: "Gemini Flash", description: "Google AI Gateway", icon: Cloud }, | |
| { id: "perplexity-sonar", name: "Perplexity Sonar", description: "Web-grounded search AI", icon: SearchIcon }, | |
| { id: "local", name: "LM Studio", description: "LuunaOS Local", icon: Cpu }, | |
| ]; | |
| interface ModelSwitcherProps { | |
| selectedModel: ModelType; | |
| onModelChange: (model: ModelType) => void; | |
| } | |
| type StatusType = "online" | "offline" | "checking"; | |
| export const ModelSwitcher = ({ selectedModel, onModelChange }: ModelSwitcherProps) => { | |
| const [statuses, setStatuses] = useState<Record<ModelType, StatusType>>({ | |
| local: "checking", | |
| "gemini-flash": "online", | |
| "perplexity-sonar": "online", | |
| }); | |
| useEffect(() => { | |
| const checkLocal = async () => { | |
| try { | |
| const resp = await fetch("http://localhost:1234/v1/models", { signal: AbortSignal.timeout(3000) }); | |
| setStatuses((prev) => ({ ...prev, local: resp.ok ? "online" : "offline" })); | |
| } catch { | |
| setStatuses((prev) => ({ ...prev, local: "offline" })); | |
| } | |
| }; | |
| checkLocal(); | |
| const interval = setInterval(checkLocal, 30000); | |
| return () => clearInterval(interval); | |
| }, []); | |
| const currentModel = models.find((m) => m.id === selectedModel) || models[0]; | |
| const CurrentIcon = currentModel.icon; | |
| const getStatusColor = (status: StatusType) => { | |
| switch (status) { | |
| case "online": return "text-green-500"; | |
| case "offline": return "text-destructive"; | |
| case "checking": return "text-yellow-500 animate-pulse"; | |
| } | |
| }; | |
| return ( | |
| <DropdownMenu> | |
| <DropdownMenuTrigger asChild> | |
| <motion.button | |
| whileHover={{ scale: 1.02 }} | |
| whileTap={{ scale: 0.98 }} | |
| className={cn( | |
| "flex items-center gap-2 px-3 py-2 rounded-lg", | |
| "bg-secondary hover:bg-secondary/80", | |
| "border border-border/50 hover:border-border", | |
| "transition-colors cursor-pointer", | |
| "focus:outline-none focus:ring-2 focus:ring-primary/50" | |
| )} | |
| > | |
| <CurrentIcon size={16} className="text-primary" /> | |
| <span className="text-sm font-medium text-foreground hidden sm:inline">{currentModel.name}</span> | |
| <Circle size={8} className={cn("fill-current", getStatusColor(statuses[selectedModel]))} /> | |
| </motion.button> | |
| </DropdownMenuTrigger> | |
| <DropdownMenuContent align="end" className="w-64 bg-popover border border-border shadow-xl"> | |
| <DropdownMenuLabel className="text-xs text-muted-foreground uppercase tracking-wide"> | |
| Select AI Model | |
| </DropdownMenuLabel> | |
| <DropdownMenuSeparator /> | |
| {models.map((model) => { | |
| const Icon = model.icon; | |
| const status = statuses[model.id]; | |
| const isSelected = selectedModel === model.id; | |
| const isDisabled = status === "offline"; | |
| return ( | |
| <DropdownMenuItem | |
| key={model.id} | |
| onClick={() => !isDisabled && onModelChange(model.id)} | |
| disabled={isDisabled} | |
| className={cn( | |
| "flex items-center gap-3 p-3 cursor-pointer", | |
| isSelected && "bg-accent/50", | |
| isDisabled && "opacity-50 cursor-not-allowed" | |
| )} | |
| > | |
| <div className={cn("w-10 h-10 rounded-lg flex items-center justify-center", isSelected ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground")}> | |
| <Icon size={20} /> | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| <div className="flex items-center gap-2"> | |
| <span className="font-medium text-foreground truncate">{model.name}</span> | |
| {isSelected && <Badge variant="secondary" className="text-xs px-1.5 py-0">Active</Badge>} | |
| </div> | |
| <p className="text-xs text-muted-foreground truncate">{model.description}</p> | |
| </div> | |
| <Circle size={8} className={cn("fill-current", getStatusColor(status))} /> | |
| </DropdownMenuItem> | |
| ); | |
| })} | |
| <DropdownMenuSeparator /> | |
| <div className="px-3 py-2"> | |
| <p className="text-[10px] text-muted-foreground leading-relaxed"> | |
| <strong>Gemini Flash:</strong> Fast cloud AI via Lovable Gateway<br /> | |
| <strong>Perplexity:</strong> Real-time web search grounded<br /> | |
| <strong>LM Studio:</strong> Local model (LuunaOS) | |
| </p> | |
| </div> | |
| </DropdownMenuContent> | |
| </DropdownMenu> | |
| ); | |
| }; | |
Xet Storage Details
- Size:
- 5.2 kB
- Xet hash:
- 2d05ba9e92c2a5cd750e2177cb785694cb70b1c4de81a187f68ed7718993c931
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.