File size: 3,416 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
import { Citation } from "@/lib/api";
import { displaySource } from "@/lib/format";
import { citationColor, scoreStrength, STRENGTH_META } from "@/lib/citations";

// Clean, user-facing locator (page / speaker+timestamp) — never raw char spans.
function cleanLocator(c: Citation): string {
  if (c.speaker || c.start_s != null) {
    const t = (s?: number | null) => {
      if (s == null) return "";
      const m = Math.floor(s / 60);
      const sec = Math.round(s % 60);
      return `${m}:${String(sec).padStart(2, "0")}`;
    };
    const range = c.start_s != null ? `${t(c.start_s)}${t(c.end_s)}` : "";
    return [c.speaker, range].filter(Boolean).join(" · ");
  }
  if (c.page != null) return `p.${c.page}`;
  return "";
}

function rgba(rgb: string, a: number): string {
  return rgb.replace("rgb(", "rgba(").replace(")", `,${a})`);
}

export function Citations({
  citations,
  onOpenSource,
}: {
  citations: Citation[];
  onOpenSource?: (marker: number) => void;
}) {
  if (!citations?.length) return null;
  return (
    <div className="mt-3 space-y-1.5">
      <div className="overline text-fg3">
        Sources · {citations.length}
      </div>
      <ol className="space-y-1.5">
        {citations.map((c) => {
          const loc = cleanLocator(c);
          const color = citationColor(c.marker);
          const strength = scoreStrength(c.score);
          const sm = strength ? STRENGTH_META[strength] : null;
          const clickable = !!onOpenSource;
          const Tag = clickable ? "button" : "div";
          return (
            <li key={c.marker}>
              <Tag
                {...(clickable
                  ? {
                      type: "button" as const,
                      onClick: () => onOpenSource!(c.marker),
                      "aria-label": `Open source ${c.marker}: ${displaySource(c.source)}`,
                    }
                  : {})}
                className={`flex w-full items-center gap-2.5 rounded-lg border border-edge bg-panel2 px-2.5 py-1.5 text-left text-sm transition ${
                  clickable ? "hover:border-edge2 hover:bg-panel" : ""
                }`}
              >
                <span
                  className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-xs font-semibold"
                  style={{ color, backgroundColor: rgba(color, 0.15) }}
                >
                  {c.marker}
                </span>
                <span className="min-w-0 flex-1 truncate text-fg" title={displaySource(c.source)}>
                  {displaySource(c.source)}
                  {loc && <span className="ml-1 font-mono text-xs text-fg3">· {loc}</span>}
                </span>
                {sm && c.score != null && (
                  <span className="inline-flex shrink-0 items-center gap-1" title={`${sm.label} · retrieval score ${c.score.toFixed(2)}`}>
                    <span className={`h-1.5 w-1.5 rounded-full ${sm.dot}`} aria-hidden />
                    <span className="font-mono text-xs text-fg3">{c.score.toFixed(2)}</span>
                  </span>
                )}
                {c.method && (
                  <span className="tag hidden shrink-0 font-mono text-[10px] uppercase sm:inline-flex">
                    {c.method}
                  </span>
                )}
              </Tag>
            </li>
          );
        })}
      </ol>
    </div>
  );
}