Fix observer reports: charts, filtering, project preview, dashboard stats
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m32s
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m32s
- Rewrite diversity metrics: horizontal bar charts for ocean issues and geographic distribution (replaces unreadable vertical/donut charts) - Rewrite juror score heatmap: expandable table with score distribution - Rewrite juror consistency: horizontal bar visual with juror names - Merge filtering tabs into single screening view with per-project AI reasoning and expandable rows - Add project preview dialog for juror performance table - Fix status breakdown for evaluation rounds (Fully/Partially/Not Reviewed) - Show active round name instead of count on observer dashboard - Move Global tab to last position, default to first round-specific tab - Add 4-card stats layout for evaluation with reviews/project ratio - Fix oceanIssue field (singular) and remove non-existent aiSummary Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import { ScatterChart } from '@tremor/react'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import { AlertTriangle } from 'lucide-react'
|
||||
import { scoreGradient } from './chart-theme'
|
||||
|
||||
interface JurorMetric {
|
||||
userId: string
|
||||
@@ -30,6 +30,24 @@ interface JurorConsistencyProps {
|
||||
}
|
||||
}
|
||||
|
||||
function ScoreDot({ score, maxScore = 10 }: { score: number; maxScore?: number }) {
|
||||
const pct = ((score / maxScore) * 100).toFixed(1)
|
||||
return (
|
||||
<div className="flex items-center gap-2 w-full min-w-[120px]">
|
||||
<div className="flex-1 h-2.5 rounded-full bg-muted overflow-hidden">
|
||||
<div
|
||||
className="h-full rounded-full transition-all duration-300"
|
||||
style={{
|
||||
width: `${pct}%`,
|
||||
backgroundColor: scoreGradient(score),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xs tabular-nums font-medium w-8 text-right">{score.toFixed(1)}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function JurorConsistencyChart({ data }: JurorConsistencyProps) {
|
||||
if (!data?.jurors?.length) {
|
||||
return (
|
||||
@@ -42,27 +60,19 @@ export function JurorConsistencyChart({ data }: JurorConsistencyProps) {
|
||||
}
|
||||
|
||||
const outlierCount = data.jurors.filter((j) => j.isOutlier).length
|
||||
|
||||
const scatterData = data.jurors.map((j) => ({
|
||||
'Average Score': parseFloat(j.averageScore.toFixed(2)),
|
||||
'Std Deviation': parseFloat(j.stddev.toFixed(2)),
|
||||
category: j.isOutlier ? 'Outlier' : 'Normal',
|
||||
name: j.name,
|
||||
evaluations: j.evaluationCount,
|
||||
size: Math.max(8, Math.min(20, j.evaluationCount * 2)),
|
||||
}))
|
||||
const sorted = [...data.jurors].sort((a, b) => b.averageScore - a.averageScore)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Scatter: Average Score vs Standard Deviation */}
|
||||
{/* Juror Scoring Patterns — bar-based visual instead of scatter */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>Juror Scoring Patterns</span>
|
||||
<span className="text-sm font-normal text-muted-foreground">
|
||||
<CardTitle className="flex items-center justify-between flex-wrap gap-2">
|
||||
<span className="text-base">Juror Scoring Patterns</span>
|
||||
<span className="text-sm font-normal text-muted-foreground flex items-center gap-2">
|
||||
Overall Avg: {data.overallAverage.toFixed(2)}
|
||||
{outlierCount > 0 && (
|
||||
<Badge variant="destructive" className="ml-2">
|
||||
<Badge variant="destructive">
|
||||
{outlierCount} outlier{outlierCount > 1 ? 's' : ''}
|
||||
</Badge>
|
||||
)}
|
||||
@@ -70,18 +80,31 @@ export function JurorConsistencyChart({ data }: JurorConsistencyProps) {
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ScatterChart
|
||||
data={scatterData}
|
||||
x="Average Score"
|
||||
y="Std Deviation"
|
||||
category="category"
|
||||
size="size"
|
||||
colors={['blue', 'rose']}
|
||||
className="h-[400px]"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground mt-2 text-center">
|
||||
Dot size represents number of evaluations. Red dots indicate outlier
|
||||
jurors (2+ points from mean).
|
||||
<div className="space-y-2">
|
||||
{sorted.map((juror) => (
|
||||
<div
|
||||
key={juror.userId}
|
||||
className={`flex items-center gap-3 rounded-md px-3 py-2 ${juror.isOutlier ? 'bg-destructive/5 border border-destructive/20' : 'hover:bg-muted/50'}`}
|
||||
>
|
||||
<div className="w-36 shrink-0 truncate">
|
||||
<span className="text-sm font-medium">{juror.name}</span>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<ScoreDot score={juror.averageScore} />
|
||||
</div>
|
||||
<div className="hidden sm:flex items-center gap-3 text-xs text-muted-foreground shrink-0">
|
||||
<span className="tabular-nums">σ {juror.stddev.toFixed(1)}</span>
|
||||
<span className="tabular-nums">{juror.evaluationCount} eval{juror.evaluationCount !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
{juror.isOutlier && (
|
||||
<AlertTriangle className="h-3.5 w-3.5 text-destructive shrink-0" />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* Overall average line */}
|
||||
<p className="text-xs text-muted-foreground mt-4 text-center">
|
||||
Bars show average score per juror. σ = standard deviation. Outliers deviate 2+ points from the overall mean.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -89,57 +112,92 @@ export function JurorConsistencyChart({ data }: JurorConsistencyProps) {
|
||||
{/* Juror details table */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Juror Consistency Details</CardTitle>
|
||||
<CardTitle className="text-base">Juror Consistency Details</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Juror</TableHead>
|
||||
<TableHead className="text-right">Evaluations</TableHead>
|
||||
<TableHead className="text-right">Avg Score</TableHead>
|
||||
<TableHead className="text-right">Std Dev</TableHead>
|
||||
<TableHead className="text-right">
|
||||
Deviation from Mean
|
||||
</TableHead>
|
||||
<TableHead className="text-center">Status</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.jurors.map((juror) => (
|
||||
<TableRow
|
||||
key={juror.userId}
|
||||
className={juror.isOutlier ? 'bg-destructive/5' : ''}
|
||||
>
|
||||
<TableCell>
|
||||
<p className="font-medium">{juror.name}</p>
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{juror.evaluationCount}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{juror.averageScore.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{juror.stddev.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{juror.deviationFromOverall.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{juror.isOutlier ? (
|
||||
<Badge variant="destructive" className="gap-1">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
Outlier
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary">Normal</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
{/* Desktop table */}
|
||||
<div className="hidden md:block">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Juror</TableHead>
|
||||
<TableHead className="text-right">Evaluations</TableHead>
|
||||
<TableHead className="text-right">Avg Score</TableHead>
|
||||
<TableHead className="text-right">Std Dev</TableHead>
|
||||
<TableHead className="text-right">Deviation</TableHead>
|
||||
<TableHead className="text-center">Status</TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{sorted.map((juror) => (
|
||||
<TableRow
|
||||
key={juror.userId}
|
||||
className={juror.isOutlier ? 'bg-destructive/5' : ''}
|
||||
>
|
||||
<TableCell className="font-medium">{juror.name}</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{juror.evaluationCount}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{juror.averageScore.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{juror.stddev.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">
|
||||
{juror.deviationFromOverall >= 0 ? '+' : ''}{juror.deviationFromOverall.toFixed(2)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
{juror.isOutlier ? (
|
||||
<Badge variant="destructive" className="gap-1">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
Outlier
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary">Normal</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{/* Mobile card stack */}
|
||||
<div className="space-y-2 md:hidden">
|
||||
{sorted.map((juror) => (
|
||||
<div
|
||||
key={juror.userId}
|
||||
className={`rounded-md border p-3 space-y-1 ${juror.isOutlier ? 'bg-destructive/5 border-destructive/20' : ''}`}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium">{juror.name}</span>
|
||||
{juror.isOutlier ? (
|
||||
<Badge variant="destructive" className="gap-1 text-[10px]">
|
||||
<AlertTriangle className="h-3 w-3" />
|
||||
Outlier
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary" className="text-[10px]">Normal</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-2 text-xs">
|
||||
<div>
|
||||
<p className="text-muted-foreground">Avg Score</p>
|
||||
<p className="font-medium tabular-nums">{juror.averageScore.toFixed(2)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Std Dev</p>
|
||||
<p className="font-medium tabular-nums">{juror.stddev.toFixed(2)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground">Evals</p>
|
||||
<p className="font-medium tabular-nums">{juror.evaluationCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user