File size: 4,231 Bytes
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
"use client";
import type { ModelFitScore } from "@/lib/modelfit";

function ScoreBar({ label, value }: { label: string; value: number }) {
  const color =
    value >= 85
      ? "bg-emerald-500"
      : value >= 70
        ? "bg-sky-500"
        : value >= 50
          ? "bg-amber-500"
          : "bg-red-500";
  return (
    <div className="space-y-1">
      <div className="flex justify-between text-xs text-zinc-400">
        <span>{label}</span>
        <span className="font-mono">{value}</span>
      </div>
      <div className="h-1.5 rounded bg-zinc-800">
        <div className={`h-full rounded ${color}`} style={{ width: `${value}%` }} />
      </div>
    </div>
  );
}

const LABEL_COLOR: Record<string, string> = {
  "Excellent fit": "text-emerald-400",
  "Recommended": "text-sky-400",
  "Usable with limits": "text-amber-400",
  "Not recommended": "text-orange-400",
  "Does not fit": "text-red-400",
};

const FIT_LEVEL_COLOR: Record<string, string> = {
  comfortable: "text-emerald-400",
  tight: "text-amber-400",
  not_recommended: "text-orange-400",
  impossible: "text-red-400",
};

export function ModelFitScoreCard({
  score,
  showDetails = false,
}: {
  score: ModelFitScore;
  showDetails?: boolean;
}) {
  const re = score.resource_estimate;
  return (
    <div className="rounded-xl border border-zinc-700 bg-zinc-900 p-5 space-y-4">
      {/* Header */}
      <div className="flex items-start justify-between gap-3">
        <div className="space-y-0.5">
          <div className="text-sm font-semibold text-zinc-100 truncate max-w-[200px]" title={score.model_id}>
            {score.model_id.replace(/^(ollama:|hf:|local:)/, "")}
          </div>
          <div className={`text-xs font-mono ${LABEL_COLOR[score.label] ?? "text-zinc-400"}`}>
            {score.label}
          </div>
        </div>
        <div className="text-3xl font-bold font-mono text-zinc-100 shrink-0">
          {Math.round(score.overall_score)}
        </div>
      </div>

      {/* Sub-scores */}
      <div className="space-y-2">
        <ScoreBar label="Hardware fit" value={score.hardware_fit} />
        <ScoreBar label="Speed fit" value={score.speed_fit} />
        <ScoreBar label="RAG fit" value={score.rag_fit} />
        <ScoreBar label="Task fit" value={score.task_fit} />
        <ScoreBar label="Deployment" value={score.deployment_fit} />
      </div>

      {/* Quick facts */}
      <div className="text-xs text-zinc-400 space-y-1">
        <div>
          Quantization:{" "}
          <span className="text-zinc-200 font-mono">{score.best_quantization}</span>
        </div>
        {re && (
          <>
            <div>
              Est. VRAM:{" "}
              <span className="font-mono text-zinc-200">{re.estimated_vram_gb} GB</span>
              {" "}
              <span className={`font-mono ${FIT_LEVEL_COLOR[re.fit_level] ?? ""}`}>
                ({re.fit_level.replace("_", " ")})
              </span>
            </div>
            <div>
              Rec. context:{" "}
              <span className="font-mono text-zinc-200">
                {re.recommended_context.toLocaleString()} tokens
              </span>
            </div>
          </>
        )}
        {score.benchmark?.avg_tok_per_sec != null ? (
          <div>
            Tok/s:{" "}
            <span className="font-mono text-emerald-400">
              {score.benchmark.avg_tok_per_sec} (measured)
            </span>
          </div>
        ) : (
          <div className="text-zinc-500 italic">tok/s: not measured yet</div>
        )}
        {score.estimate_used && (
          <div className="text-amber-500 italic text-[10px]">
            Speed score uses estimates — run benchmark for measured data.
          </div>
        )}
      </div>

      {/* Reason */}
      {showDetails && (
        <p className="text-xs text-zinc-400 border-t border-zinc-800 pt-3">{score.reason}</p>
      )}

      {/* Warnings */}
      {showDetails && score.warnings.length > 0 && (
        <ul className="space-y-1">
          {score.warnings.map((w, i) => (
            <li key={i} className="text-[10px] text-amber-400">
              ⚠ {w}
            </li>
          ))}
        </ul>
      )}
    </div>
  );
}