Spaces:
Runtime error
Runtime error
Commit ·
a45ece3
1
Parent(s): 83fc25d
adding the ui files
Browse files- README.md +9 -1
- app/agents/claims.py +1 -1
- ui/claimcheck-ui/.eslintrc.cjs +17 -0
- ui/claimcheck-ui/.gitignore +8 -0
- ui/claimcheck-ui/.prettierrc +5 -0
- ui/claimcheck-ui/README_RUN_UI.md +39 -0
- ui/claimcheck-ui/index.html +12 -0
- ui/claimcheck-ui/package.json +44 -0
- ui/claimcheck-ui/postcss.config.js +6 -0
- ui/claimcheck-ui/src/App.tsx +120 -0
- ui/claimcheck-ui/src/components/ClaimsTable.tsx +115 -0
- ui/claimcheck-ui/src/components/EvidenceDrawer.tsx +63 -0
- ui/claimcheck-ui/src/components/Header.tsx +45 -0
- ui/claimcheck-ui/src/components/ResultsSummary.tsx +55 -0
- ui/claimcheck-ui/src/components/RunHistory.tsx +34 -0
- ui/claimcheck-ui/src/components/SettingsPanel.tsx +40 -0
- ui/claimcheck-ui/src/components/Stepper.tsx +32 -0
- ui/claimcheck-ui/src/components/TranscriptPanel.tsx +45 -0
- ui/claimcheck-ui/src/components/TranscriptViewer.tsx +38 -0
- ui/claimcheck-ui/src/components/UploadPanel.tsx +79 -0
- ui/claimcheck-ui/src/components/index.ts +10 -0
- ui/claimcheck-ui/src/env.d.ts +9 -0
- ui/claimcheck-ui/src/index.css +74 -0
- ui/claimcheck-ui/src/lib/api.ts +40 -0
- ui/claimcheck-ui/src/lib/types.ts +45 -0
- ui/claimcheck-ui/src/lib/utils.ts +49 -0
- ui/claimcheck-ui/src/main.tsx +25 -0
- ui/claimcheck-ui/src/mocks/sampleReport.json +28 -0
- ui/claimcheck-ui/src/store/appStore.ts +112 -0
- ui/claimcheck-ui/src/store/nanoid.ts +10 -0
- ui/claimcheck-ui/tailwind.config.ts +67 -0
- ui/claimcheck-ui/tsconfig.json +22 -0
- ui/claimcheck-ui/tsconfig.node.json +10 -0
- 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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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":
|
| 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 |
+
})
|