File size: 5,131 Bytes
a45ece3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3da97ef
 
 
a45ece3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
98feb0f
 
 
 
 
 
 
 
 
 
 
 
a45ece3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import { useMemo, useState } from 'react'
import { useAppStore } from '@/store/appStore'
import type { VerdictLabel } from '@/lib/types'
import { confidencePercent, verdictColor } from '@/lib/utils'

export function ClaimsTable() {
  const run = useAppStore((s) => s.runs.find((r) => r.id === s.currentRunId))
  const filter = useAppStore((s) => s.ui.filter)
  const setFilter = useAppStore((s) => s.setFilter)
  const openEvidence = useAppStore((s) => s.openEvidence)

  const [sortAsc, setSortAsc] = useState<boolean>(false)

  const rows = useMemo(() => {
    if (!run?.report) return []
    const { claims, verdicts, evidence } = run.report
    const byClaim: Record<string, any> = {}
    for (const c of claims) byClaim[c.id] = { claim: c }
    for (const v of verdicts) byClaim[v.claim_id] = { ...(byClaim[v.claim_id] || {}), verdict: v }

    let data = Object.values(byClaim)
    if (filter !== 'all') {
      data = data.filter((r: any) => r.verdict?.label === filter)
    }
    data.sort((a: any, b: any) => (sortAsc ? (a.verdict?.confidence ?? 0) - (b.verdict?.confidence ?? 0) : (b.verdict?.confidence ?? 0) - (a.verdict?.confidence ?? 0)))

    return data.map((r: any) => {
      const cited = r.verdict?.citation_ids as string[] | undefined
      const firstId = (cited && cited[0]) || r.verdict?.best_evidence_id
      const evid = firstId ? evidence.find((e) => e.doc_id === firstId) : undefined
      return { ...r, evidence: evid }
    })
  }, [run, filter, sortAsc])

  if (!run?.report) return null

  const filters: ('all' | VerdictLabel)[] = ['all', 'supported', 'refuted', 'insufficient']

  return (
    <div className="bg-card text-card-foreground rounded-lg border">
      <div className="p-4 flex items-center justify-between">
        <h3 className="font-medium">Claims</h3>
        <div className="flex items-center gap-2">
          {filters.map((f) => (
            <button
              key={f}
              onClick={() => setFilter(f)}
              className={`text-xs px-2 py-1 rounded-md border ${filter === f ? 'bg-accent' : 'hover:bg-muted'}`}
            >
              {f}
            </button>
          ))}
          <button className="text-xs px-2 py-1 rounded-md border hover:bg-muted" onClick={() => setSortAsc((v) => !v)}>
            Sort by confidence {sortAsc ? '↑' : '↓'}
          </button>
        </div>
      </div>
      <div className="overflow-x-auto">
        <table className="w-full text-sm" role="table" aria-label="Claims table">
          <thead className="text-left text-muted-foreground">
            <tr>
              <th className="px-4 py-2 w-[50%]">Claim</th>
              <th className="px-4 py-2">Verdict</th>
              <th className="px-4 py-2">Confidence</th>
              <th className="px-4 py-2">Evidence</th>
            </tr>
          </thead>
          <tbody>
            {rows.map((r: any, idx: number) => (
              <tr key={idx} className="border-t">
                <td className="px-4 py-2 align-top">
                  <div>
                    <div>{r.claim?.text}</div>
                    {r.claim?.speaker && (
                      <div className="mt-1">
                        <span className="inline-flex items-center px-2 py-0.5 rounded-full bg-muted text-xs text-muted-foreground">
                          {r.claim.speaker}
                        </span>
                      </div>
                    )}
                  </div>
                </td>
                <td className="px-4 py-2 align-top">
                  {r.verdict && (
                    <span className={`inline-flex items-center px-2 py-1 rounded-md border text-xs ${verdictColor(r.verdict.label)}`}>
                      {labelText(r.verdict.label)}
                    </span>
                  )}
                </td>
                <td className="px-4 py-2 align-top">
                  <div className="min-w-[100px]">
                    <div className="h-2 rounded bg-muted">
                      <div className="h-2 rounded bg-primary" style={{ width: `${confidencePercent(r.verdict?.confidence)}%` }} />
                    </div>
                    <div className="text-xs text-muted-foreground mt-1">{confidencePercent(r.verdict?.confidence)}%</div>
                  </div>
                </td>
                <td className="px-4 py-2 align-top">
                  {r.evidence ? (
                    <button
                      className="text-xs px-2 py-1 rounded-md border hover:bg-muted"
                      onClick={() => openEvidence(r.claim.id)}
                    >
                      {r.evidence.source || r.evidence.doc_id}
                    </button>
                  ) : (
                    <span className="text-xs text-muted-foreground"></span>
                  )}
                </td>
              </tr>
            ))}
          </tbody>
        </table>
      </div>
    </div>
  )
}

function labelText(v: VerdictLabel): string {
  switch (v) {
    case 'supported':
      return 'Supported'
    case 'refuted':
      return 'Refuted'
    default:
      return 'Insufficient'
  }
}