SPB Git forge

spb/ai-atlas

Public
41commits 1branches 0releases
4.6 MBsize
maindefault branch
12 days agolast push
HTML 77.2% TypeScript 10.5% Python 9.6% JavaScript 2.5%
3.2 KB · 80 lines tsx
Raw Blame History
1import type { ReactNode } from 'react';2import { Evidence } from '@/components/evidence/evidence';3import { cn } from '@/lib/cn';4import { fmtValue } from '@/lib/format';5import { propertyLabel, URL_KEYS } from '@/lib/site';6import type { Provenance } from '@/lib/types';7import { ProvenanceInline } from './provenance';8import { Missing } from './unavailable';910export type KVRow = { key: string; label?: ReactNode; value?: ReactNode; raw?: unknown; hint?: ReactNode; unit?: string | null };1112/**13 * Dense spec list (<dl>): label column + value + optional per-field provenance. Pass `provenance` (from EntityDetail)14 * to render "Source · tier · observed" under each value when known. Values fall back to `fmtValue(raw, key)`.15 * Pass `slug` (+ optional `entity`) to make every value with provenance an **evidence trigger** that opens the drawer16 * (`components/evidence`). Existing call sites without `slug` render exactly as before.17 */18export function KeyValue({19  rows,20  provenance,21  className,22  dense = false,23  showMissing = false,24  slug,25  entity,26}: {27  rows: KVRow[];28  provenance?: Provenance;29  className?: string;30  dense?: boolean;31  showMissing?: boolean;32  slug?: string;33  entity?: { name?: string; entity_type?: string } | null;34}) {35  const shown = rows.filter((r) => showMissing || r.value !== undefined || (r.raw !== undefined && r.raw !== null && r.raw !== ''));36  if (shown.length === 0) return <p className="text-sm text-ink-3">No structured attributes yet.</p>;37  return (38    <dl className={cn('kv', dense && '[&>div]:py-1.5', className)}>39      {shown.map((r) => {40        const p = provenance?.[r.key];41        const raw = r.raw;42        let value: ReactNode = r.value;43        let display: string | undefined;44        if (value === undefined) {45          if (raw === null || raw === undefined || raw === '') value = <Missing />;46          else if (URL_KEYS.has(r.key) && typeof raw === 'string' && /^https?:\/\//.test(raw))47            value = (48              <a href={raw} className="link break-all" rel="noopener noreferrer" target="_blank">49                {raw.replace(/^https?:\/\/(www\.)?/, '').replace(/\/$/, '')}50              </a>51            );52          else {53            display = fmtValue(raw, r.key);54            value = <span className={typeof raw === 'number' ? 'tnum' : undefined}>{display}</span>;55          }56        }57        const canOpen = !!slug && !!p && raw !== null && raw !== undefined && raw !== '' && !(URL_KEYS.has(r.key) && typeof raw === 'string');58        return (59          <div key={r.key}>60            <dt>{r.label ?? propertyLabel(r.key)}</dt>61            <dd>62              <div className="text-ink">63                {canOpen ? (64                  <Evidence slug={slug as string} property={r.key} value={raw} display={display} unit={r.unit ?? p?.unit ?? null} fallback={p} entity={entity ?? null}>65                    {value}66                  </Evidence>67                ) : (68                  value69                )}70              </div>71              {r.hint && <div className="mt-0.5 text-xs text-ink-3">{r.hint}</div>}72              {p && <ProvenanceInline p={p} className="mt-0.5" slug={slug} property={r.key} value={raw} entity={entity ?? null} />}73            </dd>74          </div>75        );76      })}77    </dl>78  );79}80