● Zion Boggan
repos/Darwin/face/src/components/surface/HitsInbox.tsx
zionboggan.com ↗
178 lines · tsx
History for this file →
1
"use client";
2
 
3
import type { Hit } from "@/lib/types";
4
 
5
interface HitsInboxProps {
6
  hits: Hit[];
7
}
8
 
9
const ENGINE_LABEL: Record<string, string> = {
10
  rag: "RAG",
11
  "homelab-rag": "RAG",
12
  cbm: "CBM",
13
  "cbm-homelab": "CBM",
14
  serena: "SERENA",
15
  memgraph: "MEMORY",
16
  "memory-graph": "MEMORY",
17
  inbox: "INBOX",
18
  "proxmox-api": "PROXMOX",
19
  proxmox: "PROXMOX",
20
};
21
 
22
function engineLabel(engine: string): string {
23
  return ENGINE_LABEL[engine] ?? engine.toUpperCase();
24
}
25
 
26
function stripMarkdown(s: string): string {
27
  return s
28
    .replace(/```[\s\S]*?```/g, " ")
29
    .replace(/`([^`]*)`/g, "$1")          // inline code
30
    .replace(/^#{1,6}\s*/gm, "")
31
    .replace(/^\s*[-*+]\s+/gm, "· ")
32
    .replace(/\*\*(.*?)\*\*/g, "$1")
33
    .replace(/\*(.*?)\*/g, "$1")
34
    .replace(/_/g, "")
35
    .replace(/>\s?/g, "")
36
    .replace(/\[(.*?)\]\(.*?\)/g, "$1")
37
    .replace(/\s+/g, " ")
38
    .trim();
39
}
40
 
41
function cleanTitle(t: string): string {
42
  return stripMarkdown(t).replace(/^\d+\.\s*/, "");
43
}
44
 
45
function shortPath(p: string): string {
46
  if (!p) return "";
47
  if (p.startsWith("memory://") || p.includes("://")) {
48
    return p.replace(/^[a-z]+:\/\
49
  }
50
  const parts = p.split("/").filter(Boolean);
51
  if (parts.length <= 2) return p;
52
  return "…/" + parts.slice(-2).join("/");
53
}
54
 
55
function scoreTone(score: number): { label: string; color: string } {
56
  if (score >= 0.45) return { label: "strong", color: "#16e0c8" };
57
  if (score >= 0.3) return { label: "fair", color: "#0d9e8e" };
58
  return { label: "weak", color: "#4a525e" };
59
}
60
 
61
function HitCard({ hit, index, max }: { hit: Hit; index: number; max: number }) {
62
  const title = cleanTitle(hit.title) || shortPath(hit.path);
63
  const snippet = stripMarkdown(hit.snippet);
64
  const tone = scoreTone(hit.score);
65
  const barPct = max > 0 ? Math.max(8, Math.round((hit.score / max) * 100)) : 0;
66
 
67
  return (
68
    <article
69
      className="group py-3.5 px-3 -mx-1 border-b border-surface-2 last:border-0 hover:bg-surface-1/60 transition-colors duration-150"
70
      style={{ animationDelay: `${index * 50}ms` }}
71
    >
72
      {}
73
      <div className="flex items-center justify-between gap-3 mb-2">
74
        <span
75
          className="font-mono uppercase shrink-0"
76
          style={{
77
            fontSize: "0.56rem",
78
            letterSpacing: "0.1em",
79
            color: "#16e0c8",
80
            border: "1px solid rgba(22,224,200,0.22)",
81
            padding: "2px 6px",
82
            borderRadius: 2,
83
          }}
84
        >
85
          {engineLabel(hit.engine)}
86
        </span>
87
        <div className="flex items-center gap-2 shrink-0">
88
          <div className="w-14 h-1 bg-surface-3 relative rounded-full overflow-hidden">
89
            <div
90
              className="absolute left-0 top-0 h-full rounded-full"
91
              style={{ width: `${barPct}%`, background: tone.color, transition: "width 0.5s cubic-bezier(0.16,1,0.3,1)" }}
92
            />
93
          </div>
94
          <span className="font-mono tabular-nums" style={{ fontSize: "0.62rem", color: tone.color }} title={`relevance score ${tone.label}`}>
95
            {hit.score.toFixed(2)}
96
          </span>
97
        </div>
98
      </div>
99
 
100
      {}
101
      <p className="font-ui text-sm text-text-primary font-medium leading-snug mb-1 line-clamp-2">
102
        {title}
103
      </p>
104
 
105
      {}
106
      <p className="font-mono truncate mb-1.5" style={{ fontSize: "0.56rem", color: "#4a525e", letterSpacing: "0.02em" }} title={hit.path}>
107
        {shortPath(hit.path)}
108
      </p>
109
 
110
      {}
111
      <p className="font-ui text-text-secondary leading-relaxed line-clamp-2" style={{ fontSize: "0.8rem" }}>
112
        {snippet}
113
      </p>
114
    </article>
115
  );
116
}
117
 
118
function EmptyInbox() {
119
  return (
120
    <div className="flex-1 flex items-center justify-center py-12">
121
      <div className="text-center">
122
        <div className="w-10 h-10 mx-auto mb-3 border border-surface-2 flex items-center justify-center">
123
          <svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
124
            <rect x="2" y="4" width="12" height="9" stroke="#3a3a3a" strokeWidth="1" />
125
            <path d="M2 7h12" stroke="#3a3a3a" strokeWidth="1" />
126
            <path d="M5 4V3M11 4V3" stroke="#3a3a3a" strokeWidth="1" />
127
          </svg>
128
        </div>
129
        <p className="font-mono text-label-sm text-text-muted uppercase tracking-widest">No hits yet</p>
130
        <p className="font-mono text-label-sm text-text-muted mt-1 opacity-50">Ask Darwin a question</p>
131
      </div>
132
    </div>
133
  );
134
}
135
 
136
export default function HitsInbox({ hits }: HitsInboxProps) {
137
  const sorted = [...hits].sort((a, b) => b.score - a.score);
138
  const max = sorted.length ? sorted[0].score : 0;
139
 
140
  return (
141
    <div className="flex flex-col gap-4 h-full">
142
      <div className="flex items-center justify-between">
143
        <div>
144
          <p className="font-mono text-label-sm text-text-muted uppercase tracking-widest mb-1">Raw hits</p>
145
          {sorted.length > 0 && (
146
            <p className="font-mono text-label-sm text-text-secondary">
147
              {sorted.length} results · top {max.toFixed(2)}
148
            </p>
149
          )}
150
        </div>
151
        {sorted.length > 0 && (
152
          <div className="flex items-center gap-1.5">
153
            <span className="w-1.5 h-1.5 rounded-full bg-teal-bio animate-pulse-slow" />
154
            <span className="font-mono text-label-sm text-teal-bio">LIVE</span>
155
          </div>
156
        )}
157
      </div>
158
 
159
      <div className="flex-1 overflow-y-auto">
160
        {sorted.length === 0 ? (
161
          <EmptyInbox />
162
        ) : (
163
          <div>
164
            {sorted.map((hit, i) => (
165
              <HitCard key={`${hit.engine}-${i}`} hit={hit} index={i} max={max} />
166
            ))}
167
          </div>
168
        )}
169
      </div>
170
 
171
      {sorted.length > 0 && (
172
        <div className="pt-2 border-t border-surface-2">
173
          <p className="font-mono text-label-sm text-text-muted">Sorted by relevance score</p>
174
        </div>
175
      )}
176
    </div>
177
  );
178
}