feat: redesign UI to match lovable compact sidebar layout
- Replace green hacker theme with professional blue-toned design - Light theme default, manual toggle only (no system detection) - Compact w-60 sidebar with collapsible sections - New CSS tokens: sidebar, chart, candle, annotation colors - Tools displayed as compact grid buttons - Color swatches as inline bar - Chart top bar with keyboard shortcut hints - Inter + JetBrains Mono font pairing - All components updated for compact styling - Tailwind config extended with sidebar/chart tokens
This commit is contained in:
parent
2bde38d0bf
commit
4605283d2b
13 changed files with 976 additions and 740 deletions
|
|
@ -1,5 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { ChevronDown } from 'lucide-react';
|
||||
import type { PredictionState, ModelInfoResponse, PredictionSummary } from '@/types/predictions';
|
||||
|
||||
interface PredictionPanelProps {
|
||||
|
|
@ -27,6 +29,7 @@ export default function PredictionPanel({
|
|||
showOnlyDisagreements = false,
|
||||
onToggleShowOnlyDisagreements,
|
||||
}: PredictionPanelProps) {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
|
||||
const {
|
||||
visible,
|
||||
|
|
@ -38,166 +41,172 @@ export default function PredictionPanel({
|
|||
spans,
|
||||
} = predictionState;
|
||||
|
||||
if (!isModelOnline) {
|
||||
return (
|
||||
<div className="p-4 border-t border-border bg-card">
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<div className="w-2 h-2 rounded-full bg-red-500" />
|
||||
<h3 className="text-sm font-semibold text-foreground">Model Server Offline</h3>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Prediction service is unavailable. Annotation tools continue to work normally.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 border-t border-border bg-card">
|
||||
{/* Header with master toggle */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className={`w-2 h-2 rounded-full ${isModelOnline ? 'bg-green-500' : 'bg-red-500'}`} />
|
||||
<h3 className="text-sm font-semibold text-foreground">Predictions</h3>
|
||||
<div>
|
||||
{/* Collapsible header */}
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="w-full flex items-center justify-between py-1"
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div
|
||||
className="w-1.5 h-1.5 rounded-full"
|
||||
style={{ backgroundColor: isModelOnline ? '#22c55e' : '#ef4444' }}
|
||||
/>
|
||||
<span className="text-[10px] font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Predictions
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={onToggleVisibility}
|
||||
className={`px-3 py-1 text-xs rounded ${
|
||||
visible
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'bg-muted text-muted-foreground hover:bg-muted/80'
|
||||
}`}
|
||||
>
|
||||
{visible ? 'Hide' : 'Show'}
|
||||
</button>
|
||||
</div>
|
||||
<ChevronDown className={`w-3 h-3 text-muted-foreground transition-transform ${expanded ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
{/* Model Info */}
|
||||
{modelInfo && (
|
||||
<div className="mb-3 p-2 bg-muted/50 rounded text-xs">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Model:</span>
|
||||
<span className="font-mono text-foreground">{modelInfo.model_name}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Version:</span>
|
||||
<span className="font-mono text-foreground">{modelInfo.model_version || 'N/A'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Type:</span>
|
||||
<span className="text-foreground">{modelInfo.model_type}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{expanded && (
|
||||
<div className="mt-1 space-y-2">
|
||||
{!isModelOnline ? (
|
||||
<p className="text-[10px] text-muted-foreground">
|
||||
Prediction service unavailable.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{/* Model Info */}
|
||||
{modelInfo && (
|
||||
<div className="p-2 bg-secondary/30 rounded text-[10px] space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Model:</span>
|
||||
<span className="font-mono text-foreground">{modelInfo.model_name}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Version:</span>
|
||||
<span className="font-mono text-foreground">{modelInfo.model_version || 'N/A'}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Type:</span>
|
||||
<span className="text-foreground">{modelInfo.model_type}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-2 mb-3">
|
||||
<button
|
||||
onClick={onFetchPredictions}
|
||||
disabled={isLoading || !isModelOnline}
|
||||
className="flex-1 px-3 py-2 text-xs bg-primary text-primary-foreground rounded hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{isLoading ? 'Loading...' : 'Run on Visible'}
|
||||
</button>
|
||||
<button
|
||||
onClick={onFetchBatchPredictions}
|
||||
disabled={isLoading || !isModelOnline}
|
||||
className="flex-1 px-3 py-2 text-xs bg-secondary text-secondary-foreground rounded hover:bg-secondary/90 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Predict All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="mb-3 p-2 bg-destructive/10 border border-destructive/20 rounded text-xs text-destructive">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Confidence Slider */}
|
||||
<div className="mb-3">
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<label className="text-xs text-muted-foreground">Confidence Threshold</label>
|
||||
<span className="text-xs font-mono text-foreground">{(confidenceThreshold * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={confidenceThreshold * 100}
|
||||
onChange={(e) => onConfidenceChange(Number(e.target.value) / 100)}
|
||||
className="w-full h-1 bg-muted rounded-lg appearance-none cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Label Filter Checkboxes */}
|
||||
{modelInfo && (
|
||||
<div className="mb-3">
|
||||
<label className="text-xs text-muted-foreground mb-2 block">Filter by Label</label>
|
||||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||||
{modelInfo.labels.map((label) => {
|
||||
const metrics = modelInfo.per_class_metrics.find((m) => m.label === label);
|
||||
const isSelected = selectedLabels.has(label);
|
||||
|
||||
return (
|
||||
<label
|
||||
key={label}
|
||||
className="flex items-center gap-2 p-1 rounded hover:bg-muted/50 cursor-pointer"
|
||||
{/* Action Buttons */}
|
||||
<div className="flex gap-1">
|
||||
<button
|
||||
onClick={onFetchPredictions}
|
||||
disabled={isLoading}
|
||||
className="flex-1 px-2 py-1.5 text-[10px] bg-primary text-primary-foreground rounded hover:opacity-90 transition-opacity disabled:opacity-50"
|
||||
>
|
||||
{isLoading ? 'Loading...' : 'Run on Visible'}
|
||||
</button>
|
||||
<button
|
||||
onClick={onFetchBatchPredictions}
|
||||
disabled={isLoading}
|
||||
className="flex-1 px-2 py-1.5 text-[10px] bg-secondary text-secondary-foreground rounded hover:opacity-90 transition-opacity disabled:opacity-50"
|
||||
>
|
||||
Predict All
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<p className="text-[10px] text-destructive">{error}</p>
|
||||
)}
|
||||
|
||||
{/* Confidence Slider */}
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-1">
|
||||
<label className="text-[10px] text-muted-foreground">Confidence</label>
|
||||
<span className="text-[10px] font-mono text-foreground">{(confidenceThreshold * 100).toFixed(0)}%</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="100"
|
||||
value={confidenceThreshold * 100}
|
||||
onChange={(e) => onConfidenceChange(Number(e.target.value) / 100)}
|
||||
className="w-full h-1 bg-secondary rounded-lg appearance-none cursor-pointer accent-primary"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Show on chart toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="text-[10px] text-muted-foreground">Show on chart</label>
|
||||
<button
|
||||
onClick={onToggleVisibility}
|
||||
className={`px-2 py-0.5 text-[10px] rounded transition-colors ${
|
||||
visible
|
||||
? 'bg-primary/20 text-primary'
|
||||
: 'bg-secondary text-muted-foreground'
|
||||
}`}
|
||||
>
|
||||
{visible ? 'On' : 'Off'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Label Filter */}
|
||||
{modelInfo && modelInfo.labels.length > 0 && (
|
||||
<div>
|
||||
<label className="text-[10px] text-muted-foreground mb-1 block">Labels</label>
|
||||
<div className="space-y-0.5 max-h-24 overflow-y-auto scrollbar-thin">
|
||||
{modelInfo.labels.map((label) => {
|
||||
const metrics = modelInfo.per_class_metrics.find((m) => m.label === label);
|
||||
return (
|
||||
<label
|
||||
key={label}
|
||||
className="flex items-center gap-1.5 p-0.5 rounded hover:bg-secondary/50 cursor-pointer"
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedLabels.has(label)}
|
||||
onChange={() => onToggleLabelSelection(label)}
|
||||
className="w-3 h-3"
|
||||
/>
|
||||
<span className="text-[10px] text-foreground flex-1">{label}</span>
|
||||
{metrics && (
|
||||
<span className="text-[10px] text-muted-foreground font-mono">
|
||||
F1:{(metrics.f1_score * 100).toFixed(0)}%
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Disagreement Filter */}
|
||||
{predictionSummary && predictionSummary.disagreements.length > 0 && onToggleShowOnlyDisagreements && (
|
||||
<label className="flex items-center gap-1.5 p-1 bg-secondary/30 rounded cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => onToggleLabelSelection(label)}
|
||||
checked={showOnlyDisagreements}
|
||||
onChange={onToggleShowOnlyDisagreements}
|
||||
className="w-3 h-3"
|
||||
/>
|
||||
<span className="text-xs text-foreground flex-1">{label}</span>
|
||||
{metrics && (
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
F1: {(metrics.f1_score * 100).toFixed(0)}%
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[10px] text-foreground">Show only disagreements</span>
|
||||
</label>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
|
||||
{/* Disagreement Filter */}
|
||||
{predictionSummary && predictionSummary.disagreements.length > 0 && onToggleShowOnlyDisagreements && (
|
||||
<div className="mb-3">
|
||||
<label className="flex items-center gap-2 p-2 bg-muted/50 rounded cursor-pointer hover:bg-muted">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={showOnlyDisagreements}
|
||||
onChange={onToggleShowOnlyDisagreements}
|
||||
className="w-3 h-3"
|
||||
/>
|
||||
<span className="text-xs text-foreground">Show only disagreements</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Prediction Summary */}
|
||||
{visible && spans.length > 0 && predictionSummary && (
|
||||
<div className="p-2 bg-muted/30 rounded text-xs space-y-1">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Predictions:</span>
|
||||
<span className="text-foreground font-mono">{predictionSummary.total_predictions}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Human annotations:</span>
|
||||
<span className="text-foreground font-mono">{predictionSummary.total_human_annotations}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Agreements:</span>
|
||||
<span className="text-green-600 font-mono">{predictionSummary.agreements}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Disagreements:</span>
|
||||
<span className="text-orange-600 font-mono">{predictionSummary.disagreements.length}</span>
|
||||
</div>
|
||||
{/* Summary */}
|
||||
{visible && spans.length > 0 && predictionSummary ? (
|
||||
<div className="p-2 bg-secondary/30 rounded text-[10px] space-y-0.5">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Predictions:</span>
|
||||
<span className="font-mono text-foreground">{predictionSummary.total_predictions}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Agreements:</span>
|
||||
<span className="text-green-600 font-mono">{predictionSummary.agreements}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Disagreements:</span>
|
||||
<span className="text-orange-500 font-mono">{predictionSummary.disagreements.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-[10px] text-muted-foreground text-center py-2">
|
||||
No predictions loaded.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue