Spaces:
Sleeping
Sleeping
File size: 4,739 Bytes
30cd0c9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | import { useEffect, useMemo, useState } from "react";
import { Check, Database, FileText, Loader2, RefreshCw } from "lucide-react";
import {
getDatabaseClients,
getDocuments,
type DataBindItem,
} from "@/services/orchestrationApi";
import { getCurrentSession } from "./session";
import { cx, uniqueById } from "./utils";
interface DataBindSelectorProps {
value: DataBindItem[];
onChange: (items: DataBindItem[]) => void;
disabled?: boolean;
}
export function DataBindSelector({ value, onChange, disabled }: DataBindSelectorProps) {
const [options, setOptions] = useState<DataBindItem[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const selected = useMemo(() => new Set(value.map((item) => item.id)), [value]);
const loadOptions = async () => {
const session = getCurrentSession();
if (!session?.user_id) return;
setLoading(true);
setError(null);
try {
const [docs, dbs] = await Promise.all([getDocuments(session.user_id), getDatabaseClients(session.user_id)]);
const docItems = docs
.filter((doc) => ["processed", "completed", "uploaded", "processing"].includes(doc.status))
.map<DataBindItem>((doc) => ({
id: doc.id,
name: doc.filename,
group_type: "document",
type: doc.file_type,
}));
const dbItems = dbs
.filter((db) => db.status !== "inactive")
.map<DataBindItem>((db) => ({
id: db.id,
name: db.name,
group_type: "database",
type: db.db_type,
}));
setOptions(uniqueById([...docItems, ...dbItems]));
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to load sources");
} finally {
setLoading(false);
}
};
useEffect(() => {
loadOptions();
}, []);
const toggle = (item: DataBindItem) => {
if (disabled) return;
if (selected.has(item.id)) {
onChange(value.filter((existing) => existing.id !== item.id));
return;
}
onChange([...value, item]);
};
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<label className="text-xs font-medium text-slate-600">Data binding</label>
<button
type="button"
onClick={loadOptions}
disabled={disabled || loading}
className="inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-slate-600 hover:bg-slate-100 disabled:opacity-40"
>
{loading ? <Loader2 className="h-3 w-3 animate-spin" /> : <RefreshCw className="h-3 w-3" />}
Refresh
</button>
</div>
{error && <p className="rounded-md bg-red-50 px-3 py-2 text-xs text-red-700">{error}</p>}
<div className="max-h-56 space-y-2 overflow-y-auto rounded-lg border border-slate-200 bg-white p-2">
{loading ? (
<div className="flex items-center justify-center gap-2 py-8 text-sm text-slate-500">
<Loader2 className="h-4 w-4 animate-spin" />
Loading sources
</div>
) : options.length === 0 ? (
<div className="py-8 text-center text-sm text-slate-500">No eligible sources yet.</div>
) : (
options.map((item) => {
const isSelected = selected.has(item.id);
const Icon = item.group_type === "database" ? Database : FileText;
return (
<button
type="button"
key={`${item.group_type}-${item.id}`}
onClick={() => toggle(item)}
disabled={disabled}
className={cx(
"flex w-full items-center gap-3 rounded-md border px-3 py-2 text-left transition disabled:opacity-50",
isSelected ? "border-emerald-200 bg-emerald-50" : "border-transparent hover:border-slate-200 hover:bg-slate-50"
)}
>
<span className="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md bg-slate-100 text-slate-600">
<Icon className="h-4 w-4" />
</span>
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-medium text-slate-800">{item.name}</span>
<span className="block text-xs capitalize text-slate-500">{item.group_type} / {item.type}</span>
</span>
{isSelected && <Check className="h-4 w-4 text-emerald-600" />}
</button>
);
})
)}
</div>
{value.length === 0 && <p className="text-xs text-red-600">Select at least one source.</p>}
</div>
);
}
|