File size: 14,809 Bytes
8c1b9fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f1969b
8c1b9fe
 
 
 
 
9f1969b
 
8c1b9fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f1969b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8c1b9fe
 
 
 
9f1969b
 
 
8c1b9fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f1969b
8c1b9fe
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9f1969b
8c1b9fe
 
 
 
 
 
 
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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
"use client";
import type { DiscoverEntry, DiscoverHardware, DiscoverResult, GPUInfo } from "@/lib/modelfit";
import { verdictMeta } from "@/components/modelfit/verdict";

// ── Small building blocks ─────────────────────────────────────────────────────

function Chip({ label, tone = "zinc" }: { label: string; tone?: "zinc" | "green" | "sky" | "amber" }) {
  const tones = {
    zinc: "bg-zinc-800 text-zinc-300",
    green: "bg-emerald-900/50 text-emerald-300",
    sky: "bg-sky-900/50 text-sky-300",
    amber: "bg-amber-900/50 text-amber-300",
  };
  return <span className={`px-1.5 py-0.5 rounded text-[10px] font-mono ${tones[tone]}`}>{label}</span>;
}

function Stat({ label, value, accent }: { label: string; value: React.ReactNode; accent?: string }) {
  return (
    <div>
      <div className="text-[10px] uppercase tracking-wider text-zinc-500">{label}</div>
      <div className={`text-sm font-mono ${accent ?? "text-zinc-100"}`}>{value}</div>
    </div>
  );
}

function backendTone(b: string) {
  return b === "cuda"
    ? "bg-emerald-900/50 text-emerald-300 ring-emerald-600/40"
    : b === "metal"
      ? "bg-sky-900/50 text-sky-300 ring-sky-600/40"
      : b === "rocm"
        ? "bg-purple-900/50 text-purple-300 ring-purple-600/40"
        : "bg-zinc-800 text-zinc-300 ring-zinc-600/40";
}

/** Per-GPU VRAM bar: used (other processes) + free. */
function GpuBar({ gpu }: { gpu: GPUInfo }) {
  const total = gpu.vram_gb || 0;
  const free = gpu.vram_free_gb ?? null;
  const usedPct = total > 0 && free != null ? Math.max(0, Math.min(100, ((total - free) / total) * 100)) : 0;
  return (
    <div className="space-y-1">
      <div className="flex items-center justify-between text-xs">
        <span className="text-zinc-300 truncate">{gpu.name}</span>
        <span className="text-zinc-400 font-mono shrink-0 ml-2">
          {total} GB{gpu.integrated ? " unified" : ""}
        </span>
      </div>
      {free != null && (
        <>
          <div className="h-1.5 rounded bg-zinc-800 overflow-hidden">
            <div className="h-full bg-zinc-500" style={{ width: `${usedPct}%` }} />
          </div>
          <div className="text-[10px] text-zinc-500 font-mono">
            {free} GB free Β· {(total - free).toFixed(1)} GB in use
          </div>
        </>
      )}
    </div>
  );
}

/** Usage gauge for the recommended model vs available memory, with 80% marker. */
function UsageGauge({ entry, hardware }: { entry: DiscoverEntry; hardware: DiscoverHardware }) {
  const re = entry.resource_estimate;
  if (!re) return null;
  const onGpu = hardware.total_vram_gb > 0;
  const capacity = onGpu ? hardware.total_vram_gb : hardware.ram_gb;
  const model = re.estimated_vram_gb;
  const pct = capacity > 0 ? Math.min(100, (model / capacity) * 100) : 0;
  const meta = verdictMeta(re.verdict);
  const freeNow = onGpu ? hardware.total_vram_free_gb ?? null : null;

  return (
    <div className="space-y-1.5">
      <div className="flex items-baseline justify-between">
        <span className="text-[10px] uppercase tracking-wider text-zinc-500">
          Estimated {onGpu ? "VRAM" : "RAM"} use
        </span>
        <span className="text-xs font-mono text-zinc-300">
          {model} / {capacity} GB
        </span>
      </div>
      <div className="relative h-3 rounded-full bg-zinc-800 overflow-hidden">
        <div className={`h-full rounded-full ${meta.barClass}`} style={{ width: `${pct}%` }} />
        {/* 80% headroom marker */}
        <div className="absolute top-0 bottom-0 w-px bg-zinc-400/70" style={{ left: "80%" }} title="80% headroom target" />
      </div>
      <div className="text-[10px] text-zinc-500 font-mono">
        {re.headroom_gb != null && re.headroom_gb >= 0
          ? `${re.headroom_gb} GB headroom`
          : "exceeds capacity"}
        {freeNow != null && ` Β· ${freeNow} GB free right now`}
        {" Β· dashed line = 80% target"}
      </div>
    </div>
  );
}

// ── Plain-language summary ────────────────────────────────────────────────────

function gpuSummary(gpus: GPUInfo[], totalVram: number): string {
  if (gpus.length === 0) return "no GPU";
  const first = gpus[0].name.replace(/NVIDIA GeForce |NVIDIA /i, "");
  const prefix = gpus.length > 1 ? `${gpus.length}Γ— ${first}` : first;
  return `${prefix} (${totalVram} GB VRAM)`;
}

