techprotrade's picture
download
raw
5.2 kB
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.