mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-18 21:15:16 +02:00
198 lines
6 KiB
TypeScript
198 lines
6 KiB
TypeScript
import { RefreshCw } from "lucide-react";
|
|
import { useState } from "react";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Spinner } from "@/components/ui/spinner";
|
|
import {
|
|
capability,
|
|
capabilityLabels,
|
|
MODEL_CAPABILITY_FILTERS,
|
|
type ModelCapabilityFilter,
|
|
modelLabel,
|
|
type SelectableModel,
|
|
} from "./model-utils";
|
|
|
|
interface ModelsSelectionPanelProps {
|
|
models: SelectableModel[];
|
|
description?: string;
|
|
emptyMessage?: string;
|
|
manualInputPlaceholder?: string;
|
|
refreshLabel?: string;
|
|
isRefreshing?: boolean;
|
|
isAddingManual?: boolean;
|
|
isUpdatingModel?: boolean;
|
|
isBulkUpdating?: boolean;
|
|
onRefresh?: () => void;
|
|
onAddManual?: (modelId: string) => void;
|
|
onToggleModel?: (model: SelectableModel, enabled: boolean) => void;
|
|
onBulkToggle?: (models: SelectableModel[], enabled: boolean) => void;
|
|
}
|
|
|
|
export function ModelsSelectionPanel({
|
|
models,
|
|
description = "Select models to make available for this provider.",
|
|
emptyMessage = "No models available.",
|
|
manualInputPlaceholder = "Add a model ID manually",
|
|
refreshLabel = "Refresh models",
|
|
isRefreshing = false,
|
|
isAddingManual = false,
|
|
isUpdatingModel = false,
|
|
isBulkUpdating = false,
|
|
onRefresh,
|
|
onAddManual,
|
|
onToggleModel,
|
|
onBulkToggle,
|
|
}: ModelsSelectionPanelProps) {
|
|
const [manualModelId, setManualModelId] = useState("");
|
|
const [modelFilter, setModelFilter] = useState<ModelCapabilityFilter | null>(null);
|
|
|
|
const filteredModels = modelFilter
|
|
? models.filter((model) => capability(model, modelFilter))
|
|
: models;
|
|
const allFilteredModelsEnabled =
|
|
filteredModels.length > 0 && filteredModels.every((model) => model.enabled);
|
|
|
|
function addModel() {
|
|
const modelId = manualModelId.trim();
|
|
if (!modelId || !onAddManual) return;
|
|
onAddManual(modelId);
|
|
setManualModelId("");
|
|
}
|
|
|
|
function toggleFilteredModels() {
|
|
const nextEnabled = !allFilteredModelsEnabled;
|
|
const changedModels = filteredModels.filter((model) => model.enabled !== nextEnabled);
|
|
if (changedModels.length === 0) return;
|
|
onBulkToggle?.(changedModels, nextEnabled);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
<div>
|
|
<div className="font-semibold">Models</div>
|
|
<p className="text-sm text-muted-foreground">{description}</p>
|
|
</div>
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
type="button"
|
|
onClick={toggleFilteredModels}
|
|
disabled={!onBulkToggle || isBulkUpdating || filteredModels.length === 0}
|
|
>
|
|
{allFilteredModelsEnabled ? "Deselect All" : "Select All"}
|
|
</Button>
|
|
{onRefresh ? (
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
type="button"
|
|
onClick={onRefresh}
|
|
disabled={isRefreshing}
|
|
aria-label={refreshLabel}
|
|
>
|
|
<RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
|
|
</Button>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
|
|
{onAddManual ? (
|
|
<div className="flex gap-2">
|
|
<Input
|
|
value={manualModelId}
|
|
onChange={(event) => setManualModelId(event.target.value)}
|
|
onKeyDown={(event) => {
|
|
if (event.key === "Enter") {
|
|
event.preventDefault();
|
|
addModel();
|
|
}
|
|
}}
|
|
placeholder={manualInputPlaceholder}
|
|
/>
|
|
<Button
|
|
size="sm"
|
|
type="button"
|
|
onClick={addModel}
|
|
disabled={isAddingManual || !manualModelId.trim()}
|
|
className="relative min-w-[88px]"
|
|
>
|
|
<span className={isAddingManual ? "opacity-0" : ""}>Add model</span>
|
|
{isAddingManual ? <Spinner size="xs" className="absolute" /> : null}
|
|
</Button>
|
|
</div>
|
|
) : null}
|
|
|
|
{models.length > 0 ? (
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="text-xs font-medium text-muted-foreground">Filter models</span>
|
|
{MODEL_CAPABILITY_FILTERS.map((filter) => {
|
|
const count = models.filter((model) => capability(model, filter.key)).length;
|
|
const isActive = modelFilter === filter.key;
|
|
|
|
return (
|
|
<Button
|
|
key={filter.key}
|
|
type="button"
|
|
variant="secondary"
|
|
size="sm"
|
|
className={`h-7 rounded-full px-3 text-xs ${isActive ? "" : "opacity-80"}`}
|
|
onClick={() => setModelFilter(isActive ? null : filter.key)}
|
|
>
|
|
{filter.label}
|
|
<span className="ml-1 text-muted-foreground">{count}</span>
|
|
</Button>
|
|
);
|
|
})}
|
|
</div>
|
|
) : null}
|
|
|
|
<div className="h-80 overflow-y-auto rounded-xl border bg-muted/20 p-2">
|
|
{models.length === 0 ? (
|
|
<div className="rounded-lg px-3 py-6 text-center text-sm text-muted-foreground">
|
|
{emptyMessage}
|
|
</div>
|
|
) : null}
|
|
{filteredModels.length === 0 && modelFilter ? (
|
|
<div className="rounded-lg px-3 py-6 text-center text-sm text-muted-foreground">
|
|
No{" "}
|
|
{MODEL_CAPABILITY_FILTERS.find(
|
|
(filter) => filter.key === modelFilter
|
|
)?.label.toLowerCase()}{" "}
|
|
models found on this connection.
|
|
</div>
|
|
) : null}
|
|
<div className="space-y-2">
|
|
{filteredModels.map((model) => (
|
|
<div
|
|
key={model.id ?? model.model_id}
|
|
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-colors hover:bg-background"
|
|
>
|
|
<Checkbox
|
|
checked={model.enabled}
|
|
onCheckedChange={(checked) => onToggleModel?.(model, checked === true)}
|
|
disabled={!onToggleModel || isUpdatingModel}
|
|
/>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-center gap-2 text-sm font-medium">
|
|
<span className="truncate">{modelLabel(model)}</span>
|
|
{model.source === "MANUAL" ? (
|
|
<Badge variant="outline" className="text-[10px]">
|
|
manual
|
|
</Badge>
|
|
) : null}
|
|
</div>
|
|
<div className="text-xs text-muted-foreground">
|
|
{capabilityLabels(model) || "No discovered capabilities"}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|