function buildSummary(hardware: DiscoverHardware, entry: DiscoverEntry): string {
  const re = entry.resource_estimate;
  const name = (entry.model_meta.display_name || entry.model_id).replace(/^(ollama:|hf:|local:)/, "");
  const hw = hardware.total_vram_gb > 0 ? gpuSummary(hardware.gpus, hardware.total_vram_gb) : `${hardware.ram_gb} GB RAM (CPU)`;
  if (!re) return `${name} is the best match for your ${hw}.`;
  const v = re.verdict;
  const use = `~${re.estimated_vram_gb} GB at ${entry.best_quantization}`;
  if (v === "runs_great") return `Your ${hw} runs ${name} comfortably β€” ${use}, ${re.headroom_gb} GB to spare.`;
  if (v === "runs_ok") return `Your ${hw} runs ${name} β€” ${use}, a tight but workable fit.`;
  if (v === "runs_offload")
    return `${name} needs ${use} β€” more than your ${hw}. It runs by offloading layers to system RAM (slower). A smaller model or lower quant would run fully on the GPU.`;
  if (v === "runs_cpu") return `${name} runs on your CPU β€” ${use} in RAM, ${re.headroom_gb} GB to spare.`;
  if (v === "runs_cpu_tight") return `${name} runs on your CPU but leaves little RAM headroom (${use}).`;
  return `${name} needs ${use} β€” too large for this machine. Pick a smaller model below.`;
}

// ── Main report ───────────────────────────────────────────────────────────────

