pappitti commited on
Commit
d26f541
·
0 Parent(s):

first commit

Browse files
.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
+ })