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>
  );
}