export function SystemReport({
  result,
  onPull,
  onRefresh,
  loading,
  hosted = false,
}: {
  result: DiscoverResult;
  onPull: (entry: DiscoverEntry) => void;
  onRefresh: () => void;
  loading: boolean;
  /** True on a hosted demo (HF Space): the probe reads the SERVER, not the visitor's device. */
  hosted?: boolean;
}) {
  const hw = result.hardware;
  const top = result.recommendations[0] as DiscoverEntry | undefined;
  const onGpu = hw.total_vram_gb > 0;
  const meta = top?.resource_estimate ? verdictMeta(top.resource_estimate.verdict) : null;

  return (
    <div className="rounded-2xl border border-zinc-700 bg-gradient-to-b from-zinc-900 to-zinc-900/60 overflow-hidden">
      {/* Header */}
      <div className="flex items-center justify-between px-6 py-4 border-b border-zinc-800">
        <div>
          <h2 className="text-base font-semibold text-zinc-100">Your System Report</h2>
          <p className="text-xs text-zinc-500 mt-0.5">
            Detected hardware and the best-fitting model, ranked for {result.task ?? "your tasks"}.
          </p>
        </div>
        <button
          onClick={onRefresh}
          disabled={loading}
          className="flex items-center gap-1.5 px-3 py-1.5 rounded-lg border border-zinc-700 text-xs text-zinc-400 hover:text-zinc-200 hover:border-zinc-500 transition-colors disabled:opacity-40"
        >
          <span className={loading ? "animate-spin" : ""}>⟳</span>
          {loading ? "Scanning…" : "Re-scan"}
        </button>
      </div>

      {/* Hosted-demo notice: the probe reads the Space's server, not the visitor's device. */}
      {hosted && (
        <div className="mx-6 mt-4 rounded-lg border border-amber-600/40 bg-amber-900/20 px-3.5 py-2.5 text-[11px] leading-relaxed text-amber-200/90">
          <span className="font-semibold text-amber-200">You&apos;re on the hosted demo.</span>{" "}
          This report profiles the <span className="font-semibold">Space&apos;s server</span>{" "}
          ({hw.os}
          {hw.arch ? ` Β· ${hw.arch}` : ""}) β€” not your own device. Models shown here would run on the
          server, not on your machine. To profile your Mac/PC and pull models onto it, run Auralynq
          locally (
          <a
            href="https://github.com/MHHamdan/Auralynq"
            target="_blank"
            rel="noopener noreferrer"
            className="underline hover:text-amber-100"
          >
            one command
          </a>
          ).
        </div>
      )}

      <div className="grid grid-cols-1 lg:grid-cols-2 divide-y lg:divide-y-0 lg:divide-x divide-zinc-800">
        {/* ── This machine ── */}
        <div className="p-6 space-y-4">
          <div className="flex items-center gap-2">
            <span className="text-[10px] font-semibold uppercase tracking-wider text-zinc-500">
              {hosted ? "Server (hosted demo)" : "This machine"}
            </span>
            <span className={`px-2 py-0.5 rounded-full text-[10px] font-mono font-semibold ring-1 ${backendTone(hw.best_backend)}`}>
              {hw.best_backend.toUpperCase()}
            </span>
          </div>

          <div className="grid grid-cols-2 gap-y-3 gap-x-4">
            <Stat label="OS" value={`${hw.os}${hw.arch ? ` Β· ${hw.arch}` : ""}`} />
            <Stat label="RAM" value={`${hw.ram_gb} GB`} accent="text-zinc-100 font-semibold" />
            <div className="col-span-2">
              <Stat label="CPU" value={hw.cpu || "unknown"} />
              <div className="flex flex-wrap gap-1.5 mt-1">
                {hw.cpu_cores_physical != null && (
                  <Chip label={`${hw.cpu_cores_physical}P / ${hw.cpu_cores_logical}L`} />
                )}
                {hw.avx2 && <Chip label="AVX2" tone="green" />}
                {hw.avx512 && <Chip label="AVX-512" tone="green" />}
              </div>
            </div>
          </div>

          {/* GPUs */}
          <div className="space-y-2 pt-1">
            <div className="flex items-center justify-between">
              <span className="text-[10px] uppercase tracking-wider text-zinc-500">
                {onGpu ? `GPU${hw.gpus.length > 1 ? `s Β· ${hw.gpus.length}` : ""}` : "GPU"}
              </span>
              {onGpu && (
                <span className="text-xs font-mono text-emerald-400 font-semibold">
                  {hw.total_vram_gb} GB total
                  {hw.total_vram_free_gb != null && ` Β· ${hw.total_vram_free_gb} free`}
                </span>
              )}
            </div>
            {hw.gpus.length === 0 ? (
              <p className="text-xs text-zinc-500">
                None detected β€” inference will use the CPU.
                {hw.in_container && " (Running in a container; host GPUs may be hidden.)"}
              </p>
            ) : (
              <div className="space-y-2.5">
                {hw.gpus.slice(0, 4).map((g, i) => (
                  <GpuBar key={i} gpu={g} />
                ))}
              </div>
            )}
          </div>
        </div>

        {/* ── Top pick ── */}
        <div className="p-6 space-y-4">
          <span className="text-[10px] font-semibold uppercase tracking-wider text-zinc-500">
            {hosted ? "Best pick for this server" : "Best pick for you"}
          </span>

          {!top ? (
            <p className="text-sm text-zinc-500">No models matched this machine and task.</p>
          ) : (
            <>
              {/* Model identity */}
              <div className="flex items-start justify-between gap-3">
                <div className="min-w-0">
                  <div className="text-lg font-semibold text-zinc-100 truncate">
                    {(top.model_meta.display_name || top.model_id).replace(/^(ollama:|hf:|local:)/, "")}
                  </div>
                  <div className="flex items-center gap-2 mt-0.5 text-[11px] text-zinc-500">
                    <span className="uppercase">{top.source}</span>
                    {top.model_meta.parameter_count_b != null && <span>Β· {top.model_meta.parameter_count_b}B</span>}
                    <span>Β· {top.best_quantization}</span>
                    <span>Β· score {Math.round(top.overall_score)}</span>
                  </div>
                </div>
                {top.already_installed && (
                  <span className="shrink-0 flex items-center gap-1 text-xs text-emerald-400 font-medium">
                    <span className="w-1.5 h-1.5 rounded-full bg-emerald-400" /> Installed
                  </span>
                )}
              </div>

              {/* Verdict badge */}
              {meta && (
                <div className={`inline-flex items-center gap-2 rounded-lg px-3 py-1.5 ${meta.badgeClass}`}>
                  <meta.icon className="h-4 w-4 shrink-0" aria-hidden />
                  <span className="text-sm font-semibold">{meta.label}</span>
                </div>
              )}

              {/* Usage gauge */}
              <UsageGauge entry={top} hardware={hw} />

              {/* Quick stats */}
              <div className="grid grid-cols-3 gap-3 pt-1">
                {top.resource_estimate && (
                  <>
                    <Stat label="Download" value={`${top.resource_estimate.estimated_disk_gb} GB`} />
                    <Stat label="RAM need" value={`${top.resource_estimate.estimated_ram_gb} GB`} />
                    <Stat
                      label="Max context"
                      value={`${(top.resource_estimate.recommended_context / 1024).toFixed(0)}K`}
                    />
                  </>
                )}
              </div>

              {/* Plain-language summary */}
              <p className="text-xs text-zinc-400 leading-relaxed">{buildSummary(hw, top)}</p>

              {/* CTA */}
              {!top.already_installed && top.pull_command && (
                <button
                  onClick={() => onPull(top)}
                  className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-xl bg-sky-600 hover:bg-sky-500 text-sm text-white font-semibold transition-colors"
                >
                  {top.source === "ollama" ? "↓ Pull this model" : "⎘ Get download command"}
                </button>
              )}
            </>
          )}
        </div>
      </div>

      {/* Footer */}
      <div className="px-6 py-2.5 border-t border-zinc-800 text-[11px] text-zinc-500 flex items-center gap-2">
        <span>
          <span className="text-zinc-300 font-medium">{result.total_candidates}</span> models scored against
          {hosted ? " the server's hardware" : " your hardware"}
        </span>
        <span className="text-zinc-700">Β·</span>
        <span>full ranking below ↓</span>
      </div>
    </div>
  );
}