The file /home/ubuntu/training/asr-benchmark-dashboard/src/app/globals.css has been updated successfully.

@import "tailwindcss";

@theme inline {
  --color-background: #09090b;
  --color-foreground: #fafafa;
  --color-card: #18181b;
  --color-card-hover: #27272a;
  --color-border: #27272a;
  --color-border-light: #3f3f46;
  --color-muted: #71717a;
  --color-accent: #a1a1aa;
  --color-success: #22c55e;
  --color-danger: #ef4444;
  --color-warning: #eab308;
  --font-sans: var(--font-geist-sans);
  --font-mono: var(--font-geist-mono);
}

body {
  background: var(--color-background);
  color: var(--color-foreground);
  font-family: var(--font-mono), monospace;
}

/* Custom scrollbar */
::-webkit-scrollbar {
  width: 6px;
  height: 6px;
}
::-webkit-scrollbar-track {
  background: var(--color-background);
}
::-webkit-scrollbar-thumb {
  background: var(--color-border-light);
  border-radius: 3px;
}

/* Recharts overrides */
.recharts-cartesian-grid line {
  stroke: #27272a !important;
}
.recharts-text {
  fill: #a1a1aa !important;
  font-family: var(--font-mono), monospace !important;
  font-size: 11px !important;
}
.recharts-tooltip-wrapper .recharts-default-tooltip {
  background: #18181b !important;
  border: 1px solid #3f3f46 !important;
  border-radius: 8px !important;
}
.recharts-tooltip-item {
  color: #fafafa !important;
  font-family: var(--font-mono), monospace !important;
  font-size: 12px !important;
}
.recharts-tooltip-label {
  color: #a1a1aa !important;
  font-family: var(--font-mono), monospace !important;
  font-size: 12px !important;
  margin-bottom: 4px !important;
}

-  --color-background: var(--background);

-  --color-foreground: var(--foreground);

+  font-family: var(--font-mono), monospace;

-  font-family: Arial, Helvetica, sans-serif;

+  background: var(--color-border-light);

+  font-family: var(--font-mono), monospace !important;

+  border: 1px solid #3f3f46 !important;

@import "tailwindcss";

:root {
  --background: #ffffff;
  --foreground: #171717;
}

@theme inline {
  --color-background: var(--background);
  --color-foreground: var(--foreground);
  --font-sans: var(--font-geist-sans);
  --font-mono: var(--font-geist-mono);
}

@media (prefers-color-scheme: dark) {
  :root {
    --background: #0a0a0a;
    --foreground: #ededed;
  }
}

body {
  background: var(--background);
  color: var(--foreground);
  font-family: Arial, Helvetica, sans-serif;
}


Write

import type { Metadata } from "next";
import { Geist_Mono } from "next/font/google";
import "./globals.css";

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "ASR Benchmark Dashboard",
  description: "Indic ASR model benchmarking & WER analysis",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" className={`${geistMono.variable} h-full antialiased dark`}>
      <body className="min-h-full flex flex-col font-mono">{children}</body>
    </html>
  );
}


The file /home/ubuntu/training/asr-benchmark-dashboard/src/app/layout.tsx has been updated successfully.

import type { Metadata } from "next";
import { Geist_Mono } from "next/font/google";
import "./globals.css";

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "ASR Benchmark Dashboard",
  description: "Indic ASR model benchmarking & WER analysis",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" className={`${geistMono.variable} h-full antialiased dark`}>
      <body className="min-h-full flex flex-col font-mono">{children}</body>
    </html>
  );
}

-import { Geist, Geist_Mono } from "next/font/google";

+import { Geist_Mono } from "next/font/google";

-  description: "Generated by create next app",

+  description: "Indic ASR model benchmarking & WER analysis",

-      className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}

-      <body className="min-h-full flex flex-col">{children}</body>

+    <html lang="en" className={`${geistMono.variable} h-full antialiased dark`}>

+      <body className="min-h-full flex flex-col font-mono">{children}</body>

import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";

const geistSans = Geist({
  variable: "--font-geist-sans",
  subsets: ["latin"],
});

const geistMono = Geist_Mono({
  variable: "--font-geist-mono",
  subsets: ["latin"],
});

export const metadata: Metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html
      lang="en"
      className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
    >
      <body className="min-h-full flex flex-col">{children}</body>
    </html>
  );
}


Now let me create the types, API routes, and components. I'll write them all in parallel.


export interface LanguageMetrics {
  n_samples: number;
  wer: number;
  cer: number;
  wer_normalized: number;
  cer_normalized: number;
  empty_hypotheses: number;
}

export interface OverallMetrics {
  n_samples: number;
  wer: number;
  cer: number;
  wer_normalized: number;
  cer_normalized: number;
}

export interface MacroAvgMetrics {
  n_languages: number;
  wer: number;
  cer: number;
  wer_normalized: number;
  cer_normalized: number;
}

export interface MetaMeta {
  checkpoint: string;
  checkpoint_name: string;
  model_id: string;
  model_type?: string;
  dataset: string;
  batch_size?: number;
  force_language?: boolean;
  inference_time_sec: number;
  total_audio_sec: number;
  rtf: number;
  timestamp?: string;
  gpu?: string;
  framework?: string;
}

export interface MetricsFile {
  [language: string]: LanguageMetrics;
  __overall__: OverallMetrics;
  __macro_avg__: MacroAvgMetrics;
  __meta__: MetaMeta;
}

export interface SampleAnalysis {
  id: string;
  language: string;
  reference: string;
  hypothesis: string;
  detected_language?: string;
  ref_normalized?: string;
  hyp_normalized?: string;
}

export interface ModelInfo {
  id: string;
  displayName: string;
  checkpoints: string[];
}

export interface CheckpointComparison {
  language: string;
  [key: string]: number | string; // checkpoint_name: wer value
}

export const MODEL_REGISTRY: Record<string, string> = {
  "qwen3-asr": "Qwen3-ASR-1.7B",
  "gemma3n-e2b": "Gemma-3n-E2B-ASR",
  "parakeet-1.1b": "Parakeet-1.1B-Language-Guided",
};

export const LANGUAGES = [
  "assamese", "bengali", "english", "gujarati", "hindi", "kannada",
  "malayalam", "marathi", "odia", "punjabi", "tamil", "telugu",
] as const;

export const LANGUAGE_CODES: Record<string, string> = {
  assamese: "as", bengali: "bn", english: "en", gujarati: "gu",
  hindi: "hi", kannada: "kn", malayalam: "ml", marathi: "mr",
  odia: "or", punjabi: "pa", tamil: "ta", telugu: "te",
};

export const CHART_COLORS = [
  "#6366f1", "#8b5cf6", "#a78bfa", "#c084fc",
  "#e879f9", "#f472b6", "#fb7185", "#f87171",
  "#fb923c", "#fbbf24", "#a3e635", "#34d399",
];

export const MODEL_COLORS: Record<string, string> = {
  "qwen3-asr": "#6366f1",
  "gemma3n-e2b": "#8b5cf6",
  "parakeet-1.1b": "#34d399",
};


File created successfully at: /home/ubuntu/training/asr-benchmark-dashboard/src/lib/types.ts

export interface LanguageMetrics {
  n_samples: number;
  wer: number;
  cer: number;
  wer_normalized: number;
  cer_normalized: number;
  empty_hypotheses: number;
}

export interface OverallMetrics {
  n_samples: number;
  wer: number;
  cer: number;
  wer_normalized: number;
  cer_normalized: number;
}

export interface MacroAvgMetrics {
  n_languages: number;
  wer: number;
  cer: number;
  wer_normalized: number;
  cer_normalized: number;
}

export interface MetaMeta {
  checkpoint: string;
  checkpoint_name: string;
  model_id: string;
  model_type?: string;
  dataset: string;
  batch_size?: number;
  force_language?: boolean;
  inference_time_sec: number;
  total_audio_sec: number;
  rtf: number;
  timestamp?: string;
  gpu?: string;
  framework?: string;
}

export interface MetricsFile {
  [language: string]: LanguageMetrics;
  __overall__: OverallMetrics;
  __macro_avg__: MacroAvgMetrics;
  __meta__: MetaMeta;
}

export interface SampleAnalysis {
  id: string;
  language: string;
  reference: string;
  hypothesis: string;
  detected_language?: string;
  ref_normalized?: string;
  hyp_normalized?: string;
}

export interface ModelInfo {
  id: string;
  displayName: string;
  checkpoints: string[];
}

export interface CheckpointComparison {
  language: string;
  [key: string]: number | string; // checkpoint_name: wer value
}

export const MODEL_REGISTRY: Record<string, string> = {
  "qwen3-asr": "Qwen3-ASR-1.7B",
  "gemma3n-e2b": "Gemma-3n-E2B-ASR",
  "parakeet-1.1b": "Parakeet-1.1B-Language-Guided",
};

export const LANGUAGES = [
  "assamese", "bengali", "english", "gujarati", "hindi", "kannada",
  "malayalam", "marathi", "odia", "punjabi", "tamil", "telugu",
] as const;

export const LANGUAGE_CODES: Record<string, string> = {
  assamese: "as", bengali: "bn", english: "en", gujarati: "gu",
  hindi: "hi", kannada: "kn", malayalam: "ml", marathi: "mr",
  odia: "or", punjabi: "pa", tamil: "ta", telugu: "te",
};

export const CHART_COLORS = [
  "#6366f1", "#8b5cf6", "#a78bfa", "#c084fc",
  "#e879f9", "#f472b6", "#fb7185", "#f87171",
  "#fb923c", "#fbbf24", "#a3e635", "#34d399",
];

export const MODEL_COLORS: Record<string, string> = {
  "qwen3-asr": "#6366f1",
  "gemma3n-e2b": "#8b5cf6",
  "parakeet-1.1b": "#34d399",
};


import fs from "fs";
import path from "path";
import { MetricsFile, SampleAnalysis, ModelInfo, MODEL_REGISTRY } from "./types";

const BENCHMARK_DIR = path.join(process.cwd(), "..", "benchmark_outputs");

export function getModels(): ModelInfo[] {
  const models: ModelInfo[] = [];
  for (const [modelId, displayName] of Object.entries(MODEL_REGISTRY)) {
    const modelDir = path.join(BENCHMARK_DIR, modelId);
    if (!fs.existsSync(modelDir)) {
      models.push({ id: modelId, displayName, checkpoints: [] });
      continue;
    }
    const checkpoints = fs.readdirSync(modelDir)
      .filter((d) => {
        const fullPath = path.join(modelDir, d);
        return fs.statSync(fullPath).isDirectory() &&
          fs.existsSync(path.join(fullPath, "metrics.json"));
      })
      .sort((a, b) => {
        const numA = parseInt(a.replace(/\D/g, "")) || 0;
        const numB = parseInt(b.replace(/\D/g, "")) || 0;
        return numA - numB;
      });
    models.push({ id: modelId, displayName, checkpoints });
  }
  return models;
}

export function getMetrics(modelId: string, checkpoint: string): MetricsFile | null {
  const filePath = path.join(BENCHMARK_DIR, modelId, checkpoint, "metrics.json");
  if (!fs.existsSync(filePath)) return null;
  const raw = fs.readFileSync(filePath, "utf-8");
  return JSON.parse(raw) as MetricsFile;
}

export function getSamples(modelId: string, checkpoint: string): SampleAnalysis[] | null {
  const filePath = path.join(BENCHMARK_DIR, modelId, checkpoint, "sample_analysis.json");
  if (!fs.existsSync(filePath)) return null;
  const raw = fs.readFileSync(filePath, "utf-8");
  return JSON.parse(raw) as SampleAnalysis[];
}

export function getAllMetricsForModel(modelId: string): Record<string, MetricsFile> {
  const result: Record<string, MetricsFile> = {};
  const model = getModels().find((m) => m.id === modelId);
  if (!model) return result;
  for (const ckpt of model.checkpoints) {
    const metrics = getMetrics(modelId, ckpt);
    if (metrics) result[ckpt] = metrics;
  }
  return result;
}


File created successfully at: /home/ubuntu/training/asr-benchmark-dashboard/src/lib/data.ts

import fs from "fs";
import path from "path";
import { MetricsFile, SampleAnalysis, ModelInfo, MODEL_REGISTRY } from "./types";

const BENCHMARK_DIR = path.join(process.cwd(), "..", "benchmark_outputs");

export function getModels(): ModelInfo[] {
  const models: ModelInfo[] = [];
  for (const [modelId, displayName] of Object.entries(MODEL_REGISTRY)) {
    const modelDir = path.join(BENCHMARK_DIR, modelId);
    if (!fs.existsSync(modelDir)) {
      models.push({ id: modelId, displayName, checkpoints: [] });
      continue;
    }
    const checkpoints = fs.readdirSync(modelDir)
      .filter((d) => {
        const fullPath = path.join(modelDir, d);
        return fs.statSync(fullPath).isDirectory() &&
          fs.existsSync(path.join(fullPath, "metrics.json"));
      })
      .sort((a, b) => {
        const numA = parseInt(a.replace(/\D/g, "")) || 0;
        const numB = parseInt(b.replace(/\D/g, "")) || 0;
        return numA - numB;
      });
    models.push({ id: modelId, displayName, checkpoints });
  }
  return models;
}

export function getMetrics(modelId: string, checkpoint: string): MetricsFile | null {
  const filePath = path.join(BENCHMARK_DIR, modelId, checkpoint, "metrics.json");
  if (!fs.existsSync(filePath)) return null;
  const raw = fs.readFileSync(filePath, "utf-8");
  return JSON.parse(raw) as MetricsFile;
}

export function getSamples(modelId: string, checkpoint: string): SampleAnalysis[] | null {
  const filePath = path.join(BENCHMARK_DIR, modelId, checkpoint, "sample_analysis.json");
  if (!fs.existsSync(filePath)) return null;
  const raw = fs.readFileSync(filePath, "utf-8");
  return JSON.parse(raw) as SampleAnalysis[];
}

export function getAllMetricsForModel(modelId: string): Record<string, MetricsFile> {
  const result: Record<string, MetricsFile> = {};
  const model = getModels().find((m) => m.id === modelId);
  if (!model) return result;
  for (const ckpt of model.checkpoints) {
    const metrics = getMetrics(modelId, ckpt);
    if (metrics) result[ckpt] = metrics;
  }
  return result;
}


Now the API routes:


Bash

Create API route directories


(Bash completed with no output)


Write

import { getModels } from "@/lib/data";

export async function GET() {
  const models = getModels();
  return Response.json(models);
}


File created successfully at: /home/ubuntu/training/asr-benchmark-dashboard/src/app/api/models/route.ts

import { getModels } from "@/lib/data";

export async function GET() {
  const models = getModels();
  return Response.json(models);
}


import { NextRequest } from "next/server";
import { getMetrics } from "@/lib/data";

export async function GET(request: NextRequest) {
  const modelId = request.nextUrl.searchParams.get("model");
  const checkpoint = request.nextUrl.searchParams.get("checkpoint");

  if (!modelId || !checkpoint) {
    return Response.json({ error: "model and checkpoint params required" }, { status: 400 });
  }

  const metrics = getMetrics(modelId, checkpoint);
  if (!metrics) {
    return Response.json({ error: "Not found" }, { status: 404 });
  }

  return Response.json(metrics);
}


File created successfully at: /home/ubuntu/training/asr-benchmark-dashboard/src/app/api/metrics/route.ts

import { NextRequest } from "next/server";
import { getMetrics } from "@/lib/data";

export async function GET(request: NextRequest) {
  const modelId = request.nextUrl.searchParams.get("model");
  const checkpoint = request.nextUrl.searchParams.get("checkpoint");

  if (!modelId || !checkpoint) {
    return Response.json({ error: "model and checkpoint params required" }, { status: 400 });
  }

  const metrics = getMetrics(modelId, checkpoint);
  if (!metrics) {
    return Response.json({ error: "Not found" }, { status: 404 });
  }

  return Response.json(metrics);
}


import { NextRequest } from "next/server";
import { getSamples } from "@/lib/data";

export async function GET(request: NextRequest) {
  const modelId = request.nextUrl.searchParams.get("model");
  const checkpoint = request.nextUrl.searchParams.get("checkpoint");
  const language = request.nextUrl.searchParams.get("language");
  const page = parseInt(request.nextUrl.searchParams.get("page") || "1");
  const pageSize = parseInt(request.nextUrl.searchParams.get("pageSize") || "20");

  if (!modelId || !checkpoint) {
    return Response.json({ error: "model and checkpoint params required" }, { status: 400 });
  }

  const samples = getSamples(modelId, checkpoint);
  if (!samples) {
    return Response.json({ error: "Not found" }, { status: 404 });
  }

  let filtered = samples;
  if (language) {
    filtered = samples.filter((s) => s.language === language);
  }

  const total = filtered.length;
  const totalPages = Math.ceil(total / pageSize);
  const start = (page - 1) * pageSize;
  const paged = filtered.slice(start, start + pageSize);

  return Response.json({
    samples: paged,
    pagination: { page, pageSize, total, totalPages },
  });
}


File created successfully at: /home/ubuntu/training/asr-benchmark-dashboard/src/app/api/samples/route.ts

import { NextRequest } from "next/server";
import { getSamples } from "@/lib/data";

export async function GET(request: NextRequest) {
  const modelId = request.nextUrl.searchParams.get("model");
  const checkpoint = request.nextUrl.searchParams.get("checkpoint");
  const language = request.nextUrl.searchParams.get("language");
  const page = parseInt(request.nextUrl.searchParams.get("page") || "1");
  const pageSize = parseInt(request.nextUrl.searchParams.get("pageSize") || "20");

  if (!modelId || !checkpoint) {
    return Response.json({ error: "model and checkpoint params required" }, { status: 400 });
  }

  const samples = getSamples(modelId, checkpoint);
  if (!samples) {
    return Response.json({ error: "Not found" }, { status: 404 });
  }

  let filtered = samples;
  if (language) {
    filtered = samples.filter((s) => s.language === language);
  }

  const total = filtered.length;
  const totalPages = Math.ceil(total / pageSize);
  const start = (page - 1) * pageSize;
  const paged = filtered.slice(start, start + pageSize);

  return Response.json({
    samples: paged,
    pagination: { page, pageSize, total, totalPages },
  });
}


import { NextRequest } from "next/server";
import { getMetrics, getModels } from "@/lib/data";
import { LANGUAGES } from "@/lib/types";

export async function GET(request: NextRequest) {
  const models = getModels();
  const result: Record<string, Record<string, Record<string, number>>> = {};

  for (const model of models) {
    result[model.id] = {};
    for (const ckpt of model.checkpoints) {
      const metrics = getMetrics(model.id, ckpt);
      if (!metrics) continue;
      const langMetrics: Record<string, number> = {};
      for (const lang of LANGUAGES) {
        const lm = metrics[lang];
        if (lm && typeof lm === "object" && "wer" in lm) {
          langMetrics[lang] = lm.wer;
        }
      }
      langMetrics["__overall__"] = metrics.__overall__?.wer ?? 0;
      result[model.id][ckpt] = langMetrics;
    }
  }

  return Response.json(result);
}


File created successfully at: /home/ubuntu/training/asr-benchmark-dashboard/src/app/api/compare/route.ts

import { NextRequest } from "next/server";
import { getMetrics, getModels } from "@/lib/data";
import { LANGUAGES } from "@/lib/types";

