File size: 5,547 Bytes
e1d8498
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
import { useState, useEffect } from "react";
import { Plus, RefreshCw } from "lucide-react";
import { api } from "../api";
import ModelCard from "./ModelCard";
import ModelForm from "./ModelForm";

export default function ModelManager() {
  const [models, setModels] = useState([]);
  const [loading, setLoading] = useState(true);
  const [showForm, setShowForm] = useState(false);
  const [editModel, setEditModel] = useState(null);
  const [filter, setFilter] = useState("all");

  const load = async () => {
    setLoading(true);
    try {
      const res = await api.listModels();
      setModels(res.data);
    } catch (e) {
      console.error(e);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => { load(); }, []);

  const handleDelete = async (id) => {
    if (!confirm("Delete this model? It will be removed from the gateway.")) return;
    await api.deleteModel(id);
    setModels(m => m.filter(x => x.id !== id));
  };

  const handleToggle = async (id) => {
    const res = await api.toggleModel(id);
    setModels(m => m.map(x => x.id === id ? res.data : x));
  };

  const handleSave = async (data) => {
    if (editModel) {
      const res = await api.updateModel(editModel.id, data);
      setModels(m => m.map(x => x.id === editModel.id ? res.data : x));
    } else {
      const res = await api.createModel(data);
      setModels(m => [res.data, ...m]);
    }
    setShowForm(false);
    setEditModel(null);
  };

  const TYPE_COLORS = {
    chat: "green", embedding: "cyan", image: "purple",
    audio: "orange", completion: "gray",
  };

  const filtered = filter === "all"
    ? models
    : models.filter(m => m.modelType === filter || (filter === "enabled" ? m.enabled : !m.enabled));

  const types = [...new Set(models.map(m => m.modelType))].filter(Boolean);

  return (
    <div className="animate-fade-in">
      {/* ── Header ── */}
      <div className="flex items-center justify-between mb-6">
        <div>
          <h1 className="font-display text-xl font-semibold text-text-primary">
            Model Registry
          </h1>
          <p className="text-text-secondary text-sm mt-0.5">
            Register any AI model and get an OpenAI-compatible endpoint instantly.
          </p>
        </div>
        <div className="flex items-center gap-2">
          <button onClick={load} className="btn-secondary" title="Refresh">
            <RefreshCw size={14} className={loading ? "animate-spin" : ""} />
          </button>
          <button onClick={() => { setEditModel(null); setShowForm(true); }} className="btn-primary">
            <Plus size={14} />
            Add Model
          </button>
        </div>
      </div>

      {/* ── Filters ── */}
      {models.length > 0 && (
        <div className="flex items-center gap-2 mb-4 flex-wrap">
          {["all", "enabled", ...types].map(f => (
            <button
              key={f}
              onClick={() => setFilter(f)}
              className={`badge cursor-pointer transition-all ${
                filter === f
                  ? `badge-${TYPE_COLORS[f] || "green"}`
                  : "badge-gray hover:border-surface-4"
              }`}
            >
              {f}
            </button>
          ))}
        </div>
      )}

      {/* ── Content ── */}
      {loading ? (
        <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
          {[...Array(3)].map((_, i) => (
            <div key={i} className="card p-5 animate-pulse">
              <div className="h-4 bg-surface-3 rounded w-2/3 mb-3" />
              <div className="h-3 bg-surface-3 rounded w-full mb-2" />
              <div className="h-3 bg-surface-3 rounded w-3/4" />
            </div>
          ))}
        </div>
      ) : filtered.length === 0 ? (
        <EmptyState onAdd={() => setShowForm(true)} hasModels={models.length > 0} />
      ) : (
        <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
          {filtered.map(model => (
            <ModelCard
              key={model.id}
              model={model}
              onEdit={() => { setEditModel(model); setShowForm(true); }}
              onDelete={() => handleDelete(model.id)}
              onToggle={() => handleToggle(model.id)}
            />
          ))}
        </div>
      )}

      {/* ── Form modal ── */}
      {showForm && (
        <ModelForm
          initial={editModel}
          onSave={handleSave}
          onClose={() => { setShowForm(false); setEditModel(null); }}
        />
      )}
    </div>
  );
}

function EmptyState({ onAdd, hasModels }) {
  return (
    <div className="card p-12 text-center border-dashed">
      <div className="w-14 h-14 rounded-2xl bg-accent-green/10 border border-accent-green/20
                      flex items-center justify-center mx-auto mb-4">
        <Plus size={24} className="text-accent-green" />
      </div>
      <h3 className="font-display font-semibold text-text-primary mb-1">
        {hasModels ? "No models match this filter" : "No models registered yet"}
      </h3>
      <p className="text-text-secondary text-sm mb-5 max-w-sm mx-auto">
        {hasModels
          ? "Try a different filter to see your models."
          : "Add your first model to start routing AI requests through an OpenAI-compatible endpoint."}
      </p>
      {!hasModels && (
        <button onClick={onAdd} className="btn-primary mx-auto">
          <Plus size={14} />
          Add your first model
        </button>
      )}
    </div>
  );
}