File size: 4,328 Bytes
2c073e0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5217381
 
 
2c073e0
 
 
 
 
 
 
 
 
 
5217381
 
2c073e0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3857d4d
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
const catalogSummary = document.getElementById("catalog-summary");
const providerFilterBar = document.getElementById("provider-filter-bar");
const providerGrid = document.getElementById("provider-grid");
const catalogUpdated = document.getElementById("catalog-updated");
const catalogEmpty = document.getElementById("catalog-empty");

let catalogState = {
  providers: [],
  activeProvider: "all",
};

const DISPLAY_TIMEZONE = "Asia/Shanghai";

const dateTimeFormatter = new Intl.DateTimeFormat("zh-CN", {
  month: "2-digit",
  day: "2-digit",
  hour: "2-digit",
  minute: "2-digit",
  hour12: false,
  timeZone: DISPLAY_TIMEZONE,
});

function formatDateTime(value) {
  if (!value) return "--";
  const date = new Date(value);
  if (Number.isNaN(date.getTime())) return "--";
  return dateTimeFormatter.format(date);
}

function createSummaryCard(label, value, detail = "") {
  const card = document.createElement("article");
  card.className = "summary-card";
  card.innerHTML = `<span>${label}</span><strong>${value}</strong><p>${detail}</p>`;
  return card;
}

function renderSummary(data) {
  catalogSummary.innerHTML = "";
  catalogSummary.appendChild(createSummaryCard("官方模型总数", data.total_models ?? 0, "来自 NVIDIA 官方 /v1/models"));
  catalogSummary.appendChild(createSummaryCard("提供商数量", data.providers?.length ?? 0, "按模型 ID 中的提供商前缀自动归类"));
}

function renderFilterBar() {
  providerFilterBar.innerHTML = "";
  const options = [{ provider: "all", count: catalogState.providers.reduce((sum, item) => sum + (item.count || 0), 0), label: "全部" }].concat(
    catalogState.providers.map((group) => ({ provider: group.provider, count: group.count, label: group.provider }))
  );

  options.forEach((option) => {
    const button = document.createElement("button");
    button.className = `provider-filter-btn ${catalogState.activeProvider === option.provider ? "active" : ""}`;
    button.type = "button";
    button.textContent = `${option.label} (${option.count})`;
    button.addEventListener("click", () => {
      catalogState.activeProvider = option.provider;
      renderFilterBar();
      renderProviders();
    });
    providerFilterBar.appendChild(button);
  });
}

function renderProviders() {
  providerGrid.innerHTML = "";
  const filtered = catalogState.activeProvider === "all"
    ? catalogState.providers
    : catalogState.providers.filter((group) => group.provider === catalogState.activeProvider);

  if (filtered.length === 0) {
    catalogEmpty.textContent = "当前筛选条件下没有可展示的模型。";
    return;
  }
  catalogEmpty.textContent = "";

  filtered.forEach((group) => {
    const card = document.createElement("article");
    card.className = "provider-card provider-card-fixed";
    card.innerHTML = `

      <div class="provider-card-head">

        <div>

          <h3>${group.provider}</h3>

          <p>${group.count} 个模型</p>

        </div>

      </div>

    `;

    const body = document.createElement("div");
    body.className = "provider-card-body";

    const list = document.createElement("div");
    list.className = "provider-model-list";
    (group.models || []).forEach((model) => {
      const chip = document.createElement("div");
      chip.className = "provider-model-chip";
      chip.title = model.id;
      chip.textContent = model.id;
      list.appendChild(chip);
    });

    body.appendChild(list);
    card.appendChild(body);
    providerGrid.appendChild(card);
  });
}

async function loadCatalog() {
  const response = await fetch("/api/catalog", { headers: { Accept: "application/json" } });
  if (!response.ok) {
    throw new Error("模型列表加载失败");
  }
  const payload = await response.json();
  catalogUpdated.textContent = formatDateTime(payload.synced_at || payload.generated_at);
  catalogState.providers = payload.providers || [];
  renderSummary(payload);
  renderFilterBar();
  renderProviders();
}

window.addEventListener("DOMContentLoaded", async () => {
  try {
    await loadCatalog();
  } catch (error) {
    catalogEmpty.textContent = error.message;
    providerGrid.innerHTML = "";
    providerFilterBar.innerHTML = "";
  }
});