export async function GET(request: NextRequest) {
  const models = getModels();
  const result: Record<string, Record<string, Record<string, number>>> = {};

  for (const model of models) {
    result[model.id] = {};
    for (const ckpt of model.checkpoints) {
      const metrics = getMetrics(model.id, ckpt);
      if (!metrics) continue;
      const langMetrics: Record<string, number> = {};
      for (const lang of LANGUAGES) {
        const lm = metrics[lang];
        if (lm && typeof lm === "object" && "wer" in lm) {
          langMetrics[lang] = lm.wer;
        }
      }
      langMetrics["__overall__"] = metrics.__overall__?.wer ?? 0;
      result[model.id][ckpt] = langMetrics;
    }
  }

  return Response.json(result);
}


Now the main components and pages. Let me build the dashboard client component and page structure.


Bash

Create components directory


Write

"use client";

import { ModelInfo, MODEL_COLORS } from "@/lib/types";

interface SidebarProps {
  models: ModelInfo[];
  selectedModel: string | null;
  selectedCheckpoint: string | null;
  activeView: "overview" | "wer-analysis";
  onSelectModel: (id: string) => void;
  onSelectCheckpoint: (ckpt: string) => void;
  onViewChange: (view: "overview" | "wer-analysis") => void;
}

export default function Sidebar({
  models,
  selectedModel,
  selectedCheckpoint,
  activeView,
  onSelectModel,
  onSelectCheckpoint,
  onViewChange,
}: SidebarProps) {
  return (

      {/* Logo */}

          ASR Benchmark

          Indic Language Evaluation

      {/* Navigation */}

          Navigation

        <button
          onClick={() => onViewChange("overview")}
          className={`w-full text-left px-3 py-2 rounded text-xs transition-colors ${
            activeView === "overview"
              ? "bg-[var(--color-card-hover)] text-[var(--color-foreground)]"
              : "text-[var(--color-muted)] hover:text-[var(--color-foreground)] hover:bg-[var(--color-card)]"
          }`}
        >
          Overview & Plots
        </button>
        <button
          onClick={() => onViewChange("wer-analysis")}
          className={`w-full text-left px-3 py-2 rounded text-xs transition-colors mt-1 ${
            activeView === "wer-analysis"
              ? "bg-[var(--color-card-hover)] text-[var(--color-foreground)]"
              : "text-[var(--color-muted)] hover:text-[var(--color-foreground)] hover:bg-[var(--color-card)]"
          }`}
        >
          WER Analysis
        </button>

      {/* Models */}

          Models

        {models.map((model) => (

            <button
              onClick={() => {
                onSelectModel(model.id);
                if (model.checkpoints.length > 0) {
                  onSelectCheckpoint(model.checkpoints[model.checkpoints.length - 1]);
                }
              }}
              className={`w-full text-left px-3 py-2 rounded text-xs transition-colors flex items-center gap-2 ${
                selectedModel === model.id
                  ? "bg-[var(--color-card-hover)] text-[var(--color-foreground)]"
                  : "text-[var(--color-muted)] hover:text-[var(--color-foreground)] hover:bg-[var(--color-card)]"
              }`}
            >

              {model.displayName}
            </button>

            {selectedModel === model.id && model.checkpoints.length > 0 && (

                {model.checkpoints.map((ckpt) => (
                  <button
                    key={ckpt}
                    onClick={() => onSelectCheckpoint(ckpt)}
                    className={`w-full text-left px-3 py-1.5 rounded text-[11px] transition-colors ${
                      selectedCheckpoint === ckpt
                        ? "text-[var(--color-foreground)] bg-[var(--color-card)]"
                        : "text-[var(--color-muted)] hover:text-[var(--color-accent)]"
                    }`}
                  >
                    {ckpt}
                  </button>
                ))}

            )}

            {model.checkpoints.length === 0 && selectedModel === model.id && (

                No benchmarks yet

            )}

        ))}

      {/* Footer */}

          Dataset: indic-asr-benchmark-6k

          12 languages · 6,000 samples

  );
}


File created successfully at: /home/ubuntu/training/asr-benchmark-dashboard/src/components/Sidebar.tsx

"use client";

import { ModelInfo, MODEL_COLORS } from "@/lib/types";

interface SidebarProps {
  models: ModelInfo[];
  selectedModel: string | null;
  selectedCheckpoint: string | null;
  activeView: "overview" | "wer-analysis";
  onSelectModel: (id: string) => void;
  onSelectCheckpoint: (ckpt: string) => void;
  onViewChange: (view: "overview" | "wer-analysis") => void;
}

export default function Sidebar({
  models,
  selectedModel,
  selectedCheckpoint,
  activeView,
  onSelectModel,
  onSelectCheckpoint,
  onViewChange,
}: SidebarProps) {
  return (

      {/* Logo */}

          ASR Benchmark

          Indic Language Evaluation

      {/* Navigation */}

          Navigation

        <button
          onClick={() => onViewChange("overview")}
          className={`w-full text-left px-3 py-2 rounded text-xs transition-colors ${
            activeView === "overview"
              ? "bg-[var(--color-card-hover)] text-[var(--color-foreground)]"
              : "text-[var(--color-muted)] hover:text-[var(--color-foreground)] hover:bg-[var(--color-card)]"
          }`}
        >
          Overview & Plots
        </button>
        <button
          onClick={() => onViewChange("wer-analysis")}
          className={`w-full text-left px-3 py-2 rounded text-xs transition-colors mt-1 ${
            activeView === "wer-analysis"
              ? "bg-[var(--color-card-hover)] text-[var(--color-foreground)]"
              : "text-[var(--color-muted)] hover:text-[var(--color-foreground)] hover:bg-[var(--color-card)]"
          }`}
        >
          WER Analysis
        </button>

      {/* Models */}

          Models

        {models.map((model) => (

            <button
              onClick={() => {
                onSelectModel(model.id);
                if (model.checkpoints.length > 0) {
                  onSelectCheckpoint(model.checkpoints[model.checkpoints.length - 1]);
                }
              }}
              className={`w-full text-left px-3 py-2 rounded text-xs transition-colors flex items-center gap-2 ${
                selectedModel === model.id
                  ? "bg-[var(--color-card-hover)] text-[var(--color-foreground)]"
                  : "text-[var(--color-muted)] hover:text-[var(--color-foreground)] hover:bg-[var(--color-card)]"
              }`}
            >

              {model.displayName}
            </button>

            {selectedModel === model.id && model.checkpoints.length > 0 && (

                {model.checkpoints.map((ckpt) => (
                  <button
                    key={ckpt}
                    onClick={() => onSelectCheckpoint(ckpt)}
                    className={`w-full text-left px-3 py-1.5 rounded text-[11px] transition-colors ${
                      selectedCheckpoint === ckpt
                        ? "text-[var(--color-foreground)] bg-[var(--color-card)]"
                        : "text-[var(--color-muted)] hover:text-[var(--color-accent)]"
                    }`}
                  >
                    {ckpt}
                  </button>
                ))}

            )}

            {model.checkpoints.length === 0 && selectedModel === model.id && (

                No benchmarks yet

            )}

        ))}

      {/* Footer */}

          Dataset: indic-asr-benchmark-6k

          12 languages · 6,000 samples

  );
}


"use client";

interface MetricCardProps {
  label: string;
  value: string | number;
  subtitle?: string;
  trend?: "up" | "down" | "neutral";
  trendValue?: string;
}

export default function MetricCard({ label, value, subtitle, trend, trendValue }: MetricCardProps) {
  return (

        {label}

          {typeof value === "number" ? value.toFixed(2) : value}

        {trend && trendValue && (

            {trend === "down" ? "\u2193" : trend === "up" ? "\u2191" : "\u2014"} {trendValue}

        )}

      {subtitle && (

{subtitle}

      )}

  );
}


File created successfully at: /home/ubuntu/training/asr-benchmark-dashboard/src/components/MetricCard.tsx

"use client";

interface MetricCardProps {
  label: string;
  value: string | number;
  subtitle?: string;
  trend?: "up" | "down" | "neutral";
  trendValue?: string;
}

export default function MetricCard({ label, value, subtitle, trend, trendValue }: MetricCardProps) {
  return (

        {label}

          {typeof value === "number" ? value.toFixed(2) : value}

        {trend && trendValue && (

            {trend === "down" ? "\u2193" : trend === "up" ? "\u2191" : "\u2014"} {trendValue}

        )}

      {subtitle && (

{subtitle}

      )}

  );
}


"use client";

import {
  BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip,
  ResponsiveContainer, Cell, ReferenceLine,
} from "recharts";
import { CHART_COLORS, LANGUAGES } from "@/lib/types";

interface LanguageBarChartProps {
  data: { language: string; wer: number; cer: number; wer_normalized: number }[];
  metric: "wer" | "cer" | "wer_normalized";
  title: string;
  height?: number;
}

export default function LanguageBarChart({ data, metric, title, height = 320 }: LanguageBarChartProps) {
  const sorted = [...data].sort((a, b) => a[metric] - b[metric]);
  const avg = sorted.reduce((s, d) => s + d[metric], 0) / sorted.length;

  return (

{title}

        avg: {avg.toFixed(2)}%

      <ResponsiveContainer width="100%" height={height}>
        <BarChart data={sorted} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
          <CartesianGrid strokeDasharray="3 3" vertical={false} />
          <XAxis
            dataKey="language"
            tick={{ fontSize: 10 }}
            tickFormatter={(v: string) => v.slice(0, 3).toUpperCase()}
            axisLine={false}
            tickLine={false}
          />
          <YAxis
            tick={{ fontSize: 10 }}
            axisLine={false}
            tickLine={false}
            tickFormatter={(v: number) => `${v}%`}
          />
          <Tooltip
            formatter={(value: number) => [`${value.toFixed(2)}%`, metric.toUpperCase()]}
            contentStyle={{
              background: "#18181b",
              border: "1px solid #3f3f46",
              borderRadius: "8px",
              fontSize: "12px",
            }}
          />
          <ReferenceLine y={avg} stroke="#71717a" strokeDasharray="5 5" />
          <Bar dataKey={metric} radius={[4, 4, 0, 0]} maxBarSize={40}>
            {sorted.map((_, i) => (
              <Cell key={i} fill={CHART_COLORS[i % CHART_COLORS.length]} />
            ))}
          </Bar>
        </BarChart>
      </ResponsiveContainer>

  );
}


File created successfully at: /home/ubuntu/training/asr-benchmark-dashboard/src/components/LanguageBarChart.tsx

"use client";

import {
  BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip,
  ResponsiveContainer, Cell, ReferenceLine,
} from "recharts";
import { CHART_COLORS, LANGUAGES } from "@/lib/types";

interface LanguageBarChartProps {
  data: { language: string; wer: number; cer: number; wer_normalized: number }[];
  metric: "wer" | "cer" | "wer_normalized";
  title: string;
  height?: number;
}

export default function LanguageBarChart({ data, metric, title, height = 320 }: LanguageBarChartProps) {
  const sorted = [...data].sort((a, b) => a[metric] - b[metric]);
  const avg = sorted.reduce((s, d) => s + d[metric], 0) / sorted.length;

  return (

{title}

        avg: {avg.toFixed(2)}%

      <ResponsiveContainer width="100%" height={height}>
        <BarChart data={sorted} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
          <CartesianGrid strokeDasharray="3 3" vertical={false} />
          <XAxis
            dataKey="language"
            tick={{ fontSize: 10 }}
            tickFormatter={(v: string) => v.slice(0, 3).toUpperCase()}
            axisLine={false}
            tickLine={false}
          />
          <YAxis
            tick={{ fontSize: 10 }}
            axisLine={false}
            tickLine={false}
            tickFormatter={(v: number) => `${v}%`}
          />
          <Tooltip
            formatter={(value: number) => [`${value.toFixed(2)}%`, metric.toUpperCase()]}
            contentStyle={{
              background: "#18181b",
              border: "1px solid #3f3f46",
              borderRadius: "8px",
              fontSize: "12px",
            }}
          />
          <ReferenceLine y={avg} stroke="#71717a" strokeDasharray="5 5" />
          <Bar dataKey={metric} radius={[4, 4, 0, 0]} maxBarSize={40}>
            {sorted.map((_, i) => (
              <Cell key={i} fill={CHART_COLORS[i % CHART_COLORS.length]} />
            ))}
          </Bar>
        </BarChart>
      </ResponsiveContainer>

  );
}


"use client";

import {
  LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip,
  ResponsiveContainer, Legend,
} from "recharts";
import { CHART_COLORS } from "@/lib/types";

interface CheckpointLineChartProps {
  data: Record<string, Record<string, number>>; // { ckpt: { lang: wer } }
  languages: string[];
  title: string;
  height?: number;
}

export default function CheckpointLineChart({ data, languages, title, height = 360 }: CheckpointLineChartProps) {
  const checkpoints = Object.keys(data).sort((a, b) => {
    const numA = parseInt(a.replace(/\D/g, "")) || 0;
    const numB = parseInt(b.replace(/\D/g, "")) || 0;
    return numA - numB;
  });

  const chartData = checkpoints.map((ckpt) => {
    const row: Record<string, string | number> = { checkpoint: ckpt };
    for (const lang of languages) {
      row[lang] = data[ckpt]?.[lang] ?? 0;
    }
    return row;
  });

  return (

{title}

      <ResponsiveContainer width="100%" height={height}>
        <LineChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
          <CartesianGrid strokeDasharray="3 3" vertical={false} />
          <XAxis dataKey="checkpoint" tick={{ fontSize: 10 }} axisLine={false} tickLine={false} />
          <YAxis tick={{ fontSize: 10 }} axisLine={false} tickLine={false} tickFormatter={(v: number) => `${v}%`} />
          <Tooltip
            formatter={(value: number, name: string) => [`${value.toFixed(2)}%`, name]}
            contentStyle={{
              background: "#18181b",
              border: "1px solid #3f3f46",
              borderRadius: "8px",
              fontSize: "11px",
            }}
          />
          <Legend
            wrapperStyle={{ fontSize: "10px", paddingTop: "8px" }}
            formatter={(value: string) => value.slice(0, 3).toUpperCase()}
          />
          {languages.map((lang, i) => (
            <Line
              key={lang}
              type="monotone"
              dataKey={lang}
              stroke={CHART_COLORS[i % CHART_COLORS.length]}
              strokeWidth={2}
              dot={{ r: 3, fill: CHART_COLORS[i % CHART_COLORS.length] }}
              activeDot={{ r: 5 }}
            />
          ))}
        </LineChart>
      </ResponsiveContainer>

  );
}


File created successfully at: /home/ubuntu/training/asr-benchmark-dashboard/src/components/CheckpointLineChart.tsx

"use client";

import {
  LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip,
  ResponsiveContainer, Legend,
} from "recharts";
import { CHART_COLORS } from "@/lib/types";

interface CheckpointLineChartProps {
  data: Record<string, Record<string, number>>; // { ckpt: { lang: wer } }
  languages: string[];
  title: string;
  height?: number;
}

export default function CheckpointLineChart({ data, languages, title, height = 360 }: CheckpointLineChartProps) {
  const checkpoints = Object.keys(data).sort((a, b) => {
    const numA = parseInt(a.replace(/\D/g, "")) || 0;
    const numB = parseInt(b.replace(/\D/g, "")) || 0;
    return numA - numB;
  });

  const chartData = checkpoints.map((ckpt) => {
    const row: Record<string, string | number> = { checkpoint: ckpt };
    for (const lang of languages) {
      row[lang] = data[ckpt]?.[lang] ?? 0;
    }
    return row;
  });

  return (

{title}

      <ResponsiveContainer width="100%" height={height}>
        <LineChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
          <CartesianGrid strokeDasharray="3 3" vertical={false} />
          <XAxis dataKey="checkpoint" tick={{ fontSize: 10 }} axisLine={false} tickLine={false} />
          <YAxis tick={{ fontSize: 10 }} axisLine={false} tickLine={false} tickFormatter={(v: number) => `${v}%`} />
          <Tooltip
            formatter={(value: number, name: string) => [`${value.toFixed(2)}%`, name]}
            contentStyle={{
              background: "#18181b",
              border: "1px solid #3f3f46",
              borderRadius: "8px",
              fontSize: "11px",
            }}
          />
          <Legend
            wrapperStyle={{ fontSize: "10px", paddingTop: "8px" }}
            formatter={(value: string) => value.slice(0, 3).toUpperCase()}
          />
          {languages.map((lang, i) => (
            <Line
              key={lang}
              type="monotone"
              dataKey={lang}
              stroke={CHART_COLORS[i % CHART_COLORS.length]}
              strokeWidth={2}
              dot={{ r: 3, fill: CHART_COLORS[i % CHART_COLORS.length] }}
              activeDot={{ r: 5 }}
            />
          ))}
        </LineChart>
      </ResponsiveContainer>

  );
}


"use client";

import {
  BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip,
  ResponsiveContainer, Legend,
} from "recharts";
import { MODEL_COLORS, MODEL_REGISTRY, LANGUAGES } from "@/lib/types";

interface CrossModelChartProps {
  data: Record<string, Record<string, Record<string, number>>>; // { modelId: { ckpt: { lang: wer } } }
  selectedCheckpoints: Record<string, string>; // { modelId: ckpt }
  title: string;
  height?: number;
}

export default function CrossModelChart({ data, selectedCheckpoints, title, height = 360 }: CrossModelChartProps) {
  const activeModels = Object.keys(selectedCheckpoints).filter(
    (m) => data[m] && data[m][selectedCheckpoints[m]]
  );

  const chartData = [...LANGUAGES].map((lang) => {
    const row: Record<string, string | number> = { language: lang };
    for (const modelId of activeModels) {
      const ckpt = selectedCheckpoints[modelId];
      row[modelId] = data[modelId]?.[ckpt]?.[lang] ?? 0;
    }
    return row;
  });

  return (

{title}

      <ResponsiveContainer width="100%" height={height}>
        <BarChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
          <CartesianGrid strokeDasharray="3 3" vertical={false} />
          <XAxis
            dataKey="language"
            tick={{ fontSize: 10 }}
            tickFormatter={(v: string) => v.slice(0, 3).toUpperCase()}
            axisLine={false}
            tickLine={false}
          />
          <YAxis tick={{ fontSize: 10 }} axisLine={false} tickLine={false} tickFormatter={(v: number) => `${v}%`} />
          <Tooltip
            formatter={(value: number, name: string) => [`${value.toFixed(2)}%`, MODEL_REGISTRY[name] || name]}
            contentStyle={{
              background: "#18181b",
              border: "1px solid #3f3f46",
              borderRadius: "8px",
              fontSize: "11px",
            }}
          />
          <Legend
            wrapperStyle={{ fontSize: "10px", paddingTop: "8px" }}
            formatter={(value: string) => `${MODEL_REGISTRY[value]?.split("-")[0] || value} (${selectedCheckpoints[value]})`}
          />
          {activeModels.map((modelId) => (
            <Bar
              key={modelId}
              dataKey={modelId}
              fill={MODEL_COLORS[modelId] || "#6366f1"}
              radius={[3, 3, 0, 0]}
              maxBarSize={28}
            />
          ))}
        </BarChart>
      </ResponsiveContainer>

  );
}


File created successfully at: /home/ubuntu/training/asr-benchmark-dashboard/src/components/CrossModelChart.tsx

"use client";

import {
  BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip,
  ResponsiveContainer, Legend,
} from "recharts";
import { MODEL_COLORS, MODEL_REGISTRY, LANGUAGES } from "@/lib/types";

interface CrossModelChartProps {
  data: Record<string, Record<string, Record<string, number>>>; // { modelId: { ckpt: { lang: wer } } }
  selectedCheckpoints: Record<string, string>; // { modelId: ckpt }
  title: string;
  height?: number;
}

export default function CrossModelChart({ data, selectedCheckpoints, title, height = 360 }: CrossModelChartProps) {
  const activeModels = Object.keys(selectedCheckpoints).filter(
    (m) => data[m] && data[m][selectedCheckpoints[m]]
  );

  const chartData = [...LANGUAGES].map((lang) => {
    const row: Record<string, string | number> = { language: lang };
    for (const modelId of activeModels) {
      const ckpt = selectedCheckpoints[modelId];
      row[modelId] = data[modelId]?.[ckpt]?.[lang] ?? 0;
    }
    return row;
  });

  return (

{title}

      <ResponsiveContainer width="100%" height={height}>
        <BarChart data={chartData} margin={{ top: 5, right: 10, left: 0, bottom: 5 }}>
          <CartesianGrid strokeDasharray="3 3" vertical={false} />
          <XAxis
            dataKey="language"
            tick={{ fontSize: 10 }}
            tickFormatter={(v: string) => v.slice(0, 3).toUpperCase()}
            axisLine={false}
            tickLine={false}
          />
          <YAxis tick={{ fontSize: 10 }} axisLine={false} tickLine={false} tickFormatter={(v: number) => `${v}%`} />
          <Tooltip
            formatter={(value: number, name: string) => [`${value.toFixed(2)}%`, MODEL_REGISTRY[name] || name]}
            contentStyle={{
              background: "#18181b",
              border: "1px solid #3f3f46",
              borderRadius: "8px",
              fontSize: "11px",
            }}
          />
          <Legend
            wrapperStyle={{ fontSize: "10px", paddingTop: "8px" }}
            formatter={(value: string) => `${MODEL_REGISTRY[value]?.split("-")[0] || value} (${selectedCheckpoints[value]})`}
          />
          {activeModels.map((modelId) => (
            <Bar
              key={modelId}
              dataKey={modelId}
              fill={MODEL_COLORS[modelId] || "#6366f1"}
              radius={[3, 3, 0, 0]}
              maxBarSize={28}
            />
          ))}
        </BarChart>
      </ResponsiveContainer>

  );
}


"use client";

import { LANGUAGES } from "@/lib/types";

interface HeatmapProps {
  data: Record<string, Record<string, number>>; // { ckpt: { lang: wer } }
  title: string;
}

function getColor(wer: number): string {
  if (wer <= 15) return "#22c55e";
  if (wer <= 25) return "#86efac";
  if (wer <= 35) return "#fbbf24";
  if (wer <= 45) return "#fb923c";
  if (wer <= 55) return "#f87171";
  return "#ef4444";
}

function getTextColor(wer: number): string {
  if (wer <= 15) return "#000";
  if (wer <= 25) return "#000";
  return "#fff";
}

export default function LanguageHeatmap({ data, title }: HeatmapProps) {
  const checkpoints = Object.keys(data).sort((a, b) => {
    const numA = parseInt(a.replace(/\D/g, "")) || 0;
    const numB = parseInt(b.replace(/\D/g, "")) || 0;
    return numA - numB;
  });

  return (

{title}

            Language
            {checkpoints.map((ckpt) => (

                {ckpt}

            ))}
            {checkpoints.length >= 2 && (
              Delta
            )}

          {LANGUAGES.map((lang) => {
            const values = checkpoints.map((ckpt) => data[ckpt]?.[lang] ?? 0);
            const delta = checkpoints.length >= 2
              ? values[values.length - 1] - values[0]
              : 0;
            return (

                  {lang}

                {values.map((v, i) => (

                      {v.toFixed(1)}%

                ))}
                {checkpoints.length >= 2 && (

                    <span
                      className={`text-[10px] font-medium tabular-nums ${
                        delta < 0 ? "text-[var(--color-success)]" : delta > 0 ? "text-[var(--color-danger)]" : "text-[var(--color-muted)]"
                      }`}
                    >
                      {delta > 0 ? "+" : ""}{delta.toFixed(1)}%

                )}

            );
          })}
          {/* Overall row */}

            Overall
            {checkpoints.map((ckpt) => {
              const v = data[ckpt]?.["__overall__"] ?? 0;
              return (

                    {v.toFixed(1)}%

              );
            })}
            {checkpoints.length >= 2 && (() => {
              const first = data[checkpoints[0]]?.["__overall__"] ?? 0;
              const last = data[checkpoints[checkpoints.length - 1]]?.["__overall__"] ?? 0;
              const d = last - first;
              return (

                  <span className={`text-[10px] font-bold tabular-nums ${d < 0 ? "text-[var(--color-success)]" : d > 0 ? "text-[var(--color-danger)]" : "text-[var(--color-muted)]"}`}>
                    {d > 0 ? "+" : ""}{d.toFixed(1)}%

              );
            })()}

      {/* Legend */}

        {[
          { label: "< 15%", color: "#22c55e" },
          { label: "15-25%", color: "#86efac" },
          { label: "25-35%", color: "#fbbf24" },
          { label: "35-45%", color: "#fb923c" },
          { label: "45-55%", color: "#f87171" },
          { label: "> 55%", color: "#ef4444" },
        ].map((item) => (

            {item.label}

        ))}

  );
}


File created successfully at: /home/ubuntu/training/asr-benchmark-dashboard/src/components/LanguageHeatmap.tsx

"use client";

import { LANGUAGES } from "@/lib/types";

interface HeatmapProps {
  data: Record<string, Record<string, number>>; // { ckpt: { lang: wer } }
  title: string;
}

function getColor(wer: number): string {
  if (wer <= 15) return "#22c55e";
  if (wer <= 25) return "#86efac";
  if (wer <= 35) return "#fbbf24";
  if (wer <= 45) return "#fb923c";
  if (wer <= 55) return "#f87171";
  return "#ef4444";
}

function getTextColor(wer: number): string {
  if (wer <= 15) return "#000";
  if (wer <= 25) return "#000";
  return "#fff";
}

export default function LanguageHeatmap({ data, title }: HeatmapProps) {
  const checkpoints = Object.keys(data).sort((a, b) => {
    const numA = parseInt(a.replace(/\D/g, "")) || 0;
    const numB = parseInt(b.replace(/\D/g, "")) || 0;
    return numA - numB;
  });

  return (

{title}

            Language
            {checkpoints.map((ckpt) => (

                {ckpt}

            ))}
            {checkpoints.length >= 2 && (
              Delta
            )}

          {LANGUAGES.map((lang) => {
            const values = checkpoints.map((ckpt) => data[ckpt]?.[lang] ?? 0);
            const delta = checkpoints.length >= 2
              ? values[values.length - 1] - values[0]
              : 0;
            return (

                  {lang}

                {values.map((v, i) => (

                      {v.toFixed(1)}%

                ))}
                {checkpoints.length >= 2 && (

                    <span
                      className={`text-[10px] font-medium tabular-nums ${
                        delta < 0 ? "text-[var(--color-success)]" : delta > 0 ? "text-[var(--color-danger)]" : "text-[var(--color-muted)]"
                      }`}
                    >
                      {delta > 0 ? "+" : ""}{delta.toFixed(1)}%

                )}

            );
          })}
          {/* Overall row */}

            Overall
            {checkpoints.map((ckpt) => {
              const v = data[ckpt]?.["__overall__"] ?? 0;
              return (

                    {v.toFixed(1)}%

              );
            })}
            {checkpoints.length >= 2 && (() => {
              const first = data[checkpoints[0]]?.["__overall__"] ?? 0;
              const last = data[checkpoints[checkpoints.length - 1]]?.["__overall__"] ?? 0;
              const d = last - first;
              return (

                  <span className={`text-[10px] font-bold tabular-nums ${d < 0 ? "text-[var(--color-success)]" : d > 0 ? "text-[var(--color-danger)]" : "text-[var(--color-muted)]"}`}>
                    {d > 0 ? "+" : ""}{d.toFixed(1)}%

              );
            })()}

      {/* Legend */}

        {[
          { label: "< 15%", color: "#22c55e" },
          { label: "15-25%", color: "#86efac" },
          { label: "25-35%", color: "#fbbf24" },
          { label: "35-45%", color: "#fb923c" },
          { label: "45-55%", color: "#f87171" },
          { label: "> 55%", color: "#ef4444" },
        ].map((item) => (

            {item.label}

        ))}

  );
}


"use client";

import { useState } from "react";

interface Sample {
  id: string;
  language: string;
  reference: string;
  hypothesis: string;
  detected_language?: string;
}

interface Pagination {
  page: number;
  pageSize: number;
  total: number;
  totalPages: number;
}

interface SampleTableProps {
  samples: Sample[];
  pagination: Pagination;
  onPageChange: (page: number) => void;
  loading: boolean;
}

function highlightMismatches(ref: string, hyp: string): { refParts: { text: string; match: boolean }[]; hypParts: { text: string; match: boolean }[] } {
  const refWords = ref.split(/\s+/);
  const hypWords = hyp.split(/\s+/);
  const maxLen = Math.max(refWords.length, hypWords.length);

  const refParts: { text: string; match: boolean }[] = [];
  const hypParts: { text: string; match: boolean }[] = [];

  for (let i = 0; i < maxLen; i++) {
    const rw = refWords[i] || "";
    const hw = hypWords[i] || "";
    const match = rw === hw;
    if (rw) refParts.push({ text: rw, match });
    if (hw) hypParts.push({ text: hw, match });
  }

  return { refParts, hypParts };
}

export default function SampleTable({ samples, pagination, onPageChange, loading }: SampleTableProps) {
  const [expandedId, setExpandedId] = useState<string | null>(null);

  return (

      {/* Header */}

          Sample Predictions

          {pagination.total} samples · page {pagination.page}/{pagination.totalPages}

      {loading ? (

Loading...

      ) : (
        <>

                  ID
                  Lang
                  Reference
                  Hypothesis

                {samples.map((s) => {
                  const isExpanded = expandedId === s.id;
                  const { refParts, hypParts } = highlightMismatches(s.reference, s.hypothesis);
                  return (

 setExpandedId(isExpanded ? null : s.id)}
                    >

                        {s.id}

                        {s.language.slice(0, 3)}

                          {isExpanded
                            ? refParts.map((p, i) => (

                                  {p.text}{" "}

                              ))
                            : s.reference}

                          {isExpanded
                            ? hypParts.map((p, i) => (

                                  {p.text}{" "}

                              ))
                            : s.hypothesis}

                  );
                })}

          {/* Pagination */}

            <button
              onClick={() => onPageChange(pagination.page - 1)}
              disabled={pagination.page <= 1}
              className="text-[11px] px-3 py-1 rounded border border-[var(--color-border)] text-[var(--color-muted)] hover:text-[var(--color-foreground)] hover:border-[var(--color-border-light)] disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
            >
              Previous
            </button>

              {Array.from({ length: Math.min(pagination.totalPages, 7) }, (_, i) => {
                let pageNum: number;
                if (pagination.totalPages <= 7) {
                  pageNum = i + 1;
                } else if (pagination.page <= 4) {
                  pageNum = i + 1;
                } else if (pagination.page >= pagination.totalPages - 3) {
                  pageNum = pagination.totalPages - 6 + i;
                } else {
                  pageNum = pagination.page - 3 + i;
                }
                return (
                  <button
                    key={pageNum}
                    onClick={() => onPageChange(pageNum)}
                    className={`text-[10px] w-6 h-6 rounded transition-colors ${
                      pageNum === pagination.page
                        ? "bg-[var(--color-card-hover)] text-[var(--color-foreground)]"
                        : "text-[var(--color-muted)] hover:text-[var(--color-foreground)]"
                    }`}
                  >
                    {pageNum}
                  </button>
                );
              })}

            <button
              onClick={() => onPageChange(pagination.page + 1)}
              disabled={pagination.page >= pagination.totalPages}
              className="text-[11px] px-3 py-1 rounded border border-[var(--color-border)] text-[var(--color-muted)] hover:text-[var(--color-foreground)] hover:border-[var(--color-border-light)] disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
            >
              Next
            </button>

        </>
      )}

  );
}


File created successfully at: /home/ubuntu/training/asr-benchmark-dashboard/src/components/SampleTable.tsx

"use client";

import { useState } from "react";

interface Sample {
  id: string;
  language: string;
  reference: string;
  hypothesis: string;
  detected_language?: string;
}

interface Pagination {
  page: number;
  pageSize: number;
  total: number;
  totalPages: number;
}

interface SampleTableProps {
  samples: Sample[];
  pagination: Pagination;
  onPageChange: (page: number) => void;
  loading: boolean;
}

function highlightMismatches(ref: string, hyp: string): { refParts: { text: string; match: boolean }[]; hypParts: { text: string; match: boolean }[] } {
  const refWords = ref.split(/\s+/);
  const hypWords = hyp.split(/\s+/);
  const maxLen = Math.max(refWords.length, hypWords.length);

  const refParts: { text: string; match: boolean }[] = [];
  const hypParts: { text: string; match: boolean }[] = [];

  for (let i = 0; i < maxLen; i++) {
    const rw = refWords[i] || "";
    const hw = hypWords[i] || "";
    const match = rw === hw;
    if (rw) refParts.push({ text: rw, match });
    if (hw) hypParts.push({ text: hw, match });
  }

  return { refParts, hypParts };
}

export default function SampleTable({ samples, pagination, onPageChange, loading }: SampleTableProps) {
  const [expandedId, setExpandedId] = useState<string | null>(null);

  return (

      {/* Header */}

          Sample Predictions

          {pagination.total} samples · page {pagination.page}/{pagination.totalPages}

      {loading ? (

Loading...

      ) : (
        <>

                  ID
                  Lang
                  Reference
                  Hypothesis

                {samples.map((s) => {
                  const isExpanded = expandedId === s.id;
                  const { refParts, hypParts } = highlightMismatches(s.reference, s.hypothesis);
                  return (

 setExpandedId(isExpanded ? null : s.id)}
                    >

                        {s.id}

                        {s.language.slice(0, 3)}

                          {isExpanded
                            ? refParts.map((p, i) => (

                                  {p.text}{" "}

                              ))
                            : s.reference}

                          {isExpanded
                            ? hypParts.map((p, i) => (

                                  {p.text}{" "}

                              ))
                            : s.hypothesis}

                  );
                })}

          {/* Pagination */}

            <button
              onClick={() => onPageChange(pagination.page - 1)}
              disabled={pagination.page <= 1}
              className="text-[11px] px-3 py-1 rounded border border-[var(--color-border)] text-[var(--color-muted)] hover:text-[var(--color-foreground)] hover:border-[var(--color-border-light)] disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
            >
              Previous
            </button>

              {Array.from({ length: Math.min(pagination.totalPages, 7) }, (_, i) => {
                let pageNum: number;
                if (pagination.totalPages <= 7) {
                  pageNum = i + 1;
                } else if (pagination.page <= 4) {
                  pageNum = i + 1;
                } else if (pagination.page >= pagination.totalPages - 3) {
                  pageNum = pagination.totalPages - 6 + i;
                } else {
                  pageNum = pagination.page - 3 + i;
                }
                return (
                  <button
                    key={pageNum}
                    onClick={() => onPageChange(pageNum)}
                    className={`text-[10px] w-6 h-6 rounded transition-colors ${
                      pageNum === pagination.page
                        ? "bg-[var(--color-card-hover)] text-[var(--color-foreground)]"
                        : "text-[var(--color-muted)] hover:text-[var(--color-foreground)]"
                    }`}
                  >
                    {pageNum}
                  </button>
                );
              })}

            <button
              onClick={() => onPageChange(pagination.page + 1)}
              disabled={pagination.page >= pagination.totalPages}
              className="text-[11px] px-3 py-1 rounded border border-[var(--color-border)] text-[var(--color-muted)] hover:text-[var(--color-foreground)] hover:border-[var(--color-border-light)] disabled:opacity-30 disabled:cursor-not-allowed transition-colors"
            >
              Next
            </button>

        </>
      )}

  );
}


Now the main dashboard page:


"use client";

import { useState, useEffect, useCallback } from "react";
import Sidebar from "@/components/Sidebar";
import MetricCard from "@/components/MetricCard";
import LanguageBarChart from "@/components/LanguageBarChart";
import CheckpointLineChart from "@/components/CheckpointLineChart";
import CrossModelChart from "@/components/CrossModelChart";
import LanguageHeatmap from "@/components/LanguageHeatmap";
import SampleTable from "@/components/SampleTable";
import { ModelInfo, LANGUAGES, MODEL_REGISTRY } from "@/lib/types";

type ViewMode = "overview" | "wer-analysis";

interface MetricsData {
  [key: string]: {
    n_samples?: number;
    wer?: number;
    cer?: number;
    wer_normalized?: number;
    cer_normalized?: number;
    empty_hypotheses?: number;
    n_languages?: number;
    checkpoint?: string;
    checkpoint_name?: string;
    model_id?: string;
    model_type?: string;
    dataset?: string;
    batch_size?: number;
    inference_time_sec?: number;
    total_audio_sec?: number;
    rtf?: number;
    timestamp?: string;
  };
}

interface SampleData {
  id: string;
  language: string;
  reference: string;
  hypothesis: string;
  detected_language?: string;
}

interface PaginationData {
  page: number;
  pageSize: number;
  total: number;
  totalPages: number;
}

export default function Dashboard() {
  const [models, setModels] = useState<ModelInfo[]>([]);
  const [selectedModel, setSelectedModel] = useState<string | null>(null);
  const [selectedCheckpoint, setSelectedCheckpoint] = useState<string | null>(null);
  const [activeView, setActiveView] = useState<ViewMode>("overview");
  const [metrics, setMetrics] = useState<MetricsData | null>(null);
  const [compareData, setCompareData] = useState<Record<string, Record<string, Record<string, number>>>>({});
  const [samples, setSamples] = useState<SampleData[]>([]);
  const [pagination, setPagination] = useState<PaginationData>({ page: 1, pageSize: 20, total: 0, totalPages: 0 });
  const [sampleLanguage, setSampleLanguage] = useState<string>("all");
  const [samplePage, setSamplePage] = useState(1);
  const [samplesLoading, setSamplesLoading] = useState(false);
  const [compareCheckpoints, setCompareCheckpoints] = useState<Record<string, string>>({});

  // Load models
  useEffect(() => {
    fetch("/api/models")
      .then((r) => r.json())
      .then((data: ModelInfo[]) => {
        setModels(data);
        const firstWithCkpts = data.find((m) => m.checkpoints.length > 0);
        if (firstWithCkpts) {
          setSelectedModel(firstWithCkpts.id);
          setSelectedCheckpoint(firstWithCkpts.checkpoints[firstWithCkpts.checkpoints.length - 1]);
        }
        // Set default compare checkpoints (latest for each model)
        const defaults: Record<string, string> = {};
        for (const m of data) {
          if (m.checkpoints.length > 0) {
            defaults[m.id] = m.checkpoints[m.checkpoints.length - 1];
          }
        }
        setCompareCheckpoints(defaults);
      });
  }, []);

  // Load comparison data
  useEffect(() => {
    fetch("/api/compare")
      .then((r) => r.json())
      .then(setCompareData);
  }, []);

  // Load metrics when model/checkpoint changes
  useEffect(() => {
    if (!selectedModel || !selectedCheckpoint) return;
    fetch(`/api/metrics?model=${selectedModel}&checkpoint=${selectedCheckpoint}`)
      .then((r) => r.json())
      .then(setMetrics);
  }, [selectedModel, selectedCheckpoint]);

  // Load samples
  const loadSamples = useCallback(() => {
    if (!selectedModel || !selectedCheckpoint) return;
    setSamplesLoading(true);
    const langParam = sampleLanguage === "all" ? "" : `&language=${sampleLanguage}`;
    fetch(`/api/samples?model=${selectedModel}&checkpoint=${selectedCheckpoint}&page=${samplePage}&pageSize=20${langParam}`)
      .then((r) => r.json())
      .then((data) => {
        setSamples(data.samples || []);
        setPagination(data.pagination || { page: 1, pageSize: 20, total: 0, totalPages: 0 });
        setSamplesLoading(false);
      })
      .catch(() => setSamplesLoading(false));
  }, [selectedModel, selectedCheckpoint, samplePage, sampleLanguage]);

  useEffect(() => {
    loadSamples();
  }, [loadSamples]);

  const languageData = metrics
    ? LANGUAGES.map((lang) => {
        const lm = metrics[lang];
        return {
          language: lang,
          wer: (lm?.wer as number) ?? 0,
          cer: (lm?.cer as number) ?? 0,
          wer_normalized: (lm?.wer_normalized as number) ?? 0,
        };
      })
    : [];

  const overall = metrics?.__overall__;
  const meta = metrics?.__meta__;

  // Build checkpoint progression data for current model
  const ckptProgressionData: Record<string, Record<string, number>> = {};
  if (selectedModel && compareData[selectedModel]) {
    for (const [ckpt, langData] of Object.entries(compareData[selectedModel])) {
      ckptProgressionData[ckpt] = langData;
    }
  }

  return (

      <Sidebar
        models={models}
        selectedModel={selectedModel}
        selectedCheckpoint={selectedCheckpoint}
        activeView={activeView}
        onSelectModel={setSelectedModel}
        onSelectCheckpoint={(ckpt) => {
          setSelectedCheckpoint(ckpt);
          setSamplePage(1);
        }}
        onViewChange={setActiveView}
      />

        {!selectedModel || !selectedCheckpoint ? (

Select a model and checkpoint to begin.

        ) : activeView === "overview" ? (

            {/* Header */}

                  {MODEL_REGISTRY[selectedModel] || selectedModel}

                  {selectedCheckpoint} · {meta?.dataset || "indic-asr-benchmark-6k"}

                  RTF: {meta?.rtf?.toFixed(4) ?? "—"} · Inference: {meta?.inference_time_sec ? `${(meta.inference_time_sec / 60).toFixed(1)}m` : "—"}

                {meta?.timestamp && (

                    {new Date(meta.timestamp as string).toLocaleDateString()}

                )}

            {/* Metric Cards */}

              <MetricCard
                label="Overall WER"
                value={overall?.wer ?? 0}
                subtitle="Word Error Rate"
              />
              <MetricCard
                label="Overall CER"
                value={overall?.cer ?? 0}
                subtitle="Character Error Rate"
              />
              <MetricCard
                label="Normalized WER"
                value={overall?.wer_normalized ?? 0}
                subtitle="After text normalization"
              />
              <MetricCard
                label="Normalized CER"
                value={overall?.cer_normalized ?? 0}
                subtitle="After text normalization"
              />

            {/* Bar Charts */}

              <LanguageBarChart
                data={languageData}
                metric="wer"
                title="WER by Language"
              />
              <LanguageBarChart
                data={languageData}
                metric="cer"
                title="CER by Language"
              />

            {/* Checkpoint Progression (only if multiple checkpoints) */}
            {Object.keys(ckptProgressionData).length > 1 && (
              <CheckpointLineChart
                data={ckptProgressionData}
                languages={[...LANGUAGES]}
                title={`WER Progression Across Checkpoints — ${MODEL_REGISTRY[selectedModel]}`}
              />
            )}

            {/* Cross-Model Comparison */}
            {Object.keys(compareCheckpoints).length > 1 && (
              <>

                    Cross-Model Comparison

                  {models
                    .filter((m) => m.checkpoints.length > 0)
                    .map((m) => (
                      <select
                        key={m.id}
                        value={compareCheckpoints[m.id] || ""}
                        onChange={(e) =>
                          setCompareCheckpoints((prev) => ({ ...prev, [m.id]: e.target.value }))
                        }
                        className="bg-[var(--color-card)] border border-[var(--color-border)] rounded px-2 py-1 text-[10px] text-[var(--color-foreground)]"
                      >
                        {m.checkpoints.map((ckpt) => (
                          <option key={ckpt} value={ckpt}>
                            {MODEL_REGISTRY[m.id]?.split("-")[0]} — {ckpt}
                          </option>
                        ))}
                      </select>
                    ))}

                <CrossModelChart
                  data={compareData}
                  selectedCheckpoints={compareCheckpoints}
                  title="WER Comparison Across Models (by Language)"
                />
              </>
            )}

            {/* Normalized WER bar chart */}
            <LanguageBarChart
              data={languageData}
              metric="wer_normalized"
              title="Normalized WER by Language"
            />

        ) : (
          /* WER Analysis View */

                WER Analysis — {MODEL_REGISTRY[selectedModel] || selectedModel}

                Language-wise comparison across checkpoints

            {/* Heatmap */}
            {Object.keys(ckptProgressionData).length > 0 && (
              <LanguageHeatmap
                data={ckptProgressionData}
                title="WER Heatmap — All Checkpoints"
              />
            )}

            {/* Checkpoint Line Chart */}
            {Object.keys(ckptProgressionData).length > 1 && (
              <CheckpointLineChart
                data={ckptProgressionData}
                languages={[...LANGUAGES]}
                title="WER Trend by Language Across Checkpoints"
                height={400}
              />
            )}

            {/* Sample Predictions */}

                  Sample Predictions — {selectedCheckpoint}

                <select
                  value={sampleLanguage}
                  onChange={(e) => {
                    setSampleLanguage(e.target.value);
                    setSamplePage(1);
                  }}
                  className="bg-[var(--color-card)] border border-[var(--color-border)] rounded px-2 py-1 text-[10px] text-[var(--color-foreground)]"
                >
                  <option value="all">All Languages</option>
                  {LANGUAGES.map((lang) => (
                    <option key={lang} value={lang}>
                      {lang.charAt(0).toUpperCase() + lang.slice(1)}
                    </option>
                  ))}
                </select>

              <SampleTable
                samples={samples}
                pagination={pagination}
                onPageChange={setSamplePage}
                loading={samplesLoading}
              />

            {/* Per-Language Metrics Table */}
            {metrics && (

                  Detailed Metrics — {selectedCheckpoint}

                      Language
                      Samples
                      WER
                      CER
                      Norm WER
                      Norm CER
                      Empty

                    {LANGUAGES.map((lang) => {
                      const lm = metrics[lang];
                      if (!lm || !lm.wer) return null;
                      return (

                          {lang}
                          {lm.n_samples}

                             40 ? "text-[var(--color-danger)]" : (lm.wer ?? 0) < 20 ? "text-[var(--color-success)]" : "text-[var(--color-foreground)]"}>
                              {(lm.wer ?? 0).toFixed(2)}%

                          {(lm.cer ?? 0).toFixed(2)}%
                          {(lm.wer_normalized ?? 0).toFixed(2)}%
                          {(lm.cer_normalized ?? 0).toFixed(2)}%
                          {lm.empty_hypotheses ?? 0}

                      );
                    })}

            )}

        )}

  );
}


<tool_use_error>File has not been read yet. Read it first before writing to it.</tool_use_error>

Error: File has not been read yet. Read it first before writing to it.


1→import Image from "next/image";
     2→
     3→export default function Home() {
     4→  return (
     5→

     6→
     7→        <Image
     8→          className="dark:invert"
     9→          src="/next.svg"
    10→          alt="Next.js logo"
    11→          width={100}
    12→          height={20}
    13→          priority
    14→        />
    15→

    16→

    17→            To get started, edit the page.tsx file.
    18→

    19→

    20→            Looking for a starting point or more instructions? Head over to{" "}
    21→
    25→              Templates
    26→            {" "}
    27→            or the{" "}
    28→
    32→              Learning
    33→            {" "}
    34→            center.
    35→

    36→

    37→

    38→
    44→            <Image
    45→              className="dark:invert"
    46→              src="/vercel.svg"
    47→              alt="Vercel logomark"
    48→              width={16}
    49→              height={16}
    50→            />
    51→            Deploy Now
    52→
    53→
    59→            Documentation
    60→
    61→

    62→
    63→

    64→  );
    65→}
    66→

import Image from "next/image";

export default function Home() {
  return (

        <Image
          className="dark:invert"
          src="/next.svg"
          alt="Next.js logo"
          width={100}
          height={20}
          priority
        />

            To get started, edit the page.tsx file.

            Looking for a starting point or more instructions? Head over to{" "}

              Templates
            {" "}
            or the{" "}

              Learning
            {" "}
            center.

            <Image
              className="dark:invert"
              src="/vercel.svg"
              alt="Vercel logomark"
              width={16}
              height={16}
            />
            Deploy Now

            Documentation

  );
}


"use client";

import { useState, useEffect, useCallback } from "react";
import Sidebar from "@/components/Sidebar";
import MetricCard from "@/components/MetricCard";
import LanguageBarChart from "@/components/LanguageBarChart";
import CheckpointLineChart from "@/components/CheckpointLineChart";
import CrossModelChart from "@/components/CrossModelChart";
import LanguageHeatmap from "@/components/LanguageHeatmap";
import SampleTable from "@/components/SampleTable";
import { ModelInfo, LANGUAGES, MODEL_REGISTRY } from "@/lib/types";

type ViewMode = "overview" | "wer-analysis";

interface LangMetrics {
  n_samples?: number;
  wer?: number;
  cer?: number;
  wer_normalized?: number;
  cer_normalized?: number;
  empty_hypotheses?: number;
}

interface OverallMetrics {
  n_samples?: number;
  wer?: number;
  cer?: number;
  wer_normalized?: number;
  cer_normalized?: number;
}

interface MetaInfo {
  checkpoint?: string;
  checkpoint_name?: string;
  model_id?: string;
  dataset?: string;
  inference_time_sec?: number;
  total_audio_sec?: number;
  rtf?: number;
  timestamp?: string;
}

interface MetricsData {
  [key: string]: LangMetrics | OverallMetrics | MetaInfo | { n_languages?: number; wer?: number; cer?: number; wer_normalized?: number; cer_normalized?: number };
  __overall__: OverallMetrics;
  __meta__: MetaInfo;
  __macro_avg__: { n_languages?: number; wer?: number; cer?: number; wer_normalized?: number; cer_normalized?: number };
}

interface SampleData {
  id: string;
  language: string;
  reference: string;
  hypothesis: string;
  detected_language?: string;
}

interface PaginationData {
  page: number;
  pageSize: number;
  total: number;
  totalPages: number;
}

export default function Dashboard() {
  const [models, setModels] = useState<ModelInfo[]>([]);
  const [selectedModel, setSelectedModel] = useState<string | null>(null);
  const [selectedCheckpoint, setSelectedCheckpoint] = useState<string | null>(null);
  const [activeView, setActiveView] = useState<ViewMode>("overview");
  const [metrics, setMetrics] = useState<MetricsData | null>(null);
  const [compareData, setCompareData] = useState<Record<string, Record<string, Record<string, number>>>>({});
  const [samples, setSamples] = useState<SampleData[]>([]);
  const [pagination, setPagination] = useState<PaginationData>({ page: 1, pageSize: 20, total: 0, totalPages: 0 });
  const [sampleLanguage, setSampleLanguage] = useState<string>("all");
  const [samplePage, setSamplePage] = useState(1);
  const [samplesLoading, setSamplesLoading] = useState(false);
  const [compareCheckpoints, setCompareCheckpoints] = useState<Record<string, string>>({});

  useEffect(() => {
    fetch("/api/models")
      .then((r) => r.json())
      .then((data: ModelInfo[]) => {
        setModels(data);
        const firstWithCkpts = data.find((m) => m.checkpoints.length > 0);
        if (firstWithCkpts) {
          setSelectedModel(firstWithCkpts.id);
          setSelectedCheckpoint(firstWithCkpts.checkpoints[firstWithCkpts.checkpoints.length - 1]);
        }
        const defaults: Record<string, string> = {};
        for (const m of data) {
          if (m.checkpoints.length > 0) {
            defaults[m.id] = m.checkpoints[m.checkpoints.length - 1];
          }
        }
        setCompareCheckpoints(defaults);
      });
  }, []);

  useEffect(() => {
    fetch("/api/compare")
      .then((r) => r.json())
      .then(setCompareData);
  }, []);

  useEffect(() => {
    if (!selectedModel || !selectedCheckpoint) return;
    fetch(`/api/metrics?model=${selectedModel}&checkpoint=${selectedCheckpoint}`)
      .then((r) => r.json())
      .then(setMetrics);
  }, [selectedModel, selectedCheckpoint]);

  const loadSamples = useCallback(() => {
    if (!selectedModel || !selectedCheckpoint) return;
    setSamplesLoading(true);
    const langParam = sampleLanguage === "all" ? "" : `&language=${sampleLanguage}`;
    fetch(`/api/samples?model=${selectedModel}&checkpoint=${selectedCheckpoint}&page=${samplePage}&pageSize=20${langParam}`)
      .then((r) => r.json())
      .then((data) => {
        setSamples(data.samples || []);
        setPagination(data.pagination || { page: 1, pageSize: 20, total: 0, totalPages: 0 });
        setSamplesLoading(false);
      })
      .catch(() => setSamplesLoading(false));
  }, [selectedModel, selectedCheckpoint, samplePage, sampleLanguage]);

  useEffect(() => {
    loadSamples();
  }, [loadSamples]);

  const languageData = metrics
    ? LANGUAGES.map((lang) => {
        const lm = metrics[lang] as LangMetrics | undefined;
        return {
          language: lang,
          wer: lm?.wer ?? 0,
          cer: lm?.cer ?? 0,
          wer_normalized: lm?.wer_normalized ?? 0,
        };
      })
    : [];

  const overall = metrics?.__overall__;
  const meta = metrics?.__meta__;

  const ckptProgressionData: Record<string, Record<string, number>> = {};
  if (selectedModel && compareData[selectedModel]) {
    for (const [ckpt, langData] of Object.entries(compareData[selectedModel])) {
      ckptProgressionData[ckpt] = langData;
    }
  }

  return (

      <Sidebar
        models={models}
        selectedModel={selectedModel}
        selectedCheckpoint={selectedCheckpoint}
        activeView={activeView}
        onSelectModel={setSelectedModel}
        onSelectCheckpoint={(ckpt) => {
          setSelectedCheckpoint(ckpt);
          setSamplePage(1);
        }}
        onViewChange={setActiveView}
      />

        {!selectedModel || !selectedCheckpoint ? (

Select a model and checkpoint to begin.

        ) : activeView === "overview" ? (

            {/* Header */}

                  {MODEL_REGISTRY[selectedModel] || selectedModel}

                  {selectedCheckpoint} · {(meta?.dataset as string) || "indic-asr-benchmark-6k"}

                  RTF: {typeof meta?.rtf === "number" ? meta.rtf.toFixed(4) : "—"} · Inference: {typeof meta?.inference_time_sec === "number" ? `${(meta.inference_time_sec / 60).toFixed(1)}m` : "—"}

                {meta?.timestamp && (

                    {new Date(meta.timestamp as string).toLocaleDateString()}

                )}

            {/* Metric Cards */}

              <MetricCard label="Overall WER" value={overall?.wer ?? 0} subtitle="Word Error Rate" />
              <MetricCard label="Overall CER" value={overall?.cer ?? 0} subtitle="Character Error Rate" />
              <MetricCard label="Normalized WER" value={overall?.wer_normalized ?? 0} subtitle="After text normalization" />
              <MetricCard label="Normalized CER" value={overall?.cer_normalized ?? 0} subtitle="After text normalization" />

            {/* Bar Charts */}

              <LanguageBarChart data={languageData} metric="wer" title="WER by Language" />
              <LanguageBarChart data={languageData} metric="cer" title="CER by Language" />

            {/* Checkpoint Progression */}
            {Object.keys(ckptProgressionData).length > 1 && (
              <CheckpointLineChart
                data={ckptProgressionData}
                languages={[...LANGUAGES]}
                title={`WER Progression Across Checkpoints — ${MODEL_REGISTRY[selectedModel]}`}
              />
            )}

            {/* Cross-Model Comparison */}
            {Object.keys(compareCheckpoints).length > 1 && (
              <>

                    Cross-Model Comparison

                  {models
                    .filter((m) => m.checkpoints.length > 0)
                    .map((m) => (
                      <select
                        key={m.id}
                        value={compareCheckpoints[m.id] || ""}
                        onChange={(e) =>
                          setCompareCheckpoints((prev) => ({ ...prev, [m.id]: e.target.value }))
                        }
                        className="bg-[var(--color-card)] border border-[var(--color-border)] rounded px-2 py-1 text-[10px] text-[var(--color-foreground)]"
                      >
                        {m.checkpoints.map((ckpt) => (
                          <option key={ckpt} value={ckpt}>
                            {MODEL_REGISTRY[m.id]?.split("-")[0]} — {ckpt}
                          </option>
                        ))}
                      </select>
                    ))}

                <CrossModelChart
                  data={compareData}
                  selectedCheckpoints={compareCheckpoints}
                  title="WER Comparison Across Models (by Language)"
                />
              </>
            )}

            {/* Normalized WER */}
            <LanguageBarChart data={languageData} metric="wer_normalized" title="Normalized WER by Language" />

        ) : (
          /* WER Analysis View */

                WER Analysis — {MODEL_REGISTRY[selectedModel] || selectedModel}

                Language-wise comparison across checkpoints

            {/* Heatmap */}
            {Object.keys(ckptProgressionData).length > 0 && (
              <LanguageHeatmap data={ckptProgressionData} title="WER Heatmap — All Checkpoints" />
            )}

            {/* Checkpoint Line Chart */}
            {Object.keys(ckptProgressionData).length > 1 && (
              <CheckpointLineChart
                data={ckptProgressionData}
                languages={[...LANGUAGES]}
                title="WER Trend by Language Across Checkpoints"
                height={400}
              />
            )}

            {/* Sample Predictions */}

                  Sample Predictions — {selectedCheckpoint}

                <select
                  value={sampleLanguage}
                  onChange={(e) => {
                    setSampleLanguage(e.target.value);
                    setSamplePage(1);
                  }}
                  className="bg-[var(--color-card)] border border-[var(--color-border)] rounded px-2 py-1 text-[10px] text-[var(--color-foreground)]"
                >
                  <option value="all">All Languages</option>
                  {LANGUAGES.map((lang) => (
                    <option key={lang} value={lang}>
                      {lang.charAt(0).toUpperCase() + lang.slice(1)}
                    </option>
                  ))}
                </select>

              <SampleTable
                samples={samples}
                pagination={pagination}
                onPageChange={setSamplePage}
                loading={samplesLoading}
              />

            {/* Detailed Metrics Table */}
            {metrics && (

                  Detailed Metrics — {selectedCheckpoint}

                      Language
                      Samples
                      WER
                      CER
                      Norm WER
                      Norm CER
                      Empty

                    {LANGUAGES.map((lang) => {
                      const lm = metrics[lang] as LangMetrics | undefined;
                      if (!lm || !lm.wer) return null;
                      return (

                          {lang}
                          {lm.n_samples}

                             40 ? "text-[var(--color-danger)]" : (lm.wer ?? 0) < 20 ? "text-[var(--color-success)]" : "text-[var(--color-foreground)]"}>
                              {(lm.wer ?? 0).toFixed(2)}%

                          {(lm.cer ?? 0).toFixed(2)}%
                          {(lm.wer_normalized ?? 0).toFixed(2)}%
                          {(lm.cer_normalized ?? 0).toFixed(2)}%
                          {lm.empty_hypotheses ?? 0}

                      );
                    })}

            )}

        )}

  );
}