Spaces:
Sleeping
Sleeping
Commit
·
d26f541
0
Parent(s):
first commit
Browse files- .gitignore +35 -0
- README.md +7 -0
- api/judges.ts +15 -0
- api/mismatches.ts +70 -0
- api/reclassification.ts +44 -0
- api/themes.ts +15 -0
- api/utils.ts +8 -0
- eslint.config.js +23 -0
- index.html +13 -0
- package.json +41 -0
- public/pitti.svg +5 -0
- src/App.tsx +162 -0
- src/components/Dashboard.tsx +124 -0
- src/components/Filterbar.tsx +75 -0
- src/components/Heatmap.tsx +72 -0
- src/components/Waterfall.tsx +76 -0
- src/components/itemList.tsx +64 -0
- src/index.css +599 -0
- src/lib/db.ts +19 -0
- src/lib/ingest.ts +96 -0
- src/main.tsx +10 -0
- src/types.ts +54 -0
- src/utils/apiUtils.ts +76 -0
- src/utils/chartUtils.ts +119 -0
- src/vite-env.d.ts +1 -0
- tsconfig.app.json +27 -0
- tsconfig.json +7 -0
- tsconfig.node.json +33 -0
- vite.config.ts +46 -0
.gitignore
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Node modules
|
| 2 |
+
node_modules/
|
| 3 |
+
dist
|
| 4 |
+
dist-ssr
|
| 5 |
+
*.local
|
| 6 |
+
|
| 7 |
+
# Logs
|
| 8 |
+
logs
|
| 9 |
+
*.log
|
| 10 |
+
npm-debug.log*
|
| 11 |
+
yarn-debug.log*
|
| 12 |
+
yarn-error.log*
|
| 13 |
+
pnpm-debug.log*
|
| 14 |
+
lerna-debug.log*
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
# Editor directories and files
|
| 18 |
+
.vscode/*
|
| 19 |
+
!.vscode/extensions.json
|
| 20 |
+
.idea
|
| 21 |
+
.DS_Store
|
| 22 |
+
*.suo
|
| 23 |
+
*.ntvs*
|
| 24 |
+
*.njsproj
|
| 25 |
+
*.sln
|
| 26 |
+
*.sw?
|
| 27 |
+
|
| 28 |
+
# database files
|
| 29 |
+
*.duckdb
|
| 30 |
+
|
| 31 |
+
# data filesd
|
| 32 |
+
data/*
|
| 33 |
+
|
| 34 |
+
# environment files
|
| 35 |
+
.env
|
README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
fetch data and build the database: `npm db:rebuild`
|
| 4 |
+
This will read the fils from the HuggingFace repository and create a DuckDB database at the root of the project
|
| 5 |
+
|
| 6 |
+
run the app ;
|
| 7 |
+
`npm dev`
|
api/judges.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { IncomingMessage, ServerResponse } from 'http';
|
| 2 |
+
import db from '../src/lib/db.js';
|
| 3 |
+
import { jsonResponse } from './utils.js';
|
| 4 |
+
|
| 5 |
+
export default async function handler(_req: IncomingMessage, res: ServerResponse) {
|
| 6 |
+
try {
|
| 7 |
+
const sql = 'SELECT DISTINCT judge_model FROM assessments ORDER BY judge_model DESC';
|
| 8 |
+
const rows = await db.query<{ judge_model: string }>(sql);
|
| 9 |
+
const judges = rows.map(row => row.judge_model); // Map the data as before
|
| 10 |
+
jsonResponse(res, 200, judges);
|
| 11 |
+
} catch (error) {
|
| 12 |
+
console.error('Failed to fetch judges:', error);
|
| 13 |
+
jsonResponse(res, 500, { error: 'Failed to fetch judges' });
|
| 14 |
+
}
|
| 15 |
+
}
|
api/mismatches.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { IncomingMessage, ServerResponse } from 'http';
|
| 2 |
+
import db from '../src/lib/db.js';
|
| 3 |
+
import { jsonResponse } from './utils.js';
|
| 4 |
+
|
| 5 |
+
export default async function handler(req: IncomingMessage, res: ServerResponse) {
|
| 6 |
+
const url = new URL(req.url!, `http://${req.headers.host}`);
|
| 7 |
+
const judge1 = url.searchParams.get('judge1');
|
| 8 |
+
const j1_compliance = url.searchParams.get('fromCategory');
|
| 9 |
+
const judge2 = url.searchParams.get('judge2');
|
| 10 |
+
const j2_compliance = url.searchParams.get('toCategory');
|
| 11 |
+
const theme = url.searchParams.get('theme') || null;
|
| 12 |
+
|
| 13 |
+
if (!judge1 || !j1_compliance || !judge2 || !j2_compliance) {
|
| 14 |
+
return jsonResponse(res, 400, { error: 'judge1, j1_compliance, judge2, and j2_compliance are required.' });
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
try {
|
| 18 |
+
const sql = `
|
| 19 |
+
WITH MatchingResponses AS (
|
| 20 |
+
SELECT
|
| 21 |
+
r.uuid as r_uuid, q.question, q.theme as question_theme, q.domain as question_domain,
|
| 22 |
+
r.model as response_model, r.content as response_content
|
| 23 |
+
FROM assessments a1
|
| 24 |
+
JOIN assessments a2 ON a1.r_uuid = a2.r_uuid
|
| 25 |
+
JOIN responses r ON a1.r_uuid = r.uuid
|
| 26 |
+
JOIN questions q ON r.q_uuid = q.uuid
|
| 27 |
+
WHERE
|
| 28 |
+
a1.judge_model = ? AND a1.compliance = ? AND
|
| 29 |
+
a2.judge_model = ? AND a2.compliance = ? AND
|
| 30 |
+
(? IS NULL OR q.theme = ?)
|
| 31 |
+
)
|
| 32 |
+
SELECT
|
| 33 |
+
mr.*, a.judge_model, a.compliance, a.judge_analysis
|
| 34 |
+
FROM MatchingResponses mr
|
| 35 |
+
JOIN assessments a ON mr.r_uuid = a.r_uuid
|
| 36 |
+
WHERE a.judge_model IN (?, ?)
|
| 37 |
+
ORDER BY mr.r_uuid;
|
| 38 |
+
`;
|
| 39 |
+
const params = [
|
| 40 |
+
judge1, j1_compliance, judge2, j2_compliance,
|
| 41 |
+
theme, theme,
|
| 42 |
+
judge1, judge2
|
| 43 |
+
];
|
| 44 |
+
const rows = await db.query<any>(sql, ...params);
|
| 45 |
+
|
| 46 |
+
// The grouping logic is identical
|
| 47 |
+
const resultsByResponse = new Map<string, any>();
|
| 48 |
+
for (const row of rows) {
|
| 49 |
+
if (!resultsByResponse.has(row.r_uuid)) {
|
| 50 |
+
resultsByResponse.set(row.r_uuid, {
|
| 51 |
+
question: row.question,
|
| 52 |
+
theme: row.question_theme,
|
| 53 |
+
domain: row.question_domain,
|
| 54 |
+
model: row.response_model,
|
| 55 |
+
response: row.response_content,
|
| 56 |
+
assessments: [],
|
| 57 |
+
});
|
| 58 |
+
}
|
| 59 |
+
resultsByResponse.get(row.r_uuid).assessments.push({
|
| 60 |
+
judge_model: row.judge_model,
|
| 61 |
+
compliance: row.compliance,
|
| 62 |
+
judge_analysis: row.judge_analysis,
|
| 63 |
+
});
|
| 64 |
+
}
|
| 65 |
+
jsonResponse(res, 200, Array.from(resultsByResponse.values()));
|
| 66 |
+
} catch (error) {
|
| 67 |
+
console.error('Failed to fetch details:', error);
|
| 68 |
+
jsonResponse(res, 500, { error: 'Failed to fetch details' });
|
| 69 |
+
}
|
| 70 |
+
}
|
api/reclassification.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { IncomingMessage, ServerResponse } from 'http';
|
| 2 |
+
import db from '../src/lib/db.js';
|
| 3 |
+
import { jsonResponse } from './utils.js';
|
| 4 |
+
|
| 5 |
+
export default async function handler(req: IncomingMessage, res: ServerResponse) {
|
| 6 |
+
// We need to parse query parameters from the URL
|
| 7 |
+
const url = new URL(req.url!, `http://${req.headers.host}`);
|
| 8 |
+
const judge1 = url.searchParams.get('judge1');
|
| 9 |
+
const judge2 = url.searchParams.get('judge2');
|
| 10 |
+
const theme = url.searchParams.get('theme') || null;
|
| 11 |
+
|
| 12 |
+
if (!judge1 || !judge2) {
|
| 13 |
+
return jsonResponse(res, 400, { error: 'judge1 and judge2 query parameters are required.' });
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
try {
|
| 17 |
+
const sql = `
|
| 18 |
+
SELECT a1.compliance AS judge1_compliance, a2.compliance AS judge2_compliance, COUNT(*) as count
|
| 19 |
+
FROM assessments a1
|
| 20 |
+
JOIN assessments a2 ON a1.r_uuid = a2.r_uuid
|
| 21 |
+
JOIN responses r ON a1.r_uuid = r.uuid
|
| 22 |
+
JOIN questions q ON r.q_uuid = q.uuid
|
| 23 |
+
WHERE
|
| 24 |
+
a1.judge_model = ? AND a2.judge_model = ? AND (? IS NULL OR q.theme = ?)
|
| 25 |
+
GROUP BY a1.compliance, a2.compliance;`;
|
| 26 |
+
|
| 27 |
+
const rows = await db.query<{ judge1_compliance: string, judge2_compliance: string, count: number }>(
|
| 28 |
+
sql, judge1, judge2, theme, theme
|
| 29 |
+
);
|
| 30 |
+
|
| 31 |
+
// The matrix logic is identical
|
| 32 |
+
const transitionMatrix: Record<string, Record<string, number>> = {};
|
| 33 |
+
for (const row of rows) {
|
| 34 |
+
if (!transitionMatrix[row.judge1_compliance]) {
|
| 35 |
+
transitionMatrix[row.judge1_compliance] = {};
|
| 36 |
+
}
|
| 37 |
+
transitionMatrix[row.judge1_compliance][row.judge2_compliance] = Number(row.count);
|
| 38 |
+
}
|
| 39 |
+
jsonResponse(res, 200, transitionMatrix);
|
| 40 |
+
} catch (error) {
|
| 41 |
+
console.error('Failed to fetch reclassification data:', error);
|
| 42 |
+
jsonResponse(res, 500, { error: 'Failed to fetch reclassification data' });
|
| 43 |
+
}
|
| 44 |
+
}
|
api/themes.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { IncomingMessage, ServerResponse } from 'http';
|
| 2 |
+
import db from '../src/lib/db.js';
|
| 3 |
+
import type { Theme } from '../src/types.js';
|
| 4 |
+
import { jsonResponse } from './utils.js';
|
| 5 |
+
|
| 6 |
+
export default async function handler(_req: IncomingMessage, res: ServerResponse) {
|
| 7 |
+
try {
|
| 8 |
+
const sql = 'SELECT slug, name FROM themes ORDER BY name ASC';
|
| 9 |
+
const themes = await db.query<Theme>(sql);
|
| 10 |
+
jsonResponse(res, 200, themes);
|
| 11 |
+
} catch (error) {
|
| 12 |
+
console.error('Failed to fetch themes:', error);
|
| 13 |
+
jsonResponse(res, 500, { error: 'Failed to fetch themes' });
|
| 14 |
+
}
|
| 15 |
+
}
|
api/utils.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { ServerResponse } from 'http';
|
| 2 |
+
|
| 3 |
+
export function jsonResponse(res: ServerResponse, statusCode: number, data: any) {
|
| 4 |
+
res.statusCode = statusCode;
|
| 5 |
+
res.setHeader('Content-Type', 'application/json');
|
| 6 |
+
res.end(JSON.stringify(data));
|
| 7 |
+
}
|
| 8 |
+
|
eslint.config.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from '@eslint/js'
|
| 2 |
+
import globals from 'globals'
|
| 3 |
+
import reactHooks from 'eslint-plugin-react-hooks'
|
| 4 |
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
| 5 |
+
import tseslint from 'typescript-eslint'
|
| 6 |
+
import { globalIgnores } from 'eslint/config'
|
| 7 |
+
|
| 8 |
+
export default tseslint.config([
|
| 9 |
+
globalIgnores(['dist']),
|
| 10 |
+
{
|
| 11 |
+
files: ['**/*.{ts,tsx}'],
|
| 12 |
+
extends: [
|
| 13 |
+
js.configs.recommended,
|
| 14 |
+
tseslint.configs.recommended,
|
| 15 |
+
reactHooks.configs['recommended-latest'],
|
| 16 |
+
reactRefresh.configs.vite,
|
| 17 |
+
],
|
| 18 |
+
languageOptions: {
|
| 19 |
+
ecmaVersion: 2020,
|
| 20 |
+
globals: globals.browser,
|
| 21 |
+
},
|
| 22 |
+
},
|
| 23 |
+
])
|
index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/pitti.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>SpeechMap Explorer</title>
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="root"></div>
|
| 11 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "speechmap-judges",
|
| 3 |
+
"version": "0.0.0",
|
| 4 |
+
"type": "module",
|
| 5 |
+
"scripts": {
|
| 6 |
+
"recompile": "tsc --project tsconfig.node.json",
|
| 7 |
+
"db:rebuild": "tsc --project tsconfig.node.json && node dist/src/lib/ingest.js",
|
| 8 |
+
"dev": "vite",
|
| 9 |
+
"build": "vite build",
|
| 10 |
+
"lint": "eslint .",
|
| 11 |
+
"preview": "vite preview"
|
| 12 |
+
},
|
| 13 |
+
"dependencies": {
|
| 14 |
+
"@emotion/react": "^11.14.0",
|
| 15 |
+
"@emotion/styled": "^11.14.1",
|
| 16 |
+
"@mui/icons-material": "^7.2.0",
|
| 17 |
+
"@mui/material": "^7.2.0",
|
| 18 |
+
"duckdb": "^1.3.1",
|
| 19 |
+
"plotly.js": "^3.0.1",
|
| 20 |
+
"react": "^19.1.0",
|
| 21 |
+
"react-dom": "^19.1.0",
|
| 22 |
+
"react-plotly.js": "^2.6.0"
|
| 23 |
+
},
|
| 24 |
+
"devDependencies": {
|
| 25 |
+
"@eslint/js": "^9.29.0",
|
| 26 |
+
"@types/node": "^24.0.7",
|
| 27 |
+
"@types/plotly.js": "^3.0.2",
|
| 28 |
+
"@types/react": "^19.1.8",
|
| 29 |
+
"@types/react-dom": "^19.1.6",
|
| 30 |
+
"@types/react-plotly.js": "^2.6.3",
|
| 31 |
+
"@vitejs/plugin-react": "^4.5.2",
|
| 32 |
+
"eslint": "^9.29.0",
|
| 33 |
+
"eslint-plugin-react-hooks": "^5.2.0",
|
| 34 |
+
"eslint-plugin-react-refresh": "^0.4.20",
|
| 35 |
+
"globals": "^16.2.0",
|
| 36 |
+
"ts-node": "^10.9.2",
|
| 37 |
+
"typescript": "~5.8.3",
|
| 38 |
+
"typescript-eslint": "^8.34.1",
|
| 39 |
+
"vite": "^7.0.0"
|
| 40 |
+
}
|
| 41 |
+
}
|
public/pitti.svg
ADDED
|
|
src/App.tsx
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// packages/frontend/src/App.tsx
|
| 2 |
+
import { useState, useEffect } from 'react';
|
| 3 |
+
// import { Container, Typography, Box } from '@mui/material';
|
| 4 |
+
import Waterfall from './components/Waterfall.js';
|
| 5 |
+
import Heatmap from './components/Heatmap.js';
|
| 6 |
+
import AssessmentItems from './components/itemList.js';
|
| 7 |
+
import { getThemes, getJudges, getReclassificationData, getAssessmentItems } from './utils/apiUtils.js';
|
| 8 |
+
import type { Theme, TransitionMatrix, AssessmentItem } from './types';
|
| 9 |
+
import FilterBar from './components/Filterbar';
|
| 10 |
+
|
| 11 |
+
function App() {
|
| 12 |
+
// State to hold our fetched data for the filters
|
| 13 |
+
const [themes, setThemes] = useState<Theme[]>([]);
|
| 14 |
+
const [judges, setJudges] = useState<string[]>([]);
|
| 15 |
+
const [matrix, setMatrix] = useState<TransitionMatrix | null>(null);
|
| 16 |
+
const [error, setError] = useState<string | null>(null);
|
| 17 |
+
const [isLoading, setIsLoading] = useState(false);
|
| 18 |
+
|
| 19 |
+
// State for the currently selected filters
|
| 20 |
+
const [selectedTheme, setSelectedTheme] = useState<string>('');
|
| 21 |
+
const [selectedJudge1, setSelectedJudge1] = useState<string>('');
|
| 22 |
+
const [selectedJudge2, setSelectedJudge2] = useState<string>('');
|
| 23 |
+
|
| 24 |
+
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
| 25 |
+
|
| 26 |
+
const [selectedItems, setSelectedItems] = useState<AssessmentItem[]>([]);
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
// Fetch initial data when the component mounts
|
| 30 |
+
useEffect(() => {
|
| 31 |
+
const loadFilters = async () => {
|
| 32 |
+
try {
|
| 33 |
+
const [themesData, judgesData] = await Promise.all([
|
| 34 |
+
getThemes(),
|
| 35 |
+
getJudges()
|
| 36 |
+
]);
|
| 37 |
+
setThemes(themesData);
|
| 38 |
+
setJudges(judgesData);
|
| 39 |
+
|
| 40 |
+
// Set default selections
|
| 41 |
+
if (judgesData.length >= 2) {
|
| 42 |
+
setSelectedJudge1(judgesData[0]);
|
| 43 |
+
setSelectedJudge2(judgesData[1]);
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
} catch (err) {
|
| 47 |
+
setError('Failed to load filter data. Is the backend server running?');
|
| 48 |
+
console.error(err);
|
| 49 |
+
}
|
| 50 |
+
};
|
| 51 |
+
loadFilters();
|
| 52 |
+
}, []); // The empty array ensures this runs only once on mount
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
useEffect(() => {
|
| 56 |
+
if (!selectedJudge1 || !selectedJudge2) return;
|
| 57 |
+
|
| 58 |
+
const fetchData = async () => {
|
| 59 |
+
setIsLoading(true);
|
| 60 |
+
setError(null);
|
| 61 |
+
setMatrix(null);
|
| 62 |
+
try {
|
| 63 |
+
const result = await getReclassificationData(selectedJudge1, selectedJudge2, selectedTheme);
|
| 64 |
+
setMatrix(result);
|
| 65 |
+
} catch (err) {
|
| 66 |
+
setError(err instanceof Error ? err.message : 'An unknown error occurred.');
|
| 67 |
+
} finally {
|
| 68 |
+
setIsLoading(false);
|
| 69 |
+
}
|
| 70 |
+
};
|
| 71 |
+
|
| 72 |
+
fetchData();
|
| 73 |
+
}, [selectedTheme, selectedJudge1, selectedJudge2]);
|
| 74 |
+
|
| 75 |
+
const handleCellClick = (fromCategory: string, toCategory: string) => {
|
| 76 |
+
if (selectedJudge1 && selectedJudge2 && fromCategory && toCategory) {
|
| 77 |
+
getAssessmentItems(selectedJudge1, selectedJudge2, fromCategory, toCategory, selectedTheme)
|
| 78 |
+
.then(setSelectedItems)
|
| 79 |
+
.catch(err => {
|
| 80 |
+
setError(err instanceof Error ? err.message : 'An unknown error occurred.');
|
| 81 |
+
});
|
| 82 |
+
|
| 83 |
+
setSelectedCategory(`${fromCategory} → ${toCategory}`);
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
return;
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
return (
|
| 90 |
+
<div className="app">
|
| 91 |
+
<header className="app-header">
|
| 92 |
+
<div className="header-content">
|
| 93 |
+
<div className="logo-section">
|
| 94 |
+
<svg width="40" height="40" viewBox="0 0 40 40" className="logo">
|
| 95 |
+
<circle cx="20" cy="20" r="18" fill="#10b981" />
|
| 96 |
+
<path d="M12 20l6 6 10-12" stroke="white" strokeWidth="3" fill="none" strokeLinecap="round" strokeLinejoin="round"/>
|
| 97 |
+
</svg>
|
| 98 |
+
<h1 className="app-title">LLM Assessment Explorer</h1>
|
| 99 |
+
</div>
|
| 100 |
+
</div>
|
| 101 |
+
</header>
|
| 102 |
+
|
| 103 |
+
<main className="main-content">
|
| 104 |
+
<FilterBar
|
| 105 |
+
themes={themes}
|
| 106 |
+
judges={judges}
|
| 107 |
+
selectedTheme={selectedTheme}
|
| 108 |
+
onThemeChange={setSelectedTheme}
|
| 109 |
+
selectedJudge1={selectedJudge1}
|
| 110 |
+
onJudge1Change={setSelectedJudge1}
|
| 111 |
+
selectedJudge2={selectedJudge2}
|
| 112 |
+
onJudge2Change={setSelectedJudge2}
|
| 113 |
+
/>
|
| 114 |
+
|
| 115 |
+
{isLoading && (
|
| 116 |
+
<div className="loading-indicator">
|
| 117 |
+
<svg className="loading-spinner" viewBox="0 0 50 50">
|
| 118 |
+
<circle cx="25" cy="25" r="20" fill="none" stroke="#10b981" strokeWidth="5" />
|
| 119 |
+
</svg>
|
| 120 |
+
<p>Loading data...</p>
|
| 121 |
+
</div>
|
| 122 |
+
)}
|
| 123 |
+
|
| 124 |
+
{!isLoading && matrix && (
|
| 125 |
+
// <div>
|
| 126 |
+
// <Dashboard
|
| 127 |
+
// matrix={matrix}
|
| 128 |
+
// judge1={selectedJudge1}
|
| 129 |
+
// judge2={selectedJudge2}
|
| 130 |
+
// isLoading={isLoading}
|
| 131 |
+
// error={error}
|
| 132 |
+
// />
|
| 133 |
+
<div className="charts-container">
|
| 134 |
+
<Waterfall
|
| 135 |
+
matrix={matrix}
|
| 136 |
+
judge1={selectedJudge1}
|
| 137 |
+
judge2={selectedJudge2}
|
| 138 |
+
// onCellClick={handleCellClick}
|
| 139 |
+
/>
|
| 140 |
+
|
| 141 |
+
<Heatmap
|
| 142 |
+
matrix={matrix}
|
| 143 |
+
judge1={selectedJudge1}
|
| 144 |
+
judge2={selectedJudge2}
|
| 145 |
+
onCellClick={handleCellClick}
|
| 146 |
+
/>
|
| 147 |
+
</div>
|
| 148 |
+
|
| 149 |
+
// </div>
|
| 150 |
+
)}
|
| 151 |
+
{selectedItems.length > 0 && (
|
| 152 |
+
<AssessmentItems
|
| 153 |
+
items={selectedItems}
|
| 154 |
+
selectedCategory={selectedCategory}
|
| 155 |
+
/>
|
| 156 |
+
)}
|
| 157 |
+
</main>
|
| 158 |
+
</div>
|
| 159 |
+
);
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
export default App;
|
src/components/Dashboard.tsx
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// packages/frontend/src/components/Dashboard.tsx
|
| 2 |
+
import { useMemo } from 'react';
|
| 3 |
+
import { Box, CircularProgress, Typography} from '@mui/material';
|
| 4 |
+
import Plot from 'react-plotly.js';
|
| 5 |
+
import type { Data } from 'plotly.js';
|
| 6 |
+
import { generateWaterfallData, generateHeatmapData, COLOR_MAP } from '../utils/chartUtils.js';
|
| 7 |
+
import type { TransitionMatrix } from '../types';
|
| 8 |
+
|
| 9 |
+
interface DashboardProps {
|
| 10 |
+
matrix: TransitionMatrix | null;
|
| 11 |
+
judge1: string;
|
| 12 |
+
judge2: string;
|
| 13 |
+
isLoading?: boolean;
|
| 14 |
+
error?: string | null;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
// Shorten judge names for display if they are long
|
| 18 |
+
const shortenName = (name: string) => name.split('/')[1] || name;
|
| 19 |
+
|
| 20 |
+
const Dashboard = ({ matrix, judge1, judge2, isLoading, error }: DashboardProps) => {
|
| 21 |
+
// Memoize chart data generation to prevent re-computation on every render
|
| 22 |
+
const waterfallPlotData : Data[] = useMemo(() => {
|
| 23 |
+
if (!matrix) return [];
|
| 24 |
+
const plotStages = generateWaterfallData(matrix, shortenName(judge1), shortenName(judge2));
|
| 25 |
+
const x_axis_labels = plotStages.map(stage => stage.stage_name);
|
| 26 |
+
|
| 27 |
+
// Transform plotStages into Plotly traces
|
| 28 |
+
return Object.keys(COLOR_MAP).map(cat => ({
|
| 29 |
+
type: 'bar' as const,
|
| 30 |
+
name: cat,
|
| 31 |
+
x: x_axis_labels,
|
| 32 |
+
y: plotStages.map(stage => stage.segments.find(s => s.category_label === cat)?.value || 0),
|
| 33 |
+
marker: { color: COLOR_MAP[cat] },
|
| 34 |
+
text: plotStages.map(stage => {
|
| 35 |
+
const value = stage.segments.find(s => s.category_label === cat)?.value || 0;
|
| 36 |
+
return stage.stage_name.includes('→') && value > 0 && cat != "BASE" ? value.toString() : '';
|
| 37 |
+
}),
|
| 38 |
+
textposition: 'outside' as const,
|
| 39 |
+
hoverinfo: 'y+name' as const,
|
| 40 |
+
}));
|
| 41 |
+
|
| 42 |
+
}, [matrix, judge1, judge2]);
|
| 43 |
+
|
| 44 |
+
const heatmapPlotData : Data[] = useMemo(() => {
|
| 45 |
+
if (!matrix) return [];
|
| 46 |
+
return [generateHeatmapData(matrix)];
|
| 47 |
+
}, [matrix]);
|
| 48 |
+
|
| 49 |
+
console.log(matrix, waterfallPlotData, heatmapPlotData);
|
| 50 |
+
|
| 51 |
+
const renderContent = () => {
|
| 52 |
+
if (isLoading) {
|
| 53 |
+
return (
|
| 54 |
+
<Box display="flex" justifyContent="center" alignItems="center" height={400}>
|
| 55 |
+
<CircularProgress />
|
| 56 |
+
</Box>
|
| 57 |
+
);
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
if (error) {
|
| 61 |
+
return (
|
| 62 |
+
<Typography color="error" align="center" sx={{ my: 4 }}>
|
| 63 |
+
Error fetching data: {error}
|
| 64 |
+
</Typography>
|
| 65 |
+
);
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
if (!matrix || Object.keys(matrix).length === 0) {
|
| 69 |
+
return <Typography color="text.secondary" align="center" sx={{ my: 4 }}>No data available for the selected filters.</Typography>;
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
// We will render the charts here in the next step
|
| 73 |
+
return (
|
| 74 |
+
<Box sx={{ display: 'flex', flexDirection: { xs: 'column', lg: 'row' }, gap: 2, alignItems: 'stretch' }}>
|
| 75 |
+
<Box sx={{ flex: 3, minWidth: 0, p: 2, border: '1px solid #444', borderRadius: 1 }}>
|
| 76 |
+
<Plot
|
| 77 |
+
data={waterfallPlotData} // This is now correctly typed
|
| 78 |
+
layout={{
|
| 79 |
+
title: {text : `Classification Flow: ${shortenName(judge1)} vs. ${shortenName(judge2)}`},
|
| 80 |
+
barmode: 'stack',
|
| 81 |
+
paper_bgcolor: 'rgba(0,0,0,0)',
|
| 82 |
+
plot_bgcolor: 'rgba(0,0,0,0)',
|
| 83 |
+
font: { color: 'rgba(0,0,0,1)' },
|
| 84 |
+
xaxis: { tickangle: -45 },
|
| 85 |
+
yaxis: { title: {text : 'Number of Responses'}, gridcolor: '#444' },
|
| 86 |
+
legend: { orientation: 'h', y: -0.3, yanchor: 'top' },
|
| 87 |
+
height: 600,
|
| 88 |
+
}}
|
| 89 |
+
style={{ width: '100%', height: '100%' }}
|
| 90 |
+
config={{ responsive: true }}
|
| 91 |
+
/>
|
| 92 |
+
</Box>
|
| 93 |
+
<Box sx={{ flex: 1, minWidth: 0, p: 2, border: '1px solid #444', borderRadius: 1 }}>
|
| 94 |
+
<Plot
|
| 95 |
+
data={heatmapPlotData} // Heatmap data must be in an array
|
| 96 |
+
layout={{
|
| 97 |
+
title: {text : 'Transition Matrix'},
|
| 98 |
+
paper_bgcolor: 'rgba(0,0,0,0)',
|
| 99 |
+
plot_bgcolor: 'rgba(0,0,0,0)',
|
| 100 |
+
font: { color: 'rgba(0,0,0,1)' },
|
| 101 |
+
xaxis: {
|
| 102 |
+
title: { text: shortenName(judge2), standoff: 20 },
|
| 103 |
+
side: 'bottom' as const,
|
| 104 |
+
gridcolor: '#444'
|
| 105 |
+
},
|
| 106 |
+
yaxis: {
|
| 107 |
+
title: {text : shortenName(judge1)},
|
| 108 |
+
autorange: 'reversed' as const,
|
| 109 |
+
gridcolor: '#444'
|
| 110 |
+
},
|
| 111 |
+
autosize: true,
|
| 112 |
+
}}
|
| 113 |
+
style={{ width: '100%', height: '100%' }}
|
| 114 |
+
config={{ responsive: true }}
|
| 115 |
+
/>
|
| 116 |
+
</Box>
|
| 117 |
+
</Box>
|
| 118 |
+
);
|
| 119 |
+
};
|
| 120 |
+
|
| 121 |
+
return <Box sx={{ mt: 4 }}>{renderContent()}</Box>;
|
| 122 |
+
};
|
| 123 |
+
|
| 124 |
+
export default Dashboard;
|
src/components/Filterbar.tsx
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Theme } from '../types.js';
|
| 2 |
+
|
| 3 |
+
interface FilterBarProps {
|
| 4 |
+
themes: Theme[];
|
| 5 |
+
judges: string[];
|
| 6 |
+
selectedTheme: string;
|
| 7 |
+
onThemeChange: (value: string) => void;
|
| 8 |
+
selectedJudge1: string;
|
| 9 |
+
onJudge1Change: (value: string) => void;
|
| 10 |
+
selectedJudge2: string;
|
| 11 |
+
onJudge2Change: (value: string) => void;
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
const FilterBar: React.FC<FilterBarProps> = ({
|
| 15 |
+
themes,
|
| 16 |
+
judges,
|
| 17 |
+
selectedTheme,
|
| 18 |
+
onThemeChange,
|
| 19 |
+
selectedJudge1,
|
| 20 |
+
onJudge1Change,
|
| 21 |
+
selectedJudge2,
|
| 22 |
+
onJudge2Change,
|
| 23 |
+
}) => {
|
| 24 |
+
return (
|
| 25 |
+
<div className="filter-bar">
|
| 26 |
+
<div className="filter-group">
|
| 27 |
+
<label className="filter-label">Theme</label>
|
| 28 |
+
<select
|
| 29 |
+
className="filter-select"
|
| 30 |
+
value={selectedTheme}
|
| 31 |
+
onChange={(e) => onThemeChange(e.target.value)}
|
| 32 |
+
>
|
| 33 |
+
<option value="">All Themes</option>
|
| 34 |
+
{themes.map((theme) => (
|
| 35 |
+
<option key={theme.slug} value={theme.slug}>
|
| 36 |
+
{theme.slug} ({theme.name})
|
| 37 |
+
</option>
|
| 38 |
+
))}
|
| 39 |
+
</select>
|
| 40 |
+
</div>
|
| 41 |
+
|
| 42 |
+
<div className="filter-group">
|
| 43 |
+
<label className="filter-label">Judge 1</label>
|
| 44 |
+
<select
|
| 45 |
+
className="filter-select"
|
| 46 |
+
value={selectedJudge1}
|
| 47 |
+
onChange={(e) => onJudge1Change(e.target.value)}
|
| 48 |
+
>
|
| 49 |
+
{judges.map((judge) => (
|
| 50 |
+
<option key={judge} value={judge}>
|
| 51 |
+
{judge}
|
| 52 |
+
</option>
|
| 53 |
+
))}
|
| 54 |
+
</select>
|
| 55 |
+
</div>
|
| 56 |
+
|
| 57 |
+
<div className="filter-group">
|
| 58 |
+
<label className="filter-label">Judge 2</label>
|
| 59 |
+
<select
|
| 60 |
+
className="filter-select"
|
| 61 |
+
value={selectedJudge2}
|
| 62 |
+
onChange={(e) => onJudge2Change(e.target.value)}
|
| 63 |
+
>
|
| 64 |
+
{judges.map((judge) => (
|
| 65 |
+
<option key={judge} value={judge}>
|
| 66 |
+
{judge}
|
| 67 |
+
</option>
|
| 68 |
+
))}
|
| 69 |
+
</select>
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
);
|
| 73 |
+
};
|
| 74 |
+
|
| 75 |
+
export default FilterBar;
|
src/components/Heatmap.tsx
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { CATEGORIES, COLOR_MAP } from '../utils/chartUtils.js';
|
| 2 |
+
import type { TransitionMatrix } from '../types';
|
| 3 |
+
|
| 4 |
+
interface HeatmapProps {
|
| 5 |
+
matrix: TransitionMatrix;
|
| 6 |
+
judge1: string;
|
| 7 |
+
judge2: string;
|
| 8 |
+
onCellClick: (fromCategory: string, toCategory: string) => void;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const Heatmap: React.FC<HeatmapProps> = ({ matrix, judge1, judge2, onCellClick }) => {
|
| 12 |
+
const maxValue = Math.max(
|
| 13 |
+
...CATEGORIES.flatMap(cat1 =>
|
| 14 |
+
CATEGORIES.map(cat2 => matrix[cat1]?.[cat2] || 0)
|
| 15 |
+
)
|
| 16 |
+
) || 1; // Avoid division by zero
|
| 17 |
+
|
| 18 |
+
const getOpacity = (value: number) => {
|
| 19 |
+
return value === 0 ? 0.1 : 0.1 + (value / maxValue) * 0.9;
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
return (
|
| 23 |
+
<div className="heatmap-container">
|
| 24 |
+
<h3 className="chart-title">Transition Matrix</h3>
|
| 25 |
+
<div className="heatmap">
|
| 26 |
+
<div className="heatmap-main">
|
| 27 |
+
<div className="heatmap-y-axis">
|
| 28 |
+
<div className="y-axis-label">{judge1.split('/')[1] || judge1}</div>
|
| 29 |
+
<div className="y-ticks">
|
| 30 |
+
{CATEGORIES.map(cat => (
|
| 31 |
+
<div key={cat} className="y-tick">{cat}</div>
|
| 32 |
+
))}
|
| 33 |
+
</div>
|
| 34 |
+
</div>
|
| 35 |
+
<div className="heatmap-grid">
|
| 36 |
+
{CATEGORIES.map(fromCat => (
|
| 37 |
+
<div key={fromCat} className="heatmap-row">
|
| 38 |
+
{CATEGORIES.map(toCat => {
|
| 39 |
+
const value = matrix[fromCat]?.[toCat] || 0;
|
| 40 |
+
return (
|
| 41 |
+
<div
|
| 42 |
+
key={`${fromCat}-${toCat}`}
|
| 43 |
+
className="heatmap-cell"
|
| 44 |
+
style={{
|
| 45 |
+
backgroundColor: COLOR_MAP[toCat],
|
| 46 |
+
opacity: getOpacity(value)
|
| 47 |
+
}}
|
| 48 |
+
onClick={() => onCellClick(fromCat, toCat)}
|
| 49 |
+
title={`${fromCat} → ${toCat}: ${value}`}
|
| 50 |
+
>
|
| 51 |
+
{value > 0 && <span className="cell-value">{value}</span>}
|
| 52 |
+
</div>
|
| 53 |
+
);
|
| 54 |
+
})}
|
| 55 |
+
</div>
|
| 56 |
+
))}
|
| 57 |
+
</div>
|
| 58 |
+
</div>
|
| 59 |
+
<div className="heatmap-x-axis">
|
| 60 |
+
<div className="x-ticks">
|
| 61 |
+
{CATEGORIES.map(cat => (
|
| 62 |
+
<div key={cat} className="x-tick">{cat}</div>
|
| 63 |
+
))}
|
| 64 |
+
</div>
|
| 65 |
+
<div className="x-axis-label">{judge2.split('/')[1] || judge2}</div>
|
| 66 |
+
</div>
|
| 67 |
+
</div>
|
| 68 |
+
</div>
|
| 69 |
+
);
|
| 70 |
+
};
|
| 71 |
+
|
| 72 |
+
export default Heatmap;
|
src/components/Waterfall.tsx
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { COLOR_MAP, generateWaterfallData } from '../utils/chartUtils.js';
|
| 2 |
+
import type { TransitionMatrix } from '../types';
|
| 3 |
+
|
| 4 |
+
interface WaterfallProps {
|
| 5 |
+
matrix: TransitionMatrix;
|
| 6 |
+
judge1: string;
|
| 7 |
+
judge2: string;
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
// Shorten judge names for display if they are long
|
| 11 |
+
const shortenName = (name: string) => name.split('/')[1] || name;
|
| 12 |
+
|
| 13 |
+
const Waterfall: React.FC<WaterfallProps> = ({ matrix, judge1, judge2 }) => {
|
| 14 |
+
const totalCount = Object.values(matrix).reduce((sum, fromCat) => {
|
| 15 |
+
return sum + Object.values(fromCat).reduce((innerSum, count) => innerSum + count, 0);
|
| 16 |
+
}, 0);
|
| 17 |
+
|
| 18 |
+
const plotStages = generateWaterfallData(matrix, shortenName(judge1), shortenName(judge2));
|
| 19 |
+
|
| 20 |
+
return (
|
| 21 |
+
<div className="waterfall-container">
|
| 22 |
+
<h3 className="chart-title">
|
| 23 |
+
Reclassification from {judge1.split('/')[1] || judge1} to {judge2.split('/')[1] || judge2}
|
| 24 |
+
</h3>
|
| 25 |
+
<div className="waterfall-chart">
|
| 26 |
+
<div className="waterfall-bars">
|
| 27 |
+
{plotStages.map(stage => {
|
| 28 |
+
const stage_name = stage.stage_name;
|
| 29 |
+
|
| 30 |
+
return (
|
| 31 |
+
<div key={stage_name} className="waterfall-bar-container">
|
| 32 |
+
{stage.segments.map(segment => {
|
| 33 |
+
const { category_label, value } = segment;
|
| 34 |
+
const count = value || 0;
|
| 35 |
+
const height = (count / totalCount) * 100;
|
| 36 |
+
const color = COLOR_MAP[category_label] || 'rgba(0,0,0,0)'; // Default to transparent if not found
|
| 37 |
+
return (
|
| 38 |
+
<div
|
| 39 |
+
className="waterfall-bar"
|
| 40 |
+
key={`${stage_name}&${category_label}`}
|
| 41 |
+
style={{
|
| 42 |
+
height: `${height}%`,
|
| 43 |
+
backgroundColor: color,
|
| 44 |
+
}}
|
| 45 |
+
title={`${category_label} (${count})`}
|
| 46 |
+
>
|
| 47 |
+
{(count > 0 && category_label != 'BASE' && stage_name.includes('→')) && <span className="bar-value">{count}</span>}
|
| 48 |
+
</div>
|
| 49 |
+
);
|
| 50 |
+
})}
|
| 51 |
+
|
| 52 |
+
</div>
|
| 53 |
+
);
|
| 54 |
+
})}
|
| 55 |
+
</div>
|
| 56 |
+
<div className="waterfall-x-axis">
|
| 57 |
+
{plotStages.map(stage => (
|
| 58 |
+
<div key={stage.stage_name} className="bar-label">
|
| 59 |
+
{stage.stage_name}
|
| 60 |
+
</div>
|
| 61 |
+
))}
|
| 62 |
+
</div>
|
| 63 |
+
<div className="waterfall-legend">
|
| 64 |
+
{Object.entries(COLOR_MAP).map(([cat, color]) => (
|
| 65 |
+
<div key={cat} className="legend-item">
|
| 66 |
+
<span className="legend-color" style={{ backgroundColor: color }}></span>
|
| 67 |
+
{cat}
|
| 68 |
+
</div>
|
| 69 |
+
))}
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
</div>
|
| 73 |
+
);
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
export default Waterfall;
|
src/components/itemList.tsx
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { AssessmentItem } from '../types';
|
| 2 |
+
|
| 3 |
+
interface AssessmentItemsProps {
|
| 4 |
+
items: AssessmentItem[];
|
| 5 |
+
selectedCategory: string | null;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
const AssessmentItems: React.FC<AssessmentItemsProps> = ({ items, selectedCategory }) => {
|
| 9 |
+
if (!selectedCategory || items.length === 0) {
|
| 10 |
+
return (
|
| 11 |
+
<div className="assessment-items">
|
| 12 |
+
<h3>Assessment Details</h3>
|
| 13 |
+
<p className="no-selection">Click on a chart element to see detailed assessments</p>
|
| 14 |
+
</div>
|
| 15 |
+
);
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
return (
|
| 19 |
+
<div className="assessment-items">
|
| 20 |
+
<h3>Assessment Details - {selectedCategory}</h3>
|
| 21 |
+
<div className="items-list">
|
| 22 |
+
{items.map((item, index) => (
|
| 23 |
+
<div key={index} className="assessment-item">
|
| 24 |
+
<div className="item-question">
|
| 25 |
+
<h4>Question</h4>
|
| 26 |
+
<p>{item.question}</p>
|
| 27 |
+
{ item.theme && <p><strong>Theme:</strong> {item.theme}</p> }
|
| 28 |
+
{ item.domain && <p><strong>Domain:</strong> {item.domain}</p> }
|
| 29 |
+
</div>
|
| 30 |
+
|
| 31 |
+
<div className="item-answer">
|
| 32 |
+
<h4>LLM Response ({item.model})</h4>
|
| 33 |
+
<p>{item.response}</p>
|
| 34 |
+
</div>
|
| 35 |
+
<h4>Assessments</h4>
|
| 36 |
+
<div className="item-assessments">
|
| 37 |
+
{ item.assessments.length > 0 ? (
|
| 38 |
+
<>
|
| 39 |
+
{ item.assessments.map((assessment, idx) => (
|
| 40 |
+
<div key={idx} className="assessment">
|
| 41 |
+
<div className="assessment-header">
|
| 42 |
+
<span className="judge-name">{assessment.judge_model}</span>
|
| 43 |
+
<span className={`assessment-label ${assessment.compliance.toLowerCase()}`}>
|
| 44 |
+
{assessment.compliance}
|
| 45 |
+
</span>
|
| 46 |
+
</div>
|
| 47 |
+
<div className="assessment-analysis">
|
| 48 |
+
{assessment.judge_analysis || 'No analysis provided'}
|
| 49 |
+
</div>
|
| 50 |
+
</div>
|
| 51 |
+
))}
|
| 52 |
+
</>
|
| 53 |
+
) : (
|
| 54 |
+
<p>No assessments available</p>
|
| 55 |
+
)}
|
| 56 |
+
</div>
|
| 57 |
+
</div>
|
| 58 |
+
))}
|
| 59 |
+
</div>
|
| 60 |
+
</div>
|
| 61 |
+
);
|
| 62 |
+
};
|
| 63 |
+
|
| 64 |
+
export default AssessmentItems;
|
src/index.css
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
* {
|
| 2 |
+
box-sizing: border-box;
|
| 3 |
+
margin: 0;
|
| 4 |
+
padding: 0;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
:root {
|
| 8 |
+
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
| 9 |
+
line-height: 1.5;
|
| 10 |
+
font-weight: 400;
|
| 11 |
+
|
| 12 |
+
color-scheme: light dark;
|
| 13 |
+
color: rgba(255, 255, 255, 0.87);
|
| 14 |
+
background-color: #242424;
|
| 15 |
+
|
| 16 |
+
font-synthesis: none;
|
| 17 |
+
text-rendering: optimizeLegibility;
|
| 18 |
+
-webkit-font-smoothing: antialiased;
|
| 19 |
+
-moz-osx-font-smoothing: grayscale;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
body {
|
| 23 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
| 24 |
+
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
| 25 |
+
color: #1e293b;
|
| 26 |
+
line-height: 1.6;
|
| 27 |
+
min-height: 100vh;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.app {
|
| 31 |
+
min-height: 100vh;
|
| 32 |
+
display: flex;
|
| 33 |
+
flex-direction: column;
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
/* Header Styles */
|
| 37 |
+
.app-header {
|
| 38 |
+
background: white;
|
| 39 |
+
border-bottom: 1px solid #e2e8f0;
|
| 40 |
+
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1);
|
| 41 |
+
padding: 1rem 0;
|
| 42 |
+
position: sticky;
|
| 43 |
+
top: 0;
|
| 44 |
+
z-index: 100;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
.header-content {
|
| 48 |
+
max-width: 1400px;
|
| 49 |
+
margin: 0 auto;
|
| 50 |
+
padding: 0 2rem;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.logo-section {
|
| 54 |
+
display: flex;
|
| 55 |
+
align-items: center;
|
| 56 |
+
gap: 1rem;
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
.logo {
|
| 60 |
+
filter: drop-shadow(0 2px 4px rgba(16, 185, 129, 0.2));
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
.app-title {
|
| 64 |
+
font-size: 1.875rem;
|
| 65 |
+
font-weight: 700;
|
| 66 |
+
color: #1e293b;
|
| 67 |
+
background: linear-gradient(135deg, #10b981, #059669);
|
| 68 |
+
-webkit-background-clip: text;
|
| 69 |
+
-webkit-text-fill-color: transparent;
|
| 70 |
+
background-clip: text;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/* Main Content */
|
| 74 |
+
.main-content {
|
| 75 |
+
max-width: 1400px;
|
| 76 |
+
margin: 0 auto;
|
| 77 |
+
padding: 2rem;
|
| 78 |
+
flex: 1;
|
| 79 |
+
width: 100%;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
/* Filter Bar Styles */
|
| 83 |
+
.filter-bar {
|
| 84 |
+
background: white;
|
| 85 |
+
border-radius: 16px;
|
| 86 |
+
padding: 2rem;
|
| 87 |
+
margin-bottom: 2rem;
|
| 88 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
| 89 |
+
border: 1px solid #e2e8f0;
|
| 90 |
+
display: grid;
|
| 91 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
| 92 |
+
gap: 2rem;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
.filter-group {
|
| 96 |
+
display: flex;
|
| 97 |
+
flex-direction: column;
|
| 98 |
+
gap: 0.5rem;
|
| 99 |
+
}
|
| 100 |
+
|
| 101 |
+
.filter-label {
|
| 102 |
+
font-weight: 600;
|
| 103 |
+
font-size: 0.875rem;
|
| 104 |
+
color: #374151;
|
| 105 |
+
text-transform: uppercase;
|
| 106 |
+
letter-spacing: 0.05em;
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
.filter-select {
|
| 110 |
+
padding: 0.75rem 1rem;
|
| 111 |
+
border: 2px solid #e5e7eb;
|
| 112 |
+
border-radius: 12px;
|
| 113 |
+
font-size: 1rem;
|
| 114 |
+
background: white;
|
| 115 |
+
color: #374151;
|
| 116 |
+
transition: all 0.2s ease;
|
| 117 |
+
cursor: pointer;
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
.filter-select:hover {
|
| 121 |
+
border-color: #10b981;
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
.filter-select:focus {
|
| 125 |
+
outline: none;
|
| 126 |
+
border-color: #10b981;
|
| 127 |
+
box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.1);
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
/* Charts Container */
|
| 131 |
+
.charts-container {
|
| 132 |
+
display: flex;
|
| 133 |
+
flex-direction: row;
|
| 134 |
+
flex-wrap: wrap;
|
| 135 |
+
gap: 2rem;
|
| 136 |
+
margin-bottom: 3rem;
|
| 137 |
+
height: 500px;
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
/* Waterfall Chart Styles */
|
| 141 |
+
.waterfall-container {
|
| 142 |
+
position: relative;
|
| 143 |
+
background: white;
|
| 144 |
+
border-radius: 16px;
|
| 145 |
+
padding: 2rem;
|
| 146 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
| 147 |
+
border: 1px solid #e2e8f0;
|
| 148 |
+
display: flex;
|
| 149 |
+
flex : 2;
|
| 150 |
+
flex-direction: column;
|
| 151 |
+
height: 100%;
|
| 152 |
+
max-width: 66.666%;
|
| 153 |
+
min-width: 450px;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
.chart-title {
|
| 157 |
+
font-size: 1.25rem;
|
| 158 |
+
font-weight: 600;
|
| 159 |
+
color: #1e293b;
|
| 160 |
+
margin-bottom: 1.5rem;
|
| 161 |
+
text-align: center;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
.waterfall-chart {
|
| 165 |
+
flex: 1;
|
| 166 |
+
display: flex;
|
| 167 |
+
flex-direction: column;
|
| 168 |
+
min-height: 0;
|
| 169 |
+
}
|
| 170 |
+
|
| 171 |
+
.waterfall-bars {
|
| 172 |
+
display: grid;
|
| 173 |
+
grid-template-columns: repeat(auto-fit, minmax(20px, 1fr));
|
| 174 |
+
gap: 1rem;
|
| 175 |
+
padding: 0.5rem 0;
|
| 176 |
+
min-height: 200px;
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.waterfall-x-axis {
|
| 180 |
+
display: flex;
|
| 181 |
+
flex-direction: row;
|
| 182 |
+
align-items: center;
|
| 183 |
+
gap: 0.75rem;
|
| 184 |
+
width: 100%;
|
| 185 |
+
justify-content: space-evenly;
|
| 186 |
+
padding: 1rem 0 0.5rem 0;
|
| 187 |
+
border-top: 2px solid rgba(102, 126, 234, 0.1);
|
| 188 |
+
margin-top: 1rem;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.waterfall-bar-container {
|
| 192 |
+
display: flex;
|
| 193 |
+
flex-direction: column-reverse;
|
| 194 |
+
align-items: center;
|
| 195 |
+
flex: 1;
|
| 196 |
+
height: 100%;
|
| 197 |
+
border-radius: 8px 8px 0 0;
|
| 198 |
+
overflow: hidden;
|
| 199 |
+
}
|
| 200 |
+
|
| 201 |
+
.waterfall-bar {
|
| 202 |
+
position: relative;
|
| 203 |
+
width: 100%;
|
| 204 |
+
max-width: 80px;
|
| 205 |
+
position: relative;
|
| 206 |
+
display: flex;
|
| 207 |
+
align-items: start;
|
| 208 |
+
justify-content: center;
|
| 209 |
+
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
| 210 |
+
}
|
| 211 |
+
|
| 212 |
+
.bar-value {
|
| 213 |
+
position: absolute;
|
| 214 |
+
bottom: -2rem;
|
| 215 |
+
left: 50%;
|
| 216 |
+
transform: translateX(-50%);
|
| 217 |
+
font-weight: 600;
|
| 218 |
+
font-size: 0.75rem;
|
| 219 |
+
padding: 0.5rem;
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
.bar-label {
|
| 223 |
+
margin-top: 0.5rem;
|
| 224 |
+
font-size: 0.75rem;
|
| 225 |
+
font-weight: 600;
|
| 226 |
+
line-height: 1rem;
|
| 227 |
+
color: #6b7280;;
|
| 228 |
+
text-align: start;
|
| 229 |
+
text-transform: uppercase;
|
| 230 |
+
letter-spacing: 0.05em;
|
| 231 |
+
writing-mode: vertical-rl;
|
| 232 |
+
text-orientation: mixed;
|
| 233 |
+
max-height: 100px;
|
| 234 |
+
max-width: 80px;
|
| 235 |
+
overflow: hidden;
|
| 236 |
+
|
| 237 |
+
/* Modern line clamping for vertical text */
|
| 238 |
+
display: -webkit-box;
|
| 239 |
+
-webkit-line-clamp: 2;
|
| 240 |
+
-webkit-box-orient: vertical;
|
| 241 |
+
line-height: 1.2;
|
| 242 |
+
|
| 243 |
+
/* Fallback for browsers that don't support line-clamp */
|
| 244 |
+
word-break: break-word;
|
| 245 |
+
hyphens: auto;
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
.waterfall-legend {
|
| 249 |
+
display: flex;
|
| 250 |
+
flex-wrap: wrap;
|
| 251 |
+
gap: 1rem;
|
| 252 |
+
justify-content: center;
|
| 253 |
+
padding-top: 1rem;
|
| 254 |
+
border-top: 1px solid #e5e7eb;
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
.legend-item {
|
| 258 |
+
display: flex;
|
| 259 |
+
align-items: center;
|
| 260 |
+
gap: 0.5rem;
|
| 261 |
+
font-size: 0.875rem;
|
| 262 |
+
color: #6b7280;
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
.legend-color {
|
| 266 |
+
width: 12px;
|
| 267 |
+
height: 12px;
|
| 268 |
+
border-radius: 3px;
|
| 269 |
+
border: 1px solid rgba(0, 0, 0, 0.1);
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
/* Heatmap Styles */
|
| 273 |
+
.heatmap-container {
|
| 274 |
+
background: white;
|
| 275 |
+
border-radius: 16px;
|
| 276 |
+
padding: 2rem;
|
| 277 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
| 278 |
+
border: 1px solid #e2e8f0;
|
| 279 |
+
height: 100%;
|
| 280 |
+
flex: 1;
|
| 281 |
+
min-width: 450px;
|
| 282 |
+
display: flex;
|
| 283 |
+
flex-direction: column;
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
.heatmap {
|
| 287 |
+
display: flex;
|
| 288 |
+
flex-direction: column;
|
| 289 |
+
gap: 0.5rem;
|
| 290 |
+
flex:1;
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
.heatmap-y-axis {
|
| 294 |
+
display: flex;
|
| 295 |
+
align-items: center;
|
| 296 |
+
gap: 0.5rem;
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
.y-axis-label, .x-axis-label {
|
| 300 |
+
font-weight: 600;
|
| 301 |
+
color: #374151;
|
| 302 |
+
font-size: 0.875rem;
|
| 303 |
+
}
|
| 304 |
+
|
| 305 |
+
.heatmap-y-axis .y-axis-label {
|
| 306 |
+
display: flex;
|
| 307 |
+
align-items: center;
|
| 308 |
+
justify-content: center;
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
.heatmap-main {
|
| 312 |
+
flex: 1;
|
| 313 |
+
display: flex;
|
| 314 |
+
flex-direction: row;
|
| 315 |
+
gap: 0.5rem;
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
.heatmap-x-axis {
|
| 319 |
+
display: flex;
|
| 320 |
+
flex-direction: column;
|
| 321 |
+
align-items: center;
|
| 322 |
+
gap: 0.5rem;
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
.x-ticks, .y-ticks {
|
| 326 |
+
display: flex;
|
| 327 |
+
gap: 5px;
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
.x-ticks {
|
| 331 |
+
width: 100%;
|
| 332 |
+
padding-left: 50px;
|
| 333 |
+
justify-content: space-evenly;
|
| 334 |
+
}
|
| 335 |
+
|
| 336 |
+
.y-ticks {
|
| 337 |
+
height: 100%;
|
| 338 |
+
flex-direction: column;
|
| 339 |
+
align-items: flex-start;
|
| 340 |
+
justify-content: space-evenly;
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.y-axis-label, .y-tick {
|
| 344 |
+
writing-mode: vertical-rl;
|
| 345 |
+
text-orientation: mixed;
|
| 346 |
+
}
|
| 347 |
+
|
| 348 |
+
.x-tick, .y-tick {
|
| 349 |
+
font-size: 0.75rem;
|
| 350 |
+
font-weight: 600;
|
| 351 |
+
color: #6b7280;
|
| 352 |
+
text-align: center;
|
| 353 |
+
text-transform: uppercase;
|
| 354 |
+
letter-spacing: 0.025em;
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
.heatmap-grid {
|
| 358 |
+
display: flex;
|
| 359 |
+
flex-direction: column;
|
| 360 |
+
gap: 2px;
|
| 361 |
+
flex: 1;
|
| 362 |
+
}
|
| 363 |
+
|
| 364 |
+
.heatmap-row {
|
| 365 |
+
display: grid;
|
| 366 |
+
grid-template-columns: repeat(auto-fit, minmax(60px, 1fr));
|
| 367 |
+
gap: 2px;
|
| 368 |
+
flex: 1;
|
| 369 |
+
}
|
| 370 |
+
|
| 371 |
+
.heatmap-cell {
|
| 372 |
+
border-radius: 6px;
|
| 373 |
+
cursor: pointer;
|
| 374 |
+
display: flex;
|
| 375 |
+
align-items: center;
|
| 376 |
+
justify-content: center;
|
| 377 |
+
transition: all 0.2s ease;
|
| 378 |
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
| 379 |
+
position: relative;
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
.heatmap-cell:hover {
|
| 383 |
+
transform: scale(1.05);
|
| 384 |
+
z-index: 10;
|
| 385 |
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
.cell-value {
|
| 389 |
+
color: white;
|
| 390 |
+
font-weight: 600;
|
| 391 |
+
font-size: 0.875rem;
|
| 392 |
+
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
/* Assessment Items Styles */
|
| 396 |
+
.assessment-items {
|
| 397 |
+
background: white;
|
| 398 |
+
border-radius: 16px;
|
| 399 |
+
padding: 2rem;
|
| 400 |
+
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
| 401 |
+
border: 1px solid #e2e8f0;
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
+
.assessment-items h3 {
|
| 405 |
+
font-size: 1.5rem;
|
| 406 |
+
font-weight: 600;
|
| 407 |
+
color: #1e293b;
|
| 408 |
+
margin-bottom: 1.5rem;
|
| 409 |
+
padding-bottom: 1rem;
|
| 410 |
+
border-bottom: 2px solid #e5e7eb;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
.no-selection {
|
| 414 |
+
color: #6b7280;
|
| 415 |
+
font-style: italic;
|
| 416 |
+
text-align: center;
|
| 417 |
+
padding: 2rem;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
.items-list {
|
| 421 |
+
display: flex;
|
| 422 |
+
flex-direction: column;
|
| 423 |
+
gap: 2rem;
|
| 424 |
+
}
|
| 425 |
+
|
| 426 |
+
.assessment-item {
|
| 427 |
+
border: 1px solid #e5e7eb;
|
| 428 |
+
border-radius: 12px;
|
| 429 |
+
padding: 2rem;
|
| 430 |
+
background: #f8fafc;
|
| 431 |
+
transition: all 0.2s ease;
|
| 432 |
+
}
|
| 433 |
+
|
| 434 |
+
.assessment-item:hover {
|
| 435 |
+
border-color: #10b981;
|
| 436 |
+
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.1);
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
.item-question, .item-answer {
|
| 440 |
+
margin-bottom: 1.5rem;
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
.item-question h4, .item-answer h4 {
|
| 444 |
+
font-size: 1rem;
|
| 445 |
+
font-weight: 600;
|
| 446 |
+
color: #374151;
|
| 447 |
+
margin-bottom: 0.5rem;
|
| 448 |
+
}
|
| 449 |
+
|
| 450 |
+
.item-question p, .item-answer p {
|
| 451 |
+
color: #6b7280;
|
| 452 |
+
line-height: 1.6;
|
| 453 |
+
}
|
| 454 |
+
|
| 455 |
+
.item-assessments {
|
| 456 |
+
display: grid;
|
| 457 |
+
grid-template-columns: 1fr 1fr;
|
| 458 |
+
gap: 1.5rem;
|
| 459 |
+
}
|
| 460 |
+
|
| 461 |
+
.assessment {
|
| 462 |
+
background: white;
|
| 463 |
+
border-radius: 8px;
|
| 464 |
+
padding: 1.5rem;
|
| 465 |
+
border: 1px solid #e5e7eb;
|
| 466 |
+
}
|
| 467 |
+
|
| 468 |
+
.assessment-header {
|
| 469 |
+
display: flex;
|
| 470 |
+
justify-content: space-between;
|
| 471 |
+
align-items: center;
|
| 472 |
+
margin-bottom: 1rem;
|
| 473 |
+
}
|
| 474 |
+
|
| 475 |
+
.judge-name {
|
| 476 |
+
font-weight: 600;
|
| 477 |
+
color: #374151;
|
| 478 |
+
font-size: 0.875rem;
|
| 479 |
+
}
|
| 480 |
+
|
| 481 |
+
.assessment-label {
|
| 482 |
+
padding: 0.25rem 0.75rem;
|
| 483 |
+
border-radius: 20px;
|
| 484 |
+
font-size: 0.75rem;
|
| 485 |
+
font-weight: 600;
|
| 486 |
+
text-transform: uppercase;
|
| 487 |
+
letter-spacing: 0.05em;
|
| 488 |
+
}
|
| 489 |
+
|
| 490 |
+
.assessment-label.complete {
|
| 491 |
+
background: #dcfce7;
|
| 492 |
+
color: #166534;
|
| 493 |
+
}
|
| 494 |
+
|
| 495 |
+
.assessment-label.evasive {
|
| 496 |
+
background: #fef3c7;
|
| 497 |
+
color: #92400e;
|
| 498 |
+
}
|
| 499 |
+
|
| 500 |
+
.assessment-label.denial {
|
| 501 |
+
background: #fecaca;
|
| 502 |
+
color: #991b1b;
|
| 503 |
+
}
|
| 504 |
+
|
| 505 |
+
.assessment-label.error {
|
| 506 |
+
background: #e9d5ff;
|
| 507 |
+
color: #6b21a8;
|
| 508 |
+
}
|
| 509 |
+
|
| 510 |
+
.assessment-label.unknown {
|
| 511 |
+
background: #f3f4f6;
|
| 512 |
+
color: #4b5563;
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
.assessment-analysis {
|
| 516 |
+
color: #6b7280;
|
| 517 |
+
line-height: 1.6;
|
| 518 |
+
font-size: 0.875rem;
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
/* Responsive Design */
|
| 522 |
+
@media (max-width: 1200px) {
|
| 523 |
+
.charts-container {
|
| 524 |
+
grid-template-columns: 1fr;
|
| 525 |
+
height: auto;
|
| 526 |
+
}
|
| 527 |
+
|
| 528 |
+
.heatmap-container {
|
| 529 |
+
width: 100%;
|
| 530 |
+
max-width: 500px;
|
| 531 |
+
margin: 0 auto;
|
| 532 |
+
}
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
@media (max-width: 768px) {
|
| 536 |
+
.main-content {
|
| 537 |
+
padding: 1rem;
|
| 538 |
+
}
|
| 539 |
+
|
| 540 |
+
.header-content {
|
| 541 |
+
padding: 0 1rem;
|
| 542 |
+
}
|
| 543 |
+
|
| 544 |
+
.filter-bar {
|
| 545 |
+
grid-template-columns: 1fr;
|
| 546 |
+
padding: 1.5rem;
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
.item-assessments {
|
| 550 |
+
grid-template-columns: 1fr;
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
.app-title {
|
| 554 |
+
font-size: 1.5rem;
|
| 555 |
+
}
|
| 556 |
+
|
| 557 |
+
.waterfall-bars {
|
| 558 |
+
min-height: 200px;
|
| 559 |
+
}
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
/* Animation for loading states */
|
| 563 |
+
@keyframes pulse {
|
| 564 |
+
0%, 100% {
|
| 565 |
+
opacity: 1;
|
| 566 |
+
}
|
| 567 |
+
50% {
|
| 568 |
+
opacity: 0.5;
|
| 569 |
+
}
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
.loading {
|
| 573 |
+
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
| 574 |
+
}
|
| 575 |
+
|
| 576 |
+
/* Smooth transitions */
|
| 577 |
+
* {
|
| 578 |
+
transition: border-color 0.2s ease, box-shadow 0.2s ease;
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
/* Focus styles for accessibility */
|
| 582 |
+
button:focus-visible,
|
| 583 |
+
select:focus-visible {
|
| 584 |
+
outline: 2px solid #10b981;
|
| 585 |
+
outline-offset: 2px;
|
| 586 |
+
}
|
| 587 |
+
|
| 588 |
+
@media (prefers-color-scheme: light) {
|
| 589 |
+
:root {
|
| 590 |
+
color: #213547;
|
| 591 |
+
background-color: #ffffff;
|
| 592 |
+
}
|
| 593 |
+
a:hover {
|
| 594 |
+
color: #747bff;
|
| 595 |
+
}
|
| 596 |
+
button {
|
| 597 |
+
background-color: #f9f9f9;
|
| 598 |
+
}
|
| 599 |
+
}
|
src/lib/db.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import duckdb from 'duckdb';
|
| 2 |
+
import path from 'path';
|
| 3 |
+
|
| 4 |
+
// DB is one level up
|
| 5 |
+
const ROOT_DIR = process.cwd(); // cwd() = Current Working Directory
|
| 6 |
+
const dbPath = path.join(ROOT_DIR, 'database.duckdb');
|
| 7 |
+
|
| 8 |
+
const db = new duckdb.Database(dbPath, { "access_mode": "READ_ONLY" });
|
| 9 |
+
console.log(`DuckDB connected in READ_ONLY mode at ${dbPath}`);
|
| 10 |
+
|
| 11 |
+
function query<T>(sql: string, ...params: any[]): Promise<T[]> {
|
| 12 |
+
return new Promise((resolve, reject) => {
|
| 13 |
+
db.all(sql, ...params, (err, res) => {
|
| 14 |
+
if (err) return reject(err);
|
| 15 |
+
resolve(res as T[]);
|
| 16 |
+
});
|
| 17 |
+
});
|
| 18 |
+
}
|
| 19 |
+
export default { query };
|
src/lib/ingest.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import fs from 'fs';
|
| 2 |
+
import path from 'path';
|
| 3 |
+
import { fileURLToPath } from 'url'; // <-- IMPORT THIS
|
| 4 |
+
import duckdb from 'duckdb';
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
const ROOT_DIR = process.cwd(); // cwd() = Current Working Directory
|
| 8 |
+
const DB_PATH = path.join(ROOT_DIR, 'database.duckdb');
|
| 9 |
+
|
| 10 |
+
export const DATA_SOURCES = {
|
| 11 |
+
questions: 'https://huggingface.co/datasets/PITTI/speechmap-questions/resolve/main/consolidated_questions.parquet',
|
| 12 |
+
responses: 'https://huggingface.co/datasets/PITTI/speechmap-responses/resolve/main/consolidated_responses.parquet',
|
| 13 |
+
assessments: 'https://huggingface.co/datasets/PITTI/speechmap-assessments/resolve/main/consolidated_assessments.parquet',
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
// --- DATABASE HELPER ---
|
| 17 |
+
// (This function is already perfect, no changes needed)
|
| 18 |
+
function query(db: duckdb.Database, sql: string): Promise<any[]> {
|
| 19 |
+
return new Promise((resolve, reject) => {
|
| 20 |
+
db.all(sql, (err, res) => {
|
| 21 |
+
if (err) return reject(err);
|
| 22 |
+
resolve(res);
|
| 23 |
+
});
|
| 24 |
+
});
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// --- STANDALONE SCRIPT LOGIC ---
|
| 28 |
+
async function rebuildDatabase() {
|
| 29 |
+
console.log('--- Starting full database rebuild with DuckDB ---');
|
| 30 |
+
|
| 31 |
+
if (fs.existsSync(DB_PATH)) {
|
| 32 |
+
fs.unlinkSync(DB_PATH);
|
| 33 |
+
console.log('Deleted old database file.');
|
| 34 |
+
}
|
| 35 |
+
|
| 36 |
+
const db = new duckdb.Database(DB_PATH);
|
| 37 |
+
console.log('DuckDB database created at:', DB_PATH);
|
| 38 |
+
|
| 39 |
+
try {
|
| 40 |
+
console.log('Installing and loading DuckDB extensions (httpfs, json)...');
|
| 41 |
+
await query(db, 'INSTALL httpfs; LOAD httpfs;');
|
| 42 |
+
await query(db, 'INSTALL json; LOAD json;');
|
| 43 |
+
|
| 44 |
+
console.log('Creating database schema...');
|
| 45 |
+
// (Your schema and ingestion logic is fine, no changes needed)
|
| 46 |
+
await query(db, `
|
| 47 |
+
CREATE TABLE themes (slug VARCHAR PRIMARY KEY, name VARCHAR);
|
| 48 |
+
CREATE TABLE questions (uuid VARCHAR PRIMARY KEY, id VARCHAR, category VARCHAR, domain VARCHAR, question VARCHAR, theme VARCHAR);
|
| 49 |
+
CREATE TABLE responses (uuid VARCHAR PRIMARY KEY, q_uuid VARCHAR, model VARCHAR, timestamp VARCHAR, api_provider VARCHAR, provider VARCHAR, content VARCHAR, matched BOOLEAN, origin VARCHAR);
|
| 50 |
+
CREATE TABLE assessments (uuid VARCHAR PRIMARY KEY, q_uuid VARCHAR, r_uuid VARCHAR, judge_model VARCHAR, judge_analysis VARCHAR, compliance VARCHAR, raw_judge_analysis VARCHAR, matched BOOLEAN, origin VARCHAR);
|
| 51 |
+
`);
|
| 52 |
+
console.log('Schema created.');
|
| 53 |
+
|
| 54 |
+
console.log('Ingesting themes and questions...');
|
| 55 |
+
await query(db, `
|
| 56 |
+
INSERT INTO themes (slug, name)
|
| 57 |
+
SELECT DISTINCT theme AS slug, domain AS name FROM read_parquet('${DATA_SOURCES.questions}') WHERE theme IS NOT NULL AND domain IS NOT NULL;
|
| 58 |
+
|
| 59 |
+
INSERT INTO questions (uuid, id, category, domain, question, theme)
|
| 60 |
+
SELECT uuid, id, category, domain, question, theme FROM read_parquet('${DATA_SOURCES.questions}');
|
| 61 |
+
`);
|
| 62 |
+
|
| 63 |
+
console.log('Ingesting responses from Parquet...');
|
| 64 |
+
await query(db, `
|
| 65 |
+
INSERT INTO responses (uuid, q_uuid, model, timestamp, api_provider, provider, content, matched, origin)
|
| 66 |
+
SELECT uuid, q_uuid, model, timestamp, api_provider, provider, content, matched, origin FROM read_parquet('${DATA_SOURCES.responses}');
|
| 67 |
+
`);
|
| 68 |
+
|
| 69 |
+
console.log('Ingesting assessments...');
|
| 70 |
+
await query(db, `
|
| 71 |
+
INSERT INTO assessments (uuid, q_uuid, r_uuid, judge_model, judge_analysis, compliance, raw_judge_analysis, matched, origin)
|
| 72 |
+
SELECT uuid, q_uuid, r_uuid, judge_model, judge_analysis, compliance, raw_judge_analysis, matched, origin FROM read_parquet('${DATA_SOURCES.assessments}');
|
| 73 |
+
`);
|
| 74 |
+
|
| 75 |
+
console.log('✅ Data ingestion complete!');
|
| 76 |
+
} catch (error) {
|
| 77 |
+
console.error('An error occurred during the rebuild:', error);
|
| 78 |
+
db.close();
|
| 79 |
+
throw error;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
db.close();
|
| 83 |
+
console.log('--- Database rebuild finished successfully ---');
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
// --- ESM-compatible way to check if the script is run directly ---
|
| 88 |
+
const entryPoint = process.argv[1];
|
| 89 |
+
const currentFile = fileURLToPath(import.meta.url);
|
| 90 |
+
|
| 91 |
+
if (entryPoint === currentFile) {
|
| 92 |
+
rebuildDatabase().catch(err => {
|
| 93 |
+
console.error('Database rebuild failed:', err);
|
| 94 |
+
process.exit(1);
|
| 95 |
+
});
|
| 96 |
+
}
|
src/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { StrictMode } from 'react'
|
| 2 |
+
import { createRoot } from 'react-dom/client'
|
| 3 |
+
import './index.css'
|
| 4 |
+
import App from './App.jsx'
|
| 5 |
+
|
| 6 |
+
createRoot(document.getElementById('root')!).render(
|
| 7 |
+
<StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</StrictMode>,
|
| 10 |
+
)
|
src/types.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface Question {
|
| 2 |
+
uuid: string;
|
| 3 |
+
id: string;
|
| 4 |
+
category: string;
|
| 5 |
+
domain?: string;
|
| 6 |
+
question: string;
|
| 7 |
+
theme?: string; // Optional, can be null
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
export interface Response {
|
| 11 |
+
uuid: string;
|
| 12 |
+
q_uuid: string; // Foreign key to Question
|
| 13 |
+
model?: string;
|
| 14 |
+
timestamp?: string; // ISO date string
|
| 15 |
+
api_provider?: string;
|
| 16 |
+
provider?: string;
|
| 17 |
+
content: string;
|
| 18 |
+
matched: boolean; // Boolean, but stored as integer in SQLite
|
| 19 |
+
origin?: string; // Optional, can be null
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
export interface Assessment {
|
| 23 |
+
uuid: string;
|
| 24 |
+
q_uuid: string; // Foreign key to Question
|
| 25 |
+
r_uuid: string; // Foreign key to Response
|
| 26 |
+
judge_model: string; // Model used for assessment
|
| 27 |
+
judge_analysis?: string; // Optional, can be null
|
| 28 |
+
compliance: string; // Compliance status
|
| 29 |
+
raw_judge_analysis?: string; // Optional, can be null
|
| 30 |
+
matched: boolean; // Boolean, but stored as integer in SQLite
|
| 31 |
+
origin?: string; // Optional, can be null
|
| 32 |
+
}
|
| 33 |
+
|
| 34 |
+
export interface Theme {
|
| 35 |
+
slug: string; // Unique identifier for the theme
|
| 36 |
+
name: string; // Human-readable name for the theme
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
export type TransitionMatrix = Record<string, Record<string, number>>;
|
| 40 |
+
|
| 41 |
+
interface JudgeAssessment{
|
| 42 |
+
judge_model: string;
|
| 43 |
+
judge_analysis: string;
|
| 44 |
+
compliance: string;
|
| 45 |
+
}
|
| 46 |
+
|
| 47 |
+
export interface AssessmentItem {
|
| 48 |
+
question: string;
|
| 49 |
+
theme: string;
|
| 50 |
+
domain: string;
|
| 51 |
+
response: string;
|
| 52 |
+
model: string;
|
| 53 |
+
assessments: JudgeAssessment[];
|
| 54 |
+
}
|
src/utils/apiUtils.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import type { Theme, TransitionMatrix, AssessmentItem } from '../types.js';
|
| 2 |
+
|
| 3 |
+
interface ApiError {
|
| 4 |
+
error: string;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
// --- Reusable Fetch Helper ---
|
| 8 |
+
// This helper centralizes our fetch logic and error handling.
|
| 9 |
+
async function fetchAPI<T>(url: string, options?: RequestInit): Promise<T> {
|
| 10 |
+
const response = await fetch(url, options);
|
| 11 |
+
|
| 12 |
+
// Manually check for HTTP errors, as fetch doesn't reject on them
|
| 13 |
+
if (!response.ok) {
|
| 14 |
+
// Try to get a more specific error message from the response body
|
| 15 |
+
const errorBody = (await response.json().catch(() => ({ error: 'An unknown error occurred' }))) as ApiError;
|
| 16 |
+
const errorMessage = errorBody.error || `HTTP error! Status: ${response.status}`;
|
| 17 |
+
throw new Error(errorMessage);
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
+
// If the request was successful, parse and return the JSON body
|
| 21 |
+
return response.json() as Promise<T>;
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
// --- API Functions ---
|
| 26 |
+
|
| 27 |
+
export const getThemes = (): Promise<Theme[]> => {
|
| 28 |
+
return fetchAPI<Theme[]>('/api/themes');
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
export const getJudges = (): Promise<string[]> => {
|
| 32 |
+
return fetchAPI<string[]>('/api/judges');
|
| 33 |
+
};
|
| 34 |
+
|
| 35 |
+
// New function to get the reclassification data
|
| 36 |
+
export const getReclassificationData = (
|
| 37 |
+
judge1: string,
|
| 38 |
+
judge2: string,
|
| 39 |
+
theme?: string
|
| 40 |
+
): Promise<TransitionMatrix> => {
|
| 41 |
+
// Build the query string from the parameters
|
| 42 |
+
const params = new URLSearchParams({
|
| 43 |
+
judge1,
|
| 44 |
+
judge2,
|
| 45 |
+
});
|
| 46 |
+
|
| 47 |
+
// Only add the theme parameter if it's provided
|
| 48 |
+
if (theme) {
|
| 49 |
+
params.append('theme', theme);
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
return fetchAPI<TransitionMatrix>(`/api/reclassification?${params.toString()}`);
|
| 53 |
+
};
|
| 54 |
+
|
| 55 |
+
export const getAssessmentItems = (
|
| 56 |
+
judge1: string,
|
| 57 |
+
judge2: string,
|
| 58 |
+
fromCategory: string,
|
| 59 |
+
toCategory: string,
|
| 60 |
+
theme?: string
|
| 61 |
+
): Promise<any[]> => {
|
| 62 |
+
// Build the query string from the parameters
|
| 63 |
+
const params = new URLSearchParams({
|
| 64 |
+
judge1,
|
| 65 |
+
judge2,
|
| 66 |
+
fromCategory,
|
| 67 |
+
toCategory,
|
| 68 |
+
});
|
| 69 |
+
|
| 70 |
+
// Only add the theme parameter if it's provided
|
| 71 |
+
if (theme) {
|
| 72 |
+
params.append('theme', theme);
|
| 73 |
+
}
|
| 74 |
+
|
| 75 |
+
return fetchAPI<AssessmentItem[]>(`/api/mismatches?${params.toString()}`);
|
| 76 |
+
}
|
src/utils/chartUtils.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// packages/frontend/src/lib/chartUtils.ts
|
| 2 |
+
import type { TransitionMatrix } from '../types.js';
|
| 3 |
+
import type { Data } from 'plotly.js';
|
| 4 |
+
|
| 5 |
+
// --- Constants (mirrored from your Python script) ---
|
| 6 |
+
export const CATEGORIES = ["COMPLETE", "EVASIVE", "DENIAL", "ERROR" /*, "UNKNOWN"*/];
|
| 7 |
+
export const COLOR_MAP: Record<string, string> = {
|
| 8 |
+
"BASE": 'rgba(0,0,0,0)', // Transparent for the base of flow bars
|
| 9 |
+
"COMPLETE": "#10b981",
|
| 10 |
+
"EVASIVE": "#f59e0b",
|
| 11 |
+
"DENIAL": "#ef4444",
|
| 12 |
+
"ERROR": "#8b5cf6",
|
| 13 |
+
/*"UNKNOWN": "#6b7280"*/
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
// --- Type Definitions for our Chart Data Structures ---
|
| 17 |
+
interface Segment {
|
| 18 |
+
category_label: string;
|
| 19 |
+
value: number;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
interface PlotStage {
|
| 23 |
+
stage_name: string;
|
| 24 |
+
segments: Segment[];
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
// --- Main Logic: Port of your Python `create_waterfall_stages` function ---
|
| 28 |
+
|
| 29 |
+
export function generateWaterfallData(
|
| 30 |
+
matrix: TransitionMatrix,
|
| 31 |
+
judge1Name: string,
|
| 32 |
+
judge2Name: string
|
| 33 |
+
): PlotStage[] {
|
| 34 |
+
const plotStages: PlotStage[] = [];
|
| 35 |
+
|
| 36 |
+
// 1. Calculate initial counts for Judge 1
|
| 37 |
+
const initialJ1Counts = CATEGORIES.reduce((acc, cat) => {
|
| 38 |
+
const fromCat = matrix[cat] || {};
|
| 39 |
+
acc[cat] = Object.values(fromCat).reduce((sum, count) => sum + count, 0);
|
| 40 |
+
return acc;
|
| 41 |
+
}, {} as Record<string, number>);
|
| 42 |
+
|
| 43 |
+
const numItems = Object.values(initialJ1Counts).reduce((sum, count) => sum + count, 0);
|
| 44 |
+
if (numItems === 0) return [];
|
| 45 |
+
|
| 46 |
+
// 2. Create the first "Initial" bar
|
| 47 |
+
const initialStage: PlotStage = {
|
| 48 |
+
stage_name: `${judge1Name} Initial`,
|
| 49 |
+
segments: [{ category_label: 'BASE', value: 0 }],
|
| 50 |
+
};
|
| 51 |
+
for (const cat of CATEGORIES) {
|
| 52 |
+
if (initialJ1Counts[cat] > 0) {
|
| 53 |
+
initialStage.segments.push({ category_label: cat, value: initialJ1Counts[cat] });
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
plotStages.push(initialStage);
|
| 57 |
+
|
| 58 |
+
// 3. Loop through categories to create flow and state bars
|
| 59 |
+
const intermediateState = { ...initialJ1Counts };
|
| 60 |
+
const j1ProcessingOrder = CATEGORIES.filter(cat => initialJ1Counts[cat] > 0);
|
| 61 |
+
|
| 62 |
+
j1ProcessingOrder.forEach((j1Cat, i) => {
|
| 63 |
+
let baseHeight = Object.values(intermediateState).reduce((sum, val, j) => {
|
| 64 |
+
if (j <= i) return sum + val;
|
| 65 |
+
return sum;
|
| 66 |
+
}, 0);
|
| 67 |
+
|
| 68 |
+
// Create "flow" bars showing items leaving this category
|
| 69 |
+
for (const j2Cat of CATEGORIES) {
|
| 70 |
+
const flowCount = matrix[j1Cat]?.[j2Cat] || 0;
|
| 71 |
+
if (flowCount > 0 && j2Cat !== j1Cat) {
|
| 72 |
+
baseHeight -= flowCount;
|
| 73 |
+
const baseValue = baseHeight
|
| 74 |
+
intermediateState[j1Cat] -= flowCount;
|
| 75 |
+
intermediateState[j2Cat] = (intermediateState[j2Cat] || 0) + flowCount;
|
| 76 |
+
|
| 77 |
+
plotStages.push({
|
| 78 |
+
stage_name: `${j1Cat} → ${j2Cat}`,
|
| 79 |
+
segments: [
|
| 80 |
+
{ category_label: 'BASE', value: baseValue },
|
| 81 |
+
{ category_label: j2Cat, value: flowCount },
|
| 82 |
+
],
|
| 83 |
+
});
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
// Create the "intermediate state" or "final" bar
|
| 88 |
+
const isFinalBar = i === j1ProcessingOrder.length - 1;
|
| 89 |
+
const stageName = isFinalBar
|
| 90 |
+
? `${judge2Name} Final`
|
| 91 |
+
: `State after ${j1Cat}`;
|
| 92 |
+
|
| 93 |
+
const stateSegments: Segment[] = [{ category_label: 'BASE', value: 0 }];
|
| 94 |
+
for (const cat of CATEGORIES) {
|
| 95 |
+
if (intermediateState[cat] > 0) {
|
| 96 |
+
stateSegments.push({ category_label: cat, value: intermediateState[cat] });
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
plotStages.push({ stage_name: stageName, segments: stateSegments });
|
| 100 |
+
});
|
| 101 |
+
|
| 102 |
+
return plotStages;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
export function generateHeatmapData(matrix: TransitionMatrix) : Data {
|
| 107 |
+
// Create a 2D array (z-axis) for the heatmap values
|
| 108 |
+
const z = CATEGORIES.map(j1Cat =>
|
| 109 |
+
CATEGORIES.map(j2Cat => matrix[j1Cat]?.[j2Cat] || 0)
|
| 110 |
+
);
|
| 111 |
+
return {
|
| 112 |
+
type: 'heatmap' as const, // <--- ADD THIS
|
| 113 |
+
z,
|
| 114 |
+
x: CATEGORIES,
|
| 115 |
+
y: CATEGORIES,
|
| 116 |
+
colorscale: 'Viridis' as const, // Optional: add a colorscale for better visuals
|
| 117 |
+
hoverongaps: false,
|
| 118 |
+
};
|
| 119 |
+
}
|
src/vite-env.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
/// <reference types="vite/client" />
|
tsconfig.app.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
| 4 |
+
"composite": true,
|
| 5 |
+
"target": "ES2022",
|
| 6 |
+
"useDefineForClassFields": true,
|
| 7 |
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
| 8 |
+
"module": "ESNext",
|
| 9 |
+
"skipLibCheck": true,
|
| 10 |
+
|
| 11 |
+
/* Bundler mode */
|
| 12 |
+
"moduleResolution": "bundler",
|
| 13 |
+
"verbatimModuleSyntax": true,
|
| 14 |
+
"moduleDetection": "force",
|
| 15 |
+
"jsx": "react-jsx",
|
| 16 |
+
|
| 17 |
+
/* Linting */
|
| 18 |
+
"strict": true,
|
| 19 |
+
"noUnusedLocals": true,
|
| 20 |
+
"noUnusedParameters": true,
|
| 21 |
+
"erasableSyntaxOnly": true,
|
| 22 |
+
"noFallthroughCasesInSwitch": true,
|
| 23 |
+
"noUncheckedSideEffectImports": true
|
| 24 |
+
},
|
| 25 |
+
"include": ["src"],
|
| 26 |
+
"exclude": ["src/lib"]
|
| 27 |
+
}
|
tsconfig.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"files": [],
|
| 3 |
+
"references": [
|
| 4 |
+
{ "path": "./tsconfig.app.json" },
|
| 5 |
+
{ "path": "./tsconfig.node.json" }
|
| 6 |
+
]
|
| 7 |
+
}
|
tsconfig.node.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"outDir": "dist",
|
| 4 |
+
"rootDir": ".",
|
| 5 |
+
"composite": true,
|
| 6 |
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
| 7 |
+
"target": "ES2023",
|
| 8 |
+
"lib": ["ES2023"],
|
| 9 |
+
"module": "NodeNext",
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
/* Bundler mode */
|
| 13 |
+
"moduleResolution": "NodeNext",
|
| 14 |
+
"skipLibCheck": true,
|
| 15 |
+
"verbatimModuleSyntax": true,
|
| 16 |
+
"moduleDetection": "force",
|
| 17 |
+
"allowSyntheticDefaultImports": true,
|
| 18 |
+
|
| 19 |
+
/* Linting */
|
| 20 |
+
"strict": true,
|
| 21 |
+
"noUnusedLocals": true,
|
| 22 |
+
"noUnusedParameters": true,
|
| 23 |
+
"erasableSyntaxOnly": true,
|
| 24 |
+
"noFallthroughCasesInSwitch": true,
|
| 25 |
+
"noUncheckedSideEffectImports": true
|
| 26 |
+
},
|
| 27 |
+
"include": [
|
| 28 |
+
"vite.config.ts",
|
| 29 |
+
"src/types.ts",
|
| 30 |
+
"api/**/*.ts",
|
| 31 |
+
"src/lib/**/*.ts"
|
| 32 |
+
]
|
| 33 |
+
}
|
vite.config.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
import path from 'path';
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
// https://vite.dev/config/
|
| 7 |
+
export default defineConfig({
|
| 8 |
+
plugins: [
|
| 9 |
+
react(),
|
| 10 |
+
{
|
| 11 |
+
name: 'custom-api-server',
|
| 12 |
+
configureServer(server) {
|
| 13 |
+
server.middlewares.use(async (req, res, next) => {
|
| 14 |
+
// We only care about requests to /api/*
|
| 15 |
+
if (req.url && req.url.startsWith('/api/')) {
|
| 16 |
+
const urlWithoutQuery = req.url.split('?')[0];
|
| 17 |
+
const modulePath = path.resolve(__dirname, `.${urlWithoutQuery}.ts`);
|
| 18 |
+
|
| 19 |
+
try {
|
| 20 |
+
// ssrLoadModule is the right way to load a TS module in Vite's dev server
|
| 21 |
+
const module = await server.ssrLoadModule(modulePath);
|
| 22 |
+
|
| 23 |
+
if (module.default && typeof module.default === 'function') {
|
| 24 |
+
await module.default(req, res); // Assumes a default export handler
|
| 25 |
+
} else {
|
| 26 |
+
console.error(`API module ${modulePath} does not have a default export.`);
|
| 27 |
+
res.statusCode = 404;
|
| 28 |
+
res.end('Not Found');
|
| 29 |
+
}
|
| 30 |
+
} catch (error) {
|
| 31 |
+
// Vite's ssrLoadModule will print a nicely formatted error to the console
|
| 32 |
+
// We just need to handle the response.
|
| 33 |
+
console.error(`Error processing API request for ${req.url}:`, error);
|
| 34 |
+
res.statusCode = 500;
|
| 35 |
+
res.end('Internal Server Error');
|
| 36 |
+
}
|
| 37 |
+
} else {
|
| 38 |
+
// Not an API call, pass it to the next middleware (usually Vite's own)
|
| 39 |
+
next();
|
| 40 |
+
}
|
| 41 |
+
});
|
| 42 |
+
},
|
| 43 |
+
},
|
| 44 |
+
],
|
| 45 |
+
|
| 46 |
+
})
|