stutiagrawal commited on
Commit
a45ece3
·
1 Parent(s): 83fc25d

adding the ui files

Browse files
Files changed (34) hide show
  1. README.md +9 -1
  2. app/agents/claims.py +1 -1
  3. ui/claimcheck-ui/.eslintrc.cjs +17 -0
  4. ui/claimcheck-ui/.gitignore +8 -0
  5. ui/claimcheck-ui/.prettierrc +5 -0
  6. ui/claimcheck-ui/README_RUN_UI.md +39 -0
  7. ui/claimcheck-ui/index.html +12 -0
  8. ui/claimcheck-ui/package.json +44 -0
  9. ui/claimcheck-ui/postcss.config.js +6 -0
  10. ui/claimcheck-ui/src/App.tsx +120 -0
  11. ui/claimcheck-ui/src/components/ClaimsTable.tsx +115 -0
  12. ui/claimcheck-ui/src/components/EvidenceDrawer.tsx +63 -0
  13. ui/claimcheck-ui/src/components/Header.tsx +45 -0
  14. ui/claimcheck-ui/src/components/ResultsSummary.tsx +55 -0
  15. ui/claimcheck-ui/src/components/RunHistory.tsx +34 -0
  16. ui/claimcheck-ui/src/components/SettingsPanel.tsx +40 -0
  17. ui/claimcheck-ui/src/components/Stepper.tsx +32 -0
  18. ui/claimcheck-ui/src/components/TranscriptPanel.tsx +45 -0
  19. ui/claimcheck-ui/src/components/TranscriptViewer.tsx +38 -0
  20. ui/claimcheck-ui/src/components/UploadPanel.tsx +79 -0
  21. ui/claimcheck-ui/src/components/index.ts +10 -0
  22. ui/claimcheck-ui/src/env.d.ts +9 -0
  23. ui/claimcheck-ui/src/index.css +74 -0
  24. ui/claimcheck-ui/src/lib/api.ts +40 -0
  25. ui/claimcheck-ui/src/lib/types.ts +45 -0
  26. ui/claimcheck-ui/src/lib/utils.ts +49 -0
  27. ui/claimcheck-ui/src/main.tsx +25 -0
  28. ui/claimcheck-ui/src/mocks/sampleReport.json +28 -0
  29. ui/claimcheck-ui/src/store/appStore.ts +112 -0
  30. ui/claimcheck-ui/src/store/nanoid.ts +10 -0
  31. ui/claimcheck-ui/tailwind.config.ts +67 -0
  32. ui/claimcheck-ui/tsconfig.json +22 -0
  33. ui/claimcheck-ui/tsconfig.node.json +10 -0
  34. ui/claimcheck-ui/vite.config.ts +16 -0
README.md CHANGED
@@ -98,7 +98,15 @@ If you change the KB, rebuild the index by deleting the `kb/index/` folder.
98
  KMP_DUPLICATE_LIB_OK=TRUE OMP_NUM_THREADS=1 uvicorn app.main:app --reload
99
  ```
100
 
101
- ### 5) Try it
 
 
 
 
 
 
 
 
102
 
103
  **Transcript path (no audio):**
104
  ```bash
 
98
  KMP_DUPLICATE_LIB_OK=TRUE OMP_NUM_THREADS=1 uvicorn app.main:app --reload
99
  ```
100
 
101
+ ### 5) Run the UI
102
+ ```bash
103
+ cd ui
104
+ pnpm install
105
+ pnpm dev
106
+ ```
107
+ - If you change the UI dev port/host, add it to `allow_origins` in `app/main.py`.
108
+
109
+ ### 6) Try it
110
 
111
  **Transcript path (no audio):**
112
  ```bash
app/agents/claims.py CHANGED
@@ -113,7 +113,7 @@ def run_claim_extractor(transcript: str) -> Dict[str, Any]:
113
  repair_payload = {
114
  "input": f"Return ONLY valid JSON object with key 'claims'. Fix and output JSON:\n\n{txt}",
115
  "parameters": {"decoding_method": "greedy", "max_new_tokens": 400, "temperature": 0.0},
116
- "model_id": CLAIM_MODEL_ID,
117
  "project_id": WATSONX_PROJECT
118
  }
119
  repaired = _gen_post(repair_payload)
 
113
  repair_payload = {
114
  "input": f"Return ONLY valid JSON object with key 'claims'. Fix and output JSON:\n\n{txt}",
115
  "parameters": {"decoding_method": "greedy", "max_new_tokens": 400, "temperature": 0.0},
116
+ "model_id": IBM_CLAIM_MODEL_ID,
117
  "project_id": WATSONX_PROJECT
118
  }
119
  repaired = _gen_post(repair_payload)
ui/claimcheck-ui/.eslintrc.cjs ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ root: true,
3
+ env: { browser: true, es2021: true, node: true },
4
+ parser: '@typescript-eslint/parser',
5
+ plugins: ['react-refresh', '@typescript-eslint'],
6
+ extends: [
7
+ 'eslint:recommended',
8
+ 'plugin:@typescript-eslint/recommended',
9
+ 'plugin:react-hooks/recommended',
10
+ 'plugin:react-refresh/recommended',
11
+ 'prettier',
12
+ ],
13
+ settings: { react: { version: 'detect' } },
14
+ rules: {
15
+ '@typescript-eslint/no-explicit-any': 'off',
16
+ },
17
+ }
ui/claimcheck-ui/.gitignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ .DS_Store
3
+ /dist
4
+ /.turbo
5
+ pnpm-lock.yaml
6
+ npm-debug.log*
7
+ yarn.lock
8
+ .env
ui/claimcheck-ui/.prettierrc ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "semi": false,
3
+ "singleQuote": true,
4
+ "trailingComma": "es5"
5
+ }
ui/claimcheck-ui/README_RUN_UI.md ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # ClaimCheck.AI UI
2
+
3
+ A React + Vite + TypeScript single-page app with TailwindCSS and minimal shadcn-style design.
4
+
5
+ ## Prerequisites
6
+ - Node.js 18+
7
+ - pnpm (preferred) or npm/yarn
8
+
9
+ ## Setup
10
+ ```bash
11
+ pnpm install
12
+ ```
13
+
14
+ ## Run Dev Server
15
+ ```bash
16
+ pnpm dev
17
+ ```
18
+ - App: `http://localhost:5173`
19
+ - Backend base URL is read from `VITE_API_BASE` (default `http://127.0.0.1:8000`).
20
+ - Create `.env` and set `VITE_API_BASE=http://127.0.0.1:8000` if needed.
21
+
22
+ ## Build
23
+ ```bash
24
+ pnpm build
25
+ pnpm preview
26
+ ```
27
+
28
+ ## Features
29
+ - Upload audio or paste transcript and run
30
+ - Pipeline stepper with optimistic progression
31
+ - Results summary with copy/download/print
32
+ - Claims table with verdict filters and evidence drawer
33
+ - Transcript viewer (collapsible)
34
+ - Run history (localStorage) and sample data
35
+
36
+ ## Accessibility
37
+ - Keyboard navigable
38
+ - AA contrast
39
+ - Roles/aria on tabs, tables, dialogs
ui/claimcheck-ui/index.html ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="en" class="h-full">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>ClaimCheck.AI</title>
7
+ </head>
8
+ <body class="h-full">
9
+ <div id="root" class="h-full"></div>
10
+ <script type="module" src="/src/main.tsx"></script>
11
+ </body>
12
+ </html>
ui/claimcheck-ui/package.json ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "claimcheck-ai-ui",
3
+ "private": true,
4
+ "version": "0.1.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "preview": "vite preview",
10
+ "lint": "eslint . --ext .ts,.tsx",
11
+ "format": "prettier --write ."
12
+ },
13
+ "dependencies": {
14
+ "@radix-ui/react-dialog": "^1.0.5",
15
+ "@radix-ui/react-tabs": "^1.0.4",
16
+ "@radix-ui/react-tooltip": "^1.0.7",
17
+ "@radix-ui/react-switch": "^1.0.3",
18
+ "axios": "^1.7.2",
19
+ "classnames": "^2.5.1",
20
+ "lucide-react": "^0.454.0",
21
+ "react": "^18.3.1",
22
+ "react-dom": "^18.3.1",
23
+ "react-hook-form": "^7.52.0",
24
+ "zustand": "^4.5.2"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^20.14.9",
28
+ "@types/react": "^18.3.3",
29
+ "@types/react-dom": "^18.3.0",
30
+ "@typescript-eslint/eslint-plugin": "^7.16.1",
31
+ "@typescript-eslint/parser": "^7.16.1",
32
+ "@vitejs/plugin-react-swc": "^3.7.0",
33
+ "autoprefixer": "^10.4.19",
34
+ "eslint": "^8.57.0",
35
+ "eslint-config-prettier": "^9.1.0",
36
+ "eslint-plugin-react-hooks": "^4.6.2",
37
+ "eslint-plugin-react-refresh": "^0.4.7",
38
+ "postcss": "^8.4.38",
39
+ "prettier": "^3.3.2",
40
+ "tailwindcss": "^3.4.7",
41
+ "typescript": "^5.5.4",
42
+ "vite": "^5.4.1"
43
+ }
44
+ }
ui/claimcheck-ui/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ export default {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
ui/claimcheck-ui/src/App.tsx ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect } from 'react'
2
+ import { Header } from '@/components/Header'
3
+ import { UploadPanel } from '@/components/UploadPanel'
4
+ import { TranscriptPanel } from '@/components/TranscriptPanel'
5
+ import { SettingsPanel } from '@/components/SettingsPanel'
6
+ import { Stepper } from '@/components/Stepper'
7
+ import { ResultsSummary } from '@/components/ResultsSummary'
8
+ import { ClaimsTable } from '@/components/ClaimsTable'
9
+ import { EvidenceDrawer } from '@/components/EvidenceDrawer'
10
+ import { TranscriptViewer } from '@/components/TranscriptViewer'
11
+ import { RunHistory } from '@/components/RunHistory'
12
+ import { useAppStore } from '@/store/appStore'
13
+
14
+ export default function App() {
15
+ const currentRun = useAppStore((s) => s.runs.find((r) => r.id === s.currentRunId))
16
+ const loadFromStorage = useAppStore((s) => s.loadFromStorage)
17
+
18
+ useEffect(() => {
19
+ loadFromStorage()
20
+ }, [loadFromStorage])
21
+
22
+ return (
23
+ <div className="min-h-full">
24
+ <Header />
25
+ <main className="px-4 sm:px-6 lg:px-8 py-6 max-w-7xl mx-auto">
26
+ <div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
27
+ <section className="lg:col-span-5 space-y-6" aria-label="Input panel and run history">
28
+ <div className="bg-card text-card-foreground rounded-lg border">
29
+ <div className="p-4">
30
+ <SettingsTabs />
31
+ </div>
32
+ </div>
33
+ <RunHistory />
34
+ </section>
35
+
36
+ <section className="lg:col-span-7 space-y-4" aria-label="Results panel">
37
+ <Stepper />
38
+ {currentRun?.report ? (
39
+ <div className="space-y-4">
40
+ <ResultsSummary />
41
+ <ClaimsTable />
42
+ <TranscriptViewer />
43
+ <EvidenceDrawer />
44
+ </div>
45
+ ) : (
46
+ <EmptyState />)
47
+ }
48
+ </section>
49
+ </div>
50
+ </main>
51
+ </div>
52
+ )
53
+ }
54
+
55
+ function SettingsTabs() {
56
+ // Simple tabs without external lib for accessibility
57
+ // 0: Upload, 1: Transcript, 2: Settings
58
+ const tab = useAppStore((s) => s.ui.tab)
59
+ const setTab = useAppStore((s) => s.setTab)
60
+
61
+ return (
62
+ <div role="tablist" aria-label="Input tabs" className="w-full">
63
+ <div className="flex gap-2 border-b pb-2">
64
+ <button
65
+ role="tab"
66
+ aria-selected={tab === 0}
67
+ className={`px-3 py-2 rounded-md text-sm font-medium ${tab === 0 ? 'bg-accent text-accent-foreground' : 'hover:bg-muted'}`}
68
+ onClick={() => setTab(0)}
69
+ >
70
+ Upload Audio
71
+ </button>
72
+ <button
73
+ role="tab"
74
+ aria-selected={tab === 1}
75
+ className={`px-3 py-2 rounded-md text-sm font-medium ${tab === 1 ? 'bg-accent text-accent-foreground' : 'hover:bg-muted'}`}
76
+ onClick={() => setTab(1)}
77
+ >
78
+ Paste Transcript
79
+ </button>
80
+ <button
81
+ role="tab"
82
+ aria-selected={tab === 2}
83
+ className={`px-3 py-2 rounded-md text-sm font-medium ${tab === 2 ? 'bg-accent text-accent-foreground' : 'hover:bg-muted'}`}
84
+ onClick={() => setTab(2)}
85
+ >
86
+ KB & Settings
87
+ </button>
88
+ </div>
89
+
90
+ <div className="mt-4">
91
+ {tab === 0 && <UploadPanel />}
92
+ {tab === 1 && <TranscriptPanel />}
93
+ {tab === 2 && <SettingsPanel />}
94
+ </div>
95
+ </div>
96
+ )
97
+ }
98
+
99
+ function EmptyState() {
100
+ return (
101
+ <div className="bg-card text-card-foreground rounded-lg border p-8">
102
+ <h2 className="text-lg font-semibold">Welcome to ClaimCheck.AI</h2>
103
+ <p className="text-sm text-muted-foreground mt-2">
104
+ Upload an audio file or paste a transcript to verify claims against your knowledge base. Use the sample data to explore the UI.
105
+ </p>
106
+ <div className="mt-4">
107
+ <SampleDataButton />
108
+ </div>
109
+ </div>
110
+ )
111
+ }
112
+
113
+ function SampleDataButton() {
114
+ const loadSample = useAppStore((s) => s.loadSample)
115
+ return (
116
+ <button onClick={loadSample} className="inline-flex items-center gap-2 px-3 py-2 rounded-md bg-primary text-primary-foreground hover:opacity-90">
117
+ Use Sample Data
118
+ </button>
119
+ )
120
+ }
ui/claimcheck-ui/src/components/ClaimsTable.tsx ADDED
@@ -0,0 +1,115 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useMemo, useState } from 'react'
2
+ import { useAppStore } from '@/store/appStore'
3
+ import type { VerdictLabel } from '@/lib/types'
4
+ import { confidencePercent, verdictColor } from '@/lib/utils'
5
+
6
+ export function ClaimsTable() {
7
+ const run = useAppStore((s) => s.runs.find((r) => r.id === s.currentRunId))
8
+ const filter = useAppStore((s) => s.ui.filter)
9
+ const setFilter = useAppStore((s) => s.setFilter)
10
+ const openEvidence = useAppStore((s) => s.openEvidence)
11
+
12
+ const [sortAsc, setSortAsc] = useState<boolean>(false)
13
+
14
+ const rows = useMemo(() => {
15
+ if (!run?.report) return []
16
+ const { claims, verdicts, evidence } = run.report
17
+ const byClaim: Record<string, any> = {}
18
+ for (const c of claims) byClaim[c.id] = { claim: c }
19
+ for (const v of verdicts) byClaim[v.claim_id] = { ...(byClaim[v.claim_id] || {}), verdict: v }
20
+
21
+ let data = Object.values(byClaim)
22
+ if (filter !== 'all') {
23
+ data = data.filter((r: any) => r.verdict?.label === filter)
24
+ }
25
+ data.sort((a: any, b: any) => (sortAsc ? (a.verdict?.confidence ?? 0) - (b.verdict?.confidence ?? 0) : (b.verdict?.confidence ?? 0) - (a.verdict?.confidence ?? 0)))
26
+
27
+ return data.map((r: any) => {
28
+ const evid = evidence.find((e) => e.doc_id === r.verdict?.best_evidence_id)
29
+ return { ...r, evidence: evid }
30
+ })
31
+ }, [run, filter, sortAsc])
32
+
33
+ if (!run?.report) return null
34
+
35
+ const filters: ('all' | VerdictLabel)[] = ['all', 'supported', 'refuted', 'insufficient']
36
+
37
+ return (
38
+ <div className="bg-card text-card-foreground rounded-lg border">
39
+ <div className="p-4 flex items-center justify-between">
40
+ <h3 className="font-medium">Claims</h3>
41
+ <div className="flex items-center gap-2">
42
+ {filters.map((f) => (
43
+ <button
44
+ key={f}
45
+ onClick={() => setFilter(f)}
46
+ className={`text-xs px-2 py-1 rounded-md border ${filter === f ? 'bg-accent' : 'hover:bg-muted'}`}
47
+ >
48
+ {f}
49
+ </button>
50
+ ))}
51
+ <button className="text-xs px-2 py-1 rounded-md border hover:bg-muted" onClick={() => setSortAsc((v) => !v)}>
52
+ Sort by confidence {sortAsc ? '↑' : '↓'}
53
+ </button>
54
+ </div>
55
+ </div>
56
+ <div className="overflow-x-auto">
57
+ <table className="w-full text-sm" role="table" aria-label="Claims table">
58
+ <thead className="text-left text-muted-foreground">
59
+ <tr>
60
+ <th className="px-4 py-2 w-[50%]">Claim</th>
61
+ <th className="px-4 py-2">Verdict</th>
62
+ <th className="px-4 py-2">Confidence</th>
63
+ <th className="px-4 py-2">Evidence</th>
64
+ </tr>
65
+ </thead>
66
+ <tbody>
67
+ {rows.map((r: any, idx: number) => (
68
+ <tr key={idx} className="border-t">
69
+ <td className="px-4 py-2 align-top">{r.claim?.text}</td>
70
+ <td className="px-4 py-2 align-top">
71
+ {r.verdict && (
72
+ <span className={`inline-flex items-center px-2 py-1 rounded-md border text-xs ${verdictColor(r.verdict.label)}`}>
73
+ {labelText(r.verdict.label)}
74
+ </span>
75
+ )}
76
+ </td>
77
+ <td className="px-4 py-2 align-top">
78
+ <div className="min-w-[100px]">
79
+ <div className="h-2 rounded bg-muted">
80
+ <div className="h-2 rounded bg-primary" style={{ width: `${confidencePercent(r.verdict?.confidence)}%` }} />
81
+ </div>
82
+ <div className="text-xs text-muted-foreground mt-1">{confidencePercent(r.verdict?.confidence)}%</div>
83
+ </div>
84
+ </td>
85
+ <td className="px-4 py-2 align-top">
86
+ {r.evidence ? (
87
+ <button
88
+ className="text-xs px-2 py-1 rounded-md border hover:bg-muted"
89
+ onClick={() => openEvidence(r.claim.id)}
90
+ >
91
+ {r.evidence.source || r.evidence.doc_id}
92
+ </button>
93
+ ) : (
94
+ <span className="text-xs text-muted-foreground">—</span>
95
+ )}
96
+ </td>
97
+ </tr>
98
+ ))}
99
+ </tbody>
100
+ </table>
101
+ </div>
102
+ </div>
103
+ )
104
+ }
105
+
106
+ function labelText(v: VerdictLabel): string {
107
+ switch (v) {
108
+ case 'supported':
109
+ return 'Supported'
110
+ case 'refuted':
111
+ return 'Refuted'
112
+ default:
113
+ return 'Insufficient'
114
+ }
115
+ }
ui/claimcheck-ui/src/components/EvidenceDrawer.tsx ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useMemo } from 'react'
2
+ import { useAppStore } from '@/store/appStore'
3
+
4
+ export function EvidenceDrawer() {
5
+ const run = useAppStore((s) => s.runs.find((r) => r.id === s.currentRunId))
6
+ const isOpen = useAppStore((s) => s.ui.evidenceOpen)
7
+ const claimId = useAppStore((s) => s.ui.selectedClaimId)
8
+ const close = useAppStore((s) => s.closeEvidence)
9
+
10
+ const claim = useMemo(() => run?.report?.claims.find((c) => c.id === claimId), [run, claimId])
11
+ const verdict = useMemo(() => run?.report?.verdicts.find((v) => v.claim_id === claimId), [run, claimId])
12
+
13
+ const evidences = useMemo(() => {
14
+ if (!run?.report || !verdict) return []
15
+ const sorted = [...run.report.evidence].sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
16
+ return sorted.slice(0, 5)
17
+ }, [run, verdict])
18
+
19
+ if (!isOpen) return null
20
+
21
+ return (
22
+ <div role="dialog" aria-modal="true" className="fixed inset-0 z-50">
23
+ <div className="absolute inset-0 bg-black/40" onClick={close} />
24
+ <div className="absolute top-0 right-0 h-full w-full sm:w-[420px] bg-card text-card-foreground border-l shadow-xl overflow-auto" tabIndex={-1} aria-labelledby="evidence-title">
25
+ <div className="p-4 border-b flex items-start justify-between">
26
+ <h3 id="evidence-title" className="font-semibold pr-4">Evidence & Rationale</h3>
27
+ <button className="text-sm px-2 py-1 rounded-md border hover:bg-muted" onClick={close}>Close</button>
28
+ </div>
29
+ <div className="p-4 space-y-4">
30
+ {claim && (
31
+ <div>
32
+ <div className="text-xs text-muted-foreground">Claim</div>
33
+ <div className="mt-1 text-sm">{claim.text}</div>
34
+ </div>
35
+ )}
36
+
37
+ <div>
38
+ <div className="text-xs text-muted-foreground">Top Evidence</div>
39
+ <ul className="mt-2 space-y-3">
40
+ {evidences.map((e) => (
41
+ <li key={e.doc_id} className="rounded-md border p-3">
42
+ <div className="text-xs text-muted-foreground">{e.source || e.doc_id} • score {(e.score ?? 0).toFixed(2)}</div>
43
+ <div className="mt-1 text-sm whitespace-pre-wrap">{e.snippet}</div>
44
+ {e.metadata && (
45
+ <pre className="mt-2 text-xs text-muted-foreground whitespace-pre-wrap">{JSON.stringify(e.metadata, null, 2)}</pre>
46
+ )}
47
+ </li>
48
+ ))}
49
+ {evidences.length === 0 && <li className="text-sm text-muted-foreground">No evidence</li>}
50
+ </ul>
51
+ </div>
52
+
53
+ {verdict?.rationale && (
54
+ <div>
55
+ <div className="text-xs text-muted-foreground">Model Rationale</div>
56
+ <div className="mt-1 text-sm whitespace-pre-wrap">{verdict.rationale}</div>
57
+ </div>
58
+ )}
59
+ </div>
60
+ </div>
61
+ </div>
62
+ )
63
+ }
ui/claimcheck-ui/src/components/Header.tsx ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from 'react'
2
+ import { Moon, Sun } from 'lucide-react'
3
+
4
+ export function Header() {
5
+ return (
6
+ <header className="border-b bg-background/60 backdrop-blur supports-[backdrop-filter]:bg-background/60">
7
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-14 flex items-center justify-between">
8
+ <div className="flex items-center gap-3">
9
+ <span className="text-lg font-bold">ClaimCheck.AI</span>
10
+ <span className="text-xs px-2 py-1 rounded-full border bg-muted text-muted-foreground">IBM watsonx powered</span>
11
+ </div>
12
+ <ThemeToggle />
13
+ </div>
14
+ </header>
15
+ )
16
+ }
17
+
18
+ function ThemeToggle() {
19
+ const [isDark, setIsDark] = useState(false)
20
+
21
+ useEffect(() => {
22
+ const saved = localStorage.getItem('cc_theme')
23
+ const prefers = window.matchMedia('(prefers-color-scheme: dark)').matches
24
+ const dark = saved ? saved === 'dark' : prefers
25
+ setIsDark(dark)
26
+ }, [])
27
+
28
+ function toggle() {
29
+ const next = !isDark
30
+ setIsDark(next)
31
+ document.documentElement.classList.toggle('dark', next)
32
+ localStorage.setItem('cc_theme', next ? 'dark' : 'light')
33
+ }
34
+
35
+ return (
36
+ <button
37
+ className="inline-flex items-center gap-2 text-sm px-3 py-2 rounded-md border hover:bg-muted"
38
+ aria-label="Toggle color theme"
39
+ onClick={toggle}
40
+ >
41
+ {isDark ? <Sun size={16} /> : <Moon size={16} />}
42
+ <span className="hidden sm:inline">{isDark ? 'Light' : 'Dark'} mode</span>
43
+ </button>
44
+ )
45
+ }
ui/claimcheck-ui/src/components/ResultsSummary.tsx ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useMemo } from 'react'
2
+ import { useAppStore } from '@/store/appStore'
3
+ import { copyToClipboard, downloadJSON } from '@/lib/utils'
4
+ import { Copy, Download, Printer } from 'lucide-react'
5
+
6
+ export function ResultsSummary() {
7
+ const run = useAppStore((s) => s.runs.find((r) => r.id === s.currentRunId))
8
+ const report = run?.report
9
+
10
+ const summary = useMemo(() => report?.call_summary ?? '', [report])
11
+
12
+ if (!report) return null
13
+
14
+ function onCopy() {
15
+ copyToClipboard(summary)
16
+ }
17
+
18
+ function onDownload() {
19
+ downloadJSON(report)
20
+ }
21
+
22
+ function onPrint() {
23
+ window.print()
24
+ }
25
+
26
+ return (
27
+ <div className="bg-card text-card-foreground rounded-lg border p-4">
28
+ <div className="flex items-center justify-between">
29
+ <h3 className="font-semibold">Executive Summary</h3>
30
+ <div className="flex items-center gap-2">
31
+ <button className="inline-flex items-center gap-1 text-sm px-2 py-1 rounded-md border hover:bg-muted" onClick={onCopy}>
32
+ <Copy size={14} /> Copy
33
+ </button>
34
+ <button className="inline-flex items-center gap-1 text-sm px-2 py-1 rounded-md border hover:bg-muted" onClick={onDownload}>
35
+ <Download size={14} /> Download JSON
36
+ </button>
37
+ <button className="inline-flex items-center gap-1 text-sm px-2 py-1 rounded-md border hover:bg-muted" onClick={onPrint}>
38
+ <Printer size={14} /> Download PDF
39
+ </button>
40
+ </div>
41
+ </div>
42
+ <p className="mt-3 text-sm leading-6 whitespace-pre-line">{summary}</p>
43
+ {report.action_items?.length > 0 && (
44
+ <div className="mt-4">
45
+ <h4 className="font-medium">Action Items</h4>
46
+ <ul className="list-disc list-inside text-sm mt-2 space-y-1">
47
+ {report.action_items.map((a, idx) => (
48
+ <li key={idx}>{a}</li>
49
+ ))}
50
+ </ul>
51
+ </div>
52
+ )}
53
+ </div>
54
+ )
55
+ }
ui/claimcheck-ui/src/components/RunHistory.tsx ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useAppStore } from '@/store/appStore'
2
+ import { History } from 'lucide-react'
3
+
4
+ export function RunHistory() {
5
+ const runs = useAppStore((s) => s.runs)
6
+ const selectRun = useAppStore((s) => s.selectRun)
7
+ const currentRunId = useAppStore((s) => s.currentRunId)
8
+
9
+ return (
10
+ <div className="bg-card text-card-foreground rounded-lg border">
11
+ <div className="p-4 border-b flex items-center gap-2">
12
+ <History size={16} />
13
+ <h3 className="font-medium">Run History</h3>
14
+ </div>
15
+ <ul className="divide-y" role="list" aria-label="Recent runs">
16
+ {runs.length === 0 && (
17
+ <li className="p-4 text-sm text-muted-foreground">No runs yet</li>
18
+ )}
19
+ {runs.map((r) => (
20
+ <li key={r.id} className="p-3">
21
+ <button
22
+ className={`w-full text-left px-2 py-2 rounded-md hover:bg-muted ${currentRunId === r.id ? 'bg-muted' : ''}`}
23
+ onClick={() => selectRun(r.id)}
24
+ >
25
+ <div className="text-sm font-medium">{new Date(r.startedAt).toLocaleString()}</div>
26
+ <div className="text-xs text-muted-foreground">{r.inputKind}</div>
27
+ {r.error && <div className="mt-1 text-xs text-red-600">{r.error}</div>}
28
+ </button>
29
+ </li>
30
+ ))}
31
+ </ul>
32
+ </div>
33
+ )
34
+ }
ui/claimcheck-ui/src/components/SettingsPanel.tsx ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useEffect, useState } from 'react'
2
+ import { health, rebuildKb, type Health } from '@/lib/api'
3
+
4
+ export function SettingsPanel() {
5
+ const [data, setData] = useState<Health | null>(null)
6
+ const [error, setError] = useState<string | null>(null)
7
+
8
+ useEffect(() => {
9
+ health().then(setData).catch((e) => setError(e?.message || 'Failed to load health'))
10
+ }, [])
11
+
12
+ return (
13
+ <div className="space-y-4" aria-label="Settings">
14
+ <div>
15
+ <h3 className="font-medium">Models</h3>
16
+ {!data && !error && <p className="text-sm text-muted-foreground">Loading...</p>}
17
+ {error && <p className="text-sm text-red-600">{error}</p>}
18
+ {data && (
19
+ <div className="text-sm bg-secondary rounded-md p-3 overflow-auto">
20
+ <div>Status: {data.ok ? 'OK' : 'Degraded'}</div>
21
+ <div>watsonx: {data.watsonx ? 'Yes' : 'No'}</div>
22
+ <div>stt: {data.stt ? 'Yes' : 'No'}</div>
23
+ {data.models && (
24
+ <pre className="mt-2 text-xs whitespace-pre-wrap">{JSON.stringify(data.models, null, 2)}</pre>
25
+ )}
26
+ </div>
27
+ )}
28
+ </div>
29
+ <div>
30
+ <h3 className="font-medium">Knowledge Base</h3>
31
+ <button
32
+ onClick={async () => { try { await rebuildKb(); alert('KB rebuild requested'); } catch (e) { alert('Failed to rebuild KB'); } }}
33
+ className="inline-flex items-center gap-2 px-3 py-2 rounded-md border hover:bg-muted"
34
+ >
35
+ Rebuild Index
36
+ </button>
37
+ </div>
38
+ </div>
39
+ )
40
+ }
ui/claimcheck-ui/src/components/Stepper.tsx ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useAppStore, StepKey } from '@/store/appStore'
2
+ import { AudioWaveform, FileText, ListChecks, Search, ShieldCheck } from 'lucide-react'
3
+
4
+ const steps: { key: StepKey; label: string; icon: any }[] = [
5
+ { key: 'ASR', label: 'ASR', icon: AudioWaveform },
6
+ { key: 'Claims', label: 'Claims', icon: ListChecks },
7
+ { key: 'Retrieval', label: 'Retrieval', icon: Search },
8
+ { key: 'Verification', label: 'Verification', icon: ShieldCheck },
9
+ { key: 'Summary', label: 'Summary', icon: FileText },
10
+ ]
11
+
12
+ export function Stepper() {
13
+ const state = useAppStore((s) => s.steps)
14
+ return (
15
+ <div className="bg-card text-card-foreground rounded-lg border p-3" role="group" aria-label="Pipeline status">
16
+ <ol className="grid grid-cols-1 sm:grid-cols-5 gap-2">
17
+ {steps.map(({ key, label, icon: Icon }) => {
18
+ const status = state[key]
19
+ return (
20
+ <li key={key} className="flex items-center gap-2">
21
+ <span className={`w-2 h-2 rounded-full ${status === 'done' ? 'bg-green-500' : status === 'running' ? 'bg-blue-500 animate-pulse' : status === 'error' ? 'bg-red-500' : 'bg-muted-foreground/40'}`} />
22
+ <span className="inline-flex items-center gap-2 text-sm">
23
+ <Icon size={16} /> {label}
24
+ {status === 'running' && <span className="ml-1 h-3 w-3 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" aria-label="Loading" />}
25
+ </span>
26
+ </li>
27
+ )
28
+ })}
29
+ </ol>
30
+ </div>
31
+ )
32
+ }
ui/claimcheck-ui/src/components/TranscriptPanel.tsx ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState } from 'react'
2
+ import { FileText, Play } from 'lucide-react'
3
+ import { useAppStore } from '@/store/appStore'
4
+ import { wordCount } from '@/lib/utils'
5
+
6
+ const SAMPLE = `Agent: Thanks for joining. We saw improved adoption this quarter.\nCustomer: We plan to expand to 3 more teams next month.\nAgent: Pricing will remain the same.\nCustomer: If support SLAs slip again, we may churn.`
7
+
8
+ export function TranscriptPanel() {
9
+ const [text, setText] = useState('')
10
+ const startRun = useAppStore((s) => s.startRun)
11
+
12
+ function loadSample() {
13
+ setText(SAMPLE)
14
+ }
15
+
16
+ return (
17
+ <div className="space-y-4" aria-label="Paste transcript">
18
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
19
+ <FileText size={16} /> Paste call transcript
20
+ </div>
21
+ <textarea
22
+ value={text}
23
+ onChange={(e) => setText(e.target.value)}
24
+ rows={10}
25
+ className="w-full rounded-md border bg-background p-3 outline-none focus:ring-2 focus:ring-ring"
26
+ placeholder="Paste transcript here..."
27
+ />
28
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
29
+ <span>Word count: {wordCount(text)}</span>
30
+ <button onClick={loadSample} className="underline hover:no-underline">
31
+ Use sample text
32
+ </button>
33
+ </div>
34
+ <div className="flex justify-end">
35
+ <button
36
+ disabled={!text.trim()}
37
+ onClick={() => startRun('transcript', { text })}
38
+ className={`inline-flex items-center gap-2 px-4 py-2 rounded-md ${!text.trim() ? 'bg-muted text-muted-foreground' : 'bg-primary text-primary-foreground hover:opacity-90'}`}
39
+ >
40
+ <Play size={16} /> Run
41
+ </button>
42
+ </div>
43
+ </div>
44
+ )
45
+ }
ui/claimcheck-ui/src/components/TranscriptViewer.tsx ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useMemo, useState } from 'react'
2
+ import { useAppStore } from '@/store/appStore'
3
+
4
+ export function TranscriptViewer() {
5
+ const [open, setOpen] = useState(false)
6
+ const run = useAppStore((s) => s.runs.find((r) => r.id === s.currentRunId))
7
+
8
+ const segments = useMemo(() => {
9
+ const claims = run?.report?.claims ?? []
10
+ // synthesize segments if none: group by speaker or fallback single block
11
+ const text = (run?.report?.claim_table || []).map((c) => c.claim).join('\n')
12
+ return [
13
+ { speaker: 'Call', start: 0, end: 0, text, claims },
14
+ ]
15
+ }, [run])
16
+
17
+ if (!run?.report) return null
18
+
19
+ return (
20
+ <div className="bg-card text-card-foreground rounded-lg border">
21
+ <button className="w-full text-left p-4" onClick={() => setOpen((v) => !v)} aria-expanded={open}>
22
+ <span className="font-medium">Transcript</span>
23
+ </button>
24
+ {open && (
25
+ <div className="px-4 pb-4 space-y-4">
26
+ {segments.map((seg, idx) => (
27
+ <div key={idx} className="rounded-md border p-3">
28
+ <div className="text-xs text-muted-foreground">{seg.speaker}</div>
29
+ <div className="mt-1 text-sm whitespace-pre-wrap">
30
+ {seg.text}
31
+ </div>
32
+ </div>
33
+ ))}
34
+ </div>
35
+ )}
36
+ </div>
37
+ )
38
+ }
ui/claimcheck-ui/src/components/UploadPanel.tsx ADDED
@@ -0,0 +1,79 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useCallback, useMemo } from 'react'
2
+ import { Upload, Play } from 'lucide-react'
3
+ import { useAppStore } from '@/store/appStore'
4
+ import { formatBytes } from '@/lib/utils'
5
+
6
+ export function UploadPanel() {
7
+ const [file, setFile] = useState<File | null>(null)
8
+ const [duration, setDuration] = useState<number | null>(null)
9
+ const startRun = useAppStore((s) => s.startRun)
10
+
11
+ const onFiles = useCallback((files: FileList | null) => {
12
+ if (!files || files.length === 0) return
13
+ const f = files[0]
14
+ setFile(f)
15
+ // compute duration
16
+ const url = URL.createObjectURL(f)
17
+ const audio = new Audio(url)
18
+ audio.addEventListener('loadedmetadata', () => {
19
+ setDuration(audio.duration)
20
+ URL.revokeObjectURL(url)
21
+ })
22
+ }, [])
23
+
24
+ const handleDrop = useCallback((e: React.DragEvent<HTMLDivElement>) => {
25
+ e.preventDefault()
26
+ onFiles(e.dataTransfer.files)
27
+ }, [onFiles])
28
+
29
+ const handleBrowse = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
30
+ onFiles(e.target.files)
31
+ }, [onFiles])
32
+
33
+ const disabled = !file
34
+ const durationText = useMemo(() => {
35
+ if (duration == null) return '—'
36
+ const m = Math.floor(duration / 60)
37
+ const s = Math.round(duration % 60)
38
+ return `${m}:${s.toString().padStart(2, '0')}`
39
+ }, [duration])
40
+
41
+ return (
42
+ <div className="space-y-4" aria-label="Upload audio">
43
+ <div
44
+ onDragOver={(e) => e.preventDefault()}
45
+ onDrop={handleDrop}
46
+ className="border-2 border-dashed rounded-lg p-6 text-center hover:bg-muted/50"
47
+ >
48
+ <div className="flex flex-col items-center gap-2">
49
+ <Upload />
50
+ <p className="text-sm text-muted-foreground">Drag and drop WAV/MP3/M4A here or click to browse</p>
51
+ <input
52
+ type="file"
53
+ accept="audio/wav, audio/mpeg, audio/mp4, audio/x-m4a, audio/aac"
54
+ onChange={handleBrowse}
55
+ className="mt-2"
56
+ />
57
+ </div>
58
+ </div>
59
+
60
+ {file && (
61
+ <div className="text-sm bg-secondary text-secondary-foreground rounded-md p-3 flex flex-wrap gap-4">
62
+ <span className="font-medium">{file.name}</span>
63
+ <span>Size: {formatBytes(file.size)}</span>
64
+ <span>Duration: {durationText}</span>
65
+ </div>
66
+ )}
67
+
68
+ <div className="flex justify-end">
69
+ <button
70
+ disabled={disabled}
71
+ onClick={() => file && startRun('audio', { file })}
72
+ className={`inline-flex items-center gap-2 px-4 py-2 rounded-md ${disabled ? 'bg-muted text-muted-foreground' : 'bg-primary text-primary-foreground hover:opacity-90'}`}
73
+ >
74
+ <Play size={16} /> Run
75
+ </button>
76
+ </div>
77
+ </div>
78
+ )
79
+ }
ui/claimcheck-ui/src/components/index.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ export * from './Header'
2
+ export * from './Stepper'
3
+ export * from './UploadPanel'
4
+ export * from './TranscriptPanel'
5
+ export * from './SettingsPanel'
6
+ export * from './ResultsSummary'
7
+ export * from './ClaimsTable'
8
+ export * from './EvidenceDrawer'
9
+ export * from './TranscriptViewer'
10
+ export * from './RunHistory'
ui/claimcheck-ui/src/env.d.ts ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ /// <reference types="vite/client" />
2
+
3
+ interface ImportMetaEnv {
4
+ readonly VITE_API_BASE?: string
5
+ }
6
+
7
+ interface ImportMeta {
8
+ readonly env: ImportMetaEnv
9
+ }
ui/claimcheck-ui/src/index.css ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ :root {
7
+ --background: 0 0% 100%;
8
+ --foreground: 222.2 47.4% 11.2%;
9
+
10
+ --muted: 210 40% 96.1%;
11
+ --muted-foreground: 215.4 16.3% 46.9%;
12
+
13
+ --popover: 0 0% 100%;
14
+ --popover-foreground: 222.2 47.4% 11.2%;
15
+
16
+ --card: 0 0% 100%;
17
+ --card-foreground: 222.2 47.4% 11.2%;
18
+
19
+ --border: 214.3 31.8% 91.4%;
20
+ --input: 214.3 31.8% 91.4%;
21
+ --ring: 222.2 84% 4.9%;
22
+
23
+ --radius: 0.75rem;
24
+
25
+ --primary: 221.2 83.2% 53.3%;
26
+ --primary-foreground: 210 40% 98%;
27
+
28
+ --secondary: 210 40% 96.1%;
29
+ --secondary-foreground: 222.2 47.4% 11.2%;
30
+
31
+ --accent: 210 40% 96.1%;
32
+ --accent-foreground: 222.2 47.4% 11.2%;
33
+
34
+ --destructive: 0 84.2% 60.2%;
35
+ --destructive-foreground: 210 40% 98%;
36
+ }
37
+
38
+ .dark {
39
+ --background: 222.2 84% 4.9%;
40
+ --foreground: 210 40% 98%;
41
+
42
+ --muted: 217.2 32.6% 17.5%;
43
+ --muted-foreground: 215 20.2% 65.1%;
44
+
45
+ --popover: 222.2 84% 4.9%;
46
+ --popover-foreground: 210 40% 98%;
47
+
48
+ --card: 222.2 84% 4.9%;
49
+ --card-foreground: 210 40% 98%;
50
+
51
+ --border: 217.2 32.6% 17.5%;
52
+ --input: 217.2 32.6% 17.5%;
53
+ --ring: 212.7 26.8% 83.9%;
54
+
55
+ --primary: 217.2 91.2% 59.8%;
56
+ --primary-foreground: 222.2 47.4% 11.2%;
57
+
58
+ --secondary: 217.2 32.6% 17.5%;
59
+ --secondary-foreground: 210 40% 98%;
60
+
61
+ --accent: 217.2 32.6% 17.5%;
62
+ --accent-foreground: 210 40% 98%;
63
+
64
+ --destructive: 0 62.8% 30.6%;
65
+ --destructive-foreground: 210 40% 98%;
66
+ }
67
+
68
+ * {
69
+ @apply border-border;
70
+ }
71
+ body {
72
+ @apply bg-background text-foreground;
73
+ }
74
+ }
ui/claimcheck-ui/src/lib/api.ts ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import axios from 'axios'
2
+ import type { CallReport } from '@/lib/types'
3
+
4
+ const baseURL = import.meta.env.VITE_API_BASE ?? 'http://127.0.0.1:8000'
5
+
6
+ export const api = axios.create({
7
+ baseURL,
8
+ timeout: 120000,
9
+ })
10
+
11
+ export type Health = {
12
+ ok: boolean
13
+ watsonx: boolean
14
+ stt: boolean
15
+ models?: Record<string, any>
16
+ }
17
+
18
+ export async function health(): Promise<Health> {
19
+ const { data } = await api.get('/health/ibm')
20
+ return data
21
+ }
22
+
23
+ export async function rebuildKb(): Promise<{ ok: boolean }> {
24
+ const { data } = await api.post('/kb/rebuild')
25
+ return data
26
+ }
27
+
28
+ export async function processAudio(file: File): Promise<CallReport> {
29
+ const form = new FormData()
30
+ form.append('file', file)
31
+ const { data } = await api.post('/process-audio', form, {
32
+ headers: { 'Content-Type': 'multipart/form-data' },
33
+ })
34
+ return data
35
+ }
36
+
37
+ export async function processTranscript(text: string): Promise<CallReport> {
38
+ const { data } = await api.post('/process-transcript', { text })
39
+ return data
40
+ }
ui/claimcheck-ui/src/lib/types.ts ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type Claim = {
2
+ id: string
3
+ text: string
4
+ speaker?: string | null
5
+ start?: number
6
+ end?: number
7
+ confidence?: number
8
+ }
9
+
10
+ export type Evidence = {
11
+ doc_id: string
12
+ source?: string
13
+ snippet: string
14
+ score?: number
15
+ metadata?: Record<string, any>
16
+ }
17
+
18
+ export type VerdictLabel = 'supported' | 'refuted' | 'insufficient'
19
+
20
+ export type Verdict = {
21
+ claim_id: string
22
+ label: VerdictLabel
23
+ confidence?: number
24
+ best_evidence_id?: string
25
+ rationale?: string
26
+ }
27
+
28
+ export type CallReport = {
29
+ call_summary: string
30
+ action_items: string[]
31
+ claim_table: { claim: string; status: string; evidence_source?: string }[]
32
+ claims: Claim[]
33
+ evidence: Evidence[]
34
+ verdicts: Verdict[]
35
+ }
36
+
37
+ export type InputKind = 'audio' | 'transcript' | 'sample'
38
+
39
+ export type RunRecord = {
40
+ id: string
41
+ startedAt: number
42
+ inputKind: InputKind
43
+ report?: CallReport
44
+ error?: string
45
+ }
ui/claimcheck-ui/src/lib/utils.ts ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { CallReport, VerdictLabel } from '@/lib/types'
2
+
3
+ export function formatBytes(bytes: number): string {
4
+ if (bytes === 0) return '0 B'
5
+ const k = 1024
6
+ const sizes = ['B', 'KB', 'MB', 'GB']
7
+ const i = Math.floor(Math.log(bytes) / Math.log(k))
8
+ return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`
9
+ }
10
+
11
+ export function verdictColor(label: VerdictLabel): string {
12
+ switch (label) {
13
+ case 'supported':
14
+ return 'bg-supported/15 text-supported border-supported/30'
15
+ case 'refuted':
16
+ return 'bg-refuted/15 text-refuted border-refuted/30'
17
+ default:
18
+ return 'bg-insufficient/15 text-insufficient border-insufficient/30'
19
+ }
20
+ }
21
+
22
+ export function confidencePercent(v?: number): number {
23
+ if (!v && v !== 0) return 0
24
+ const pct = Math.max(0, Math.min(1, v)) * 100
25
+ return Math.round(pct)
26
+ }
27
+
28
+ export async function copyToClipboard(text: string): Promise<boolean> {
29
+ try {
30
+ await navigator.clipboard.writeText(text)
31
+ return true
32
+ } catch {
33
+ return false
34
+ }
35
+ }
36
+
37
+ export function downloadJSON(report: CallReport, filename = 'call_report.json') {
38
+ const blob = new Blob([JSON.stringify(report, null, 2)], { type: 'application/json' })
39
+ const url = URL.createObjectURL(blob)
40
+ const a = document.createElement('a')
41
+ a.href = url
42
+ a.download = filename
43
+ a.click()
44
+ URL.revokeObjectURL(url)
45
+ }
46
+
47
+ export function wordCount(text: string): number {
48
+ return (text.trim().match(/\S+/g) || []).length
49
+ }
ui/claimcheck-ui/src/main.tsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react'
2
+ import ReactDOM from 'react-dom/client'
3
+ import App from './App'
4
+ import './index.css'
5
+
6
+ function useSystemPrefersDark(): boolean {
7
+ if (typeof window === 'undefined') return false
8
+ return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
9
+ }
10
+
11
+ function initTheme() {
12
+ const saved = localStorage.getItem('cc_theme')
13
+ const root = document.documentElement
14
+ const prefersDark = useSystemPrefersDark()
15
+ const isDark = saved ? saved === 'dark' : prefersDark
16
+ root.classList.toggle('dark', isDark)
17
+ }
18
+
19
+ initTheme()
20
+
21
+ ReactDOM.createRoot(document.getElementById('root')!).render(
22
+ <React.StrictMode>
23
+ <App />
24
+ </React.StrictMode>
25
+ )
ui/claimcheck-ui/src/mocks/sampleReport.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "call_summary": "Customer plans to expand to three additional teams next month. Pricing to remain the same. Risk of churn if support SLAs slip again.",
3
+ "action_items": [
4
+ "Confirm expansion timeline and seats",
5
+ "Share support SLA improvements",
6
+ "Send pricing confirmation"
7
+ ],
8
+ "claim_table": [
9
+ { "claim": "Customer will expand to 3 more teams next month", "status": "supported", "evidence_source": "kb:success_story_12" },
10
+ { "claim": "Pricing will increase next quarter", "status": "refuted", "evidence_source": "kb:pricing_policy" },
11
+ { "claim": "Customer will churn if SLAs slip again", "status": "supported", "evidence_source": "kb:retention_playbook" }
12
+ ],
13
+ "claims": [
14
+ { "id": "c1", "text": "Customer will expand to 3 more teams next month", "speaker": "Customer", "start": 12, "end": 18, "confidence": 0.92 },
15
+ { "id": "c2", "text": "Pricing will increase next quarter", "speaker": "Agent", "start": 33, "end": 40, "confidence": 0.71 },
16
+ { "id": "c3", "text": "Customer will churn if SLAs slip again", "speaker": "Customer", "start": 62, "end": 74, "confidence": 0.88 }
17
+ ],
18
+ "evidence": [
19
+ { "doc_id": "kb:success_story_12", "source": "Case Study", "snippet": "The expansion plan includes onboarding three additional teams in the following month.", "score": 0.83 },
20
+ { "doc_id": "kb:pricing_policy", "source": "Policy", "snippet": "Pricing remains stable for existing contracts through the next quarter.", "score": 0.79 },
21
+ { "doc_id": "kb:retention_playbook", "source": "Playbook", "snippet": "Customers citing SLA concerns are at churn risk; ensure proactive communication.", "score": 0.76 }
22
+ ],
23
+ "verdicts": [
24
+ { "claim_id": "c1", "label": "supported", "confidence": 0.86, "best_evidence_id": "kb:success_story_12", "rationale": "KB confirms planned expansion to three teams next month." },
25
+ { "claim_id": "c2", "label": "refuted", "confidence": 0.81, "best_evidence_id": "kb:pricing_policy", "rationale": "Policy states no price increase next quarter for existing contracts." },
26
+ { "claim_id": "c3", "label": "supported", "confidence": 0.74, "best_evidence_id": "kb:retention_playbook", "rationale": "Playbook indicates churn risk when SLAs slip; aligns with customer statement." }
27
+ ]
28
+ }
ui/claimcheck-ui/src/store/appStore.ts ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { create } from 'zustand'
2
+ import { persist } from 'zustand/middleware'
3
+ import { nanoid } from '@/store/nanoid'
4
+ import type { CallReport, InputKind, RunRecord, VerdictLabel } from '@/lib/types'
5
+ import { processAudio, processTranscript } from '@/lib/api'
6
+
7
+ export type StepKey = 'ASR' | 'Claims' | 'Retrieval' | 'Verification' | 'Summary'
8
+ export type StepStatus = 'idle' | 'running' | 'done' | 'error'
9
+
10
+ export type UIState = {
11
+ tab: 0 | 1 | 2
12
+ filter: 'all' | VerdictLabel
13
+ evidenceOpen: boolean
14
+ selectedClaimId?: string
15
+ }
16
+
17
+ type Store = {
18
+ runs: RunRecord[]
19
+ currentRunId?: string
20
+ steps: Record<StepKey, StepStatus>
21
+ ui: UIState
22
+ setTab: (tab: 0 | 1 | 2) => void
23
+ setFilter: (f: 'all' | VerdictLabel) => void
24
+ openEvidence: (claimId: string) => void
25
+ closeEvidence: () => void
26
+ startRun: (kind: InputKind, payload: { file?: File; text?: string }) => Promise<void>
27
+ loadSample: () => Promise<void>
28
+ loadFromStorage: () => void
29
+ selectRun: (id: string) => void
30
+ }
31
+
32
+ const initialSteps: Record<StepKey, StepStatus> = {
33
+ ASR: 'idle',
34
+ Claims: 'idle',
35
+ Retrieval: 'idle',
36
+ Verification: 'idle',
37
+ Summary: 'idle',
38
+ }
39
+
40
+ export const useAppStore = create<Store>()(
41
+ persist(
42
+ (set, get) => ({
43
+ runs: [],
44
+ currentRunId: undefined,
45
+ steps: initialSteps,
46
+ ui: { tab: 0, filter: 'all', evidenceOpen: false },
47
+ setTab: (tab) => set((s) => ({ ui: { ...s.ui, tab } })),
48
+ setFilter: (filter) => set((s) => ({ ui: { ...s.ui, filter } })),
49
+ openEvidence: (selectedClaimId) => set((s) => ({ ui: { ...s.ui, evidenceOpen: true, selectedClaimId } })),
50
+ closeEvidence: () => set((s) => ({ ui: { ...s.ui, evidenceOpen: false, selectedClaimId: undefined } })),
51
+
52
+ async startRun(kind, payload) {
53
+ const id = nanoid()
54
+ const startedAt = Date.now()
55
+ const newRun: RunRecord = { id, startedAt, inputKind: kind }
56
+ set((s) => ({ runs: [newRun, ...s.runs].slice(0, 5), currentRunId: id }))
57
+
58
+ // optimistic step progression
59
+ const audio = kind === 'audio'
60
+ set({ steps: {
61
+ ASR: audio ? 'running' : 'done',
62
+ Claims: 'idle',
63
+ Retrieval: 'idle',
64
+ Verification: 'idle',
65
+ Summary: 'idle',
66
+ } })
67
+
68
+ try {
69
+ let report: CallReport
70
+ if (kind === 'audio' && payload.file) {
71
+ report = await processAudio(payload.file)
72
+ } else if (kind === 'transcript' && payload.text) {
73
+ report = await processTranscript(payload.text)
74
+ } else {
75
+ throw new Error('Missing input')
76
+ }
77
+
78
+ // set done
79
+ set({ steps: { ASR: audio ? 'done' : 'done', Claims: 'done', Retrieval: 'done', Verification: 'done', Summary: 'done' } })
80
+ set((s) => ({ runs: s.runs.map((r) => (r.id === id ? { ...r, report } : r)) }))
81
+ } catch (err: any) {
82
+ set({ steps: { ...initialSteps, ASR: audio ? 'error' : 'done', Claims: 'error' } })
83
+ const message = err?.response?.data?.detail || err?.message || 'Failed to process request'
84
+ set((s) => ({ runs: s.runs.map((r) => (r.id === id ? { ...r, error: message } : r)) }))
85
+ }
86
+ },
87
+
88
+ async loadSample() {
89
+ const id = nanoid()
90
+ const startedAt = Date.now()
91
+ const newRun: RunRecord = { id, startedAt, inputKind: 'sample' }
92
+ set((s) => ({ runs: [newRun, ...s.runs].slice(0, 5), currentRunId: id }))
93
+ set({ steps: { ASR: 'done', Claims: 'done', Retrieval: 'done', Verification: 'done', Summary: 'done' } })
94
+ const data = await import('@/mocks/sampleReport.json')
95
+ const report = data.default
96
+ set((s) => ({ runs: s.runs.map((r) => (r.id === id ? { ...r, report } : r)) }))
97
+ },
98
+
99
+ loadFromStorage() {
100
+ // noop: handled by persist middleware rehydration
101
+ },
102
+
103
+ selectRun(id) {
104
+ set({ currentRunId: id })
105
+ },
106
+ }),
107
+ {
108
+ name: 'cc_runs',
109
+ partialize: (s) => ({ runs: s.runs, currentRunId: s.currentRunId }),
110
+ }
111
+ )
112
+ )
ui/claimcheck-ui/src/store/nanoid.ts ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ const alphabet = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz-'
2
+
3
+ export function nanoid(size = 12): string {
4
+ let id = ''
5
+ const bytes = crypto.getRandomValues(new Uint8Array(size))
6
+ for (let i = 0; i < size; i++) {
7
+ id += alphabet[bytes[i] & 63]
8
+ }
9
+ return id
10
+ }
ui/claimcheck-ui/tailwind.config.ts ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Config } from 'tailwindcss'
2
+
3
+ export default {
4
+ darkMode: ['class'],
5
+ content: [
6
+ './index.html',
7
+ './src/**/*.{ts,tsx}',
8
+ ],
9
+ theme: {
10
+ extend: {
11
+ colors: {
12
+ border: 'hsl(var(--border))',
13
+ input: 'hsl(var(--input))',
14
+ ring: 'hsl(var(--ring))',
15
+ background: 'hsl(var(--background))',
16
+ foreground: 'hsl(var(--foreground))',
17
+ primary: {
18
+ DEFAULT: 'hsl(var(--primary))',
19
+ foreground: 'hsl(var(--primary-foreground))',
20
+ },
21
+ secondary: {
22
+ DEFAULT: 'hsl(var(--secondary))',
23
+ foreground: 'hsl(var(--secondary-foreground))',
24
+ },
25
+ muted: {
26
+ DEFAULT: 'hsl(var(--muted))',
27
+ foreground: 'hsl(var(--muted-foreground))',
28
+ },
29
+ accent: {
30
+ DEFAULT: 'hsl(var(--accent))',
31
+ foreground: 'hsl(var(--accent-foreground))',
32
+ },
33
+ destructive: {
34
+ DEFAULT: 'hsl(var(--destructive))',
35
+ foreground: 'hsl(var(--destructive-foreground))',
36
+ },
37
+ card: {
38
+ DEFAULT: 'hsl(var(--card))',
39
+ foreground: 'hsl(var(--card-foreground))',
40
+ },
41
+ supported: '#16a34a',
42
+ refuted: '#dc2626',
43
+ insufficient: '#f59e0b',
44
+ },
45
+ borderRadius: {
46
+ lg: 'var(--radius)',
47
+ md: 'calc(var(--radius) - 2px)',
48
+ sm: 'calc(var(--radius) - 4px)',
49
+ },
50
+ keyframes: {
51
+ 'accordion-down': {
52
+ from: { height: '0' },
53
+ to: { height: 'var(--radix-accordion-content-height)' },
54
+ },
55
+ 'accordion-up': {
56
+ from: { height: 'var(--radix-accordion-content-height)' },
57
+ to: { height: '0' },
58
+ },
59
+ },
60
+ animation: {
61
+ 'accordion-down': 'accordion-down 0.2s ease-out',
62
+ 'accordion-up': 'accordion-up 0.2s ease-out',
63
+ },
64
+ },
65
+ },
66
+ plugins: [],
67
+ } satisfies Config
ui/claimcheck-ui/tsconfig.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "resolveJsonModule": true,
10
+ "isolatedModules": true,
11
+ "esModuleInterop": true,
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "baseUrl": ".",
16
+ "paths": {
17
+ "@/*": ["src/*"]
18
+ }
19
+ },
20
+ "include": ["src"],
21
+ "references": [{ "path": "./tsconfig.node.json" }]
22
+ }
ui/claimcheck-ui/tsconfig.node.json ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true
8
+ },
9
+ "include": ["vite.config.ts"]
10
+ }
ui/claimcheck-ui/vite.config.ts ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react-swc'
3
+ import path from 'path'
4
+
5
+ export default defineConfig({
6
+ plugins: [react()],
7
+ resolve: {
8
+ alias: {
9
+ '@': path.resolve(__dirname, 'src'),
10
+ },
11
+ },
12
+ server: {
13
+ port: 5173,
14
+ host: true,
15
+ },
16
+ })