feat: Enhance Project Overview and Review Session pages with improved UI and functionality

- Updated ProjectOverviewPage to enhance the guidelines section with better spacing, larger icons, and improved button styles.
- Refactored ReviewSessionPage to unify filter controls, improve selection actions, and enhance the findings list with a new layout.
- Added dimension tabs and severity filters to the SessionsPage for better navigation and filtering.
- Improved SessionDetailPage to utilize a mapping for status labels, enhancing internationalization support.
- Refactored TaskListTab to remove unused priority configuration code.
- Updated store types to better reflect session metadata structure.
- Added a temporary JSON file for future use.
This commit is contained in:
catlog22
2026-02-03 20:58:03 +08:00
parent 37ba849e75
commit a8385e2ea5
18 changed files with 1621 additions and 675 deletions

View File

@@ -5,13 +5,13 @@
import { memo, useMemo, useState, useEffect } from 'react';
import { useIntl } from 'react-intl';
import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts';
import { Card } from '@/components/ui/Card';
import { Progress } from '@/components/ui/Progress';
import { Button } from '@/components/ui/Button';
import { Sparkline } from '@/components/charts/Sparkline';
import { useWorkflowStatusCounts, generateMockWorkflowStatusCounts } from '@/hooks/useWorkflowStatusCounts';
import { useDashboardStats } from '@/hooks/useDashboardStats';
import { useCoordinatorStore } from '@/stores/coordinatorStore';
import { useProjectOverview } from '@/hooks/useProjectOverview';
import { cn } from '@/lib/utils';
import {
@@ -21,11 +21,6 @@ import {
CheckCircle2,
XCircle,
Activity,
Play,
Pause,
Square,
Loader2,
AlertCircle,
ChevronLeft,
ChevronRight,
ChevronDown,
@@ -40,7 +35,8 @@ import {
FileCode,
Bug,
Sparkles,
BookOpen,
BarChart3,
PieChart as PieChartIcon,
} from 'lucide-react';
export interface WorkflowTaskWidgetProps {
@@ -175,19 +171,19 @@ function MiniStatCard({ icon: Icon, title, value, variant, sparklineData }: Mini
const styles = variantStyles[variant] || variantStyles.default;
return (
<div className={cn('rounded-lg border p-2 transition-all hover:shadow-sm', styles.card)}>
<div className="flex items-start justify-between gap-1">
<div className={cn('rounded-lg border p-3 transition-all hover:shadow-sm h-full flex flex-col', styles.card)}>
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<p className="text-[10px] font-medium text-muted-foreground truncate">{title}</p>
<p className="text-lg font-semibold text-card-foreground mt-0.5">{value.toLocaleString()}</p>
<p className="text-xs font-medium text-muted-foreground truncate">{title}</p>
<p className="text-xl font-bold text-card-foreground mt-1">{value.toLocaleString()}</p>
</div>
<div className={cn('flex h-7 w-7 items-center justify-center rounded-md shrink-0', styles.icon)}>
<Icon className="h-3.5 w-3.5" />
<Icon className="h-4 w-4" />
</div>
</div>
{sparklineData && sparklineData.length > 0 && (
<div className="mt-1 -mx-1">
<Sparkline data={sparklineData} height={24} strokeWidth={1.5} />
<div className="mt-auto pt-2 -mx-1">
<Sparkline data={sparklineData} height={28} strokeWidth={1.5} />
</div>
)}
</div>
@@ -209,28 +205,12 @@ function generateSparklineData(currentValue: number, variance = 0.3): number[] {
return data;
}
// Orchestrator status icons and colors
const orchestratorStatusConfig: Record<string, { icon: typeof Play; color: string; bg: string }> = {
idle: { icon: Square, color: 'text-muted-foreground', bg: 'bg-muted' },
initializing: { icon: Loader2, color: 'text-info', bg: 'bg-info/20' },
running: { icon: Play, color: 'text-success', bg: 'bg-success/20' },
paused: { icon: Pause, color: 'text-warning', bg: 'bg-warning/20' },
completed: { icon: CheckCircle2, color: 'text-success', bg: 'bg-success/20' },
failed: { icon: XCircle, color: 'text-destructive', bg: 'bg-destructive/20' },
cancelled: { icon: AlertCircle, color: 'text-muted-foreground', bg: 'bg-muted' },
};
function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
const { formatMessage } = useIntl();
const { data, isLoading } = useWorkflowStatusCounts();
const { stats, isLoading: statsLoading } = useDashboardStats({ refetchInterval: 60000 });
const { projectOverview, isLoading: projectLoading } = useProjectOverview();
// Get coordinator state
const coordinatorState = useCoordinatorStore();
const orchestratorConfig = orchestratorStatusConfig[coordinatorState.status] || orchestratorStatusConfig.idle;
const OrchestratorIcon = orchestratorConfig.icon;
const chartData = data || generateMockWorkflowStatusCounts();
const total = chartData.reduce((sum, item) => sum + item.count, 0);
@@ -244,11 +224,6 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
todayActivity: generateSparklineData(stats?.todayActivity ?? 0, 0.6),
}), [stats]);
// Calculate orchestrator progress
const orchestratorProgress = coordinatorState.commandChain.length > 0
? Math.round((coordinatorState.commandChain.filter(n => n.status === 'completed').length / coordinatorState.commandChain.length) * 100)
: 0;
// Project info expanded state
const [projectExpanded, setProjectExpanded] = useState(false);
@@ -284,79 +259,79 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
) : (
<>
{/* Collapsed Header */}
<div className="px-4 py-3 flex items-center gap-6 flex-wrap">
<div className="px-5 py-4 flex items-center gap-6 flex-wrap">
{/* Project Name & Icon */}
<div className="flex items-center gap-2.5 min-w-0">
<div className="p-1.5 rounded-md bg-primary/10">
<Code2 className="h-4 w-4 text-primary" />
<div className="flex items-center gap-3 min-w-0">
<div className="p-2 rounded-md bg-primary/10">
<Code2 className="h-5 w-5 text-primary" />
</div>
<div className="min-w-0">
<h2 className="text-sm font-semibold text-foreground truncate">
<h2 className="text-base font-semibold text-foreground truncate">
{projectOverview?.projectName || 'Claude Code Workflow'}
</h2>
<p className="text-[10px] text-muted-foreground truncate max-w-[280px]">
<p className="text-xs text-muted-foreground truncate max-w-[300px]">
{projectOverview?.description || 'AI-powered workflow management system'}
</p>
</div>
</div>
{/* Divider */}
<div className="h-8 w-px bg-border hidden md:block" />
<div className="h-10 w-px bg-border hidden md:block" />
{/* Tech Stack Badges */}
<div className="flex items-center gap-2 text-[10px]">
<span className="flex items-center gap-1 px-2 py-1 rounded-md bg-blue-500/10 text-blue-600 font-medium">
<Code2 className="h-3 w-3" />
<div className="flex items-center gap-2.5 text-xs">
<span className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md bg-blue-500/10 text-blue-600 font-medium">
<Code2 className="h-3.5 w-3.5" />
{projectOverview?.technologyStack?.languages?.[0]?.name || 'TypeScript'}
</span>
<span className="flex items-center gap-1 px-2 py-1 rounded-md bg-green-500/10 text-green-600 font-medium">
<Server className="h-3 w-3" />
<span className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md bg-green-500/10 text-green-600 font-medium">
<Server className="h-3.5 w-3.5" />
{projectOverview?.technologyStack?.frameworks?.[0] || 'Node.js'}
</span>
<span className="flex items-center gap-1 px-2 py-1 rounded-md bg-violet-500/10 text-violet-600 font-medium">
<Layers className="h-3 w-3" />
<span className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md bg-violet-500/10 text-violet-600 font-medium">
<Layers className="h-3.5 w-3.5" />
{projectOverview?.architecture?.style || 'Modular Monolith'}
</span>
{projectOverview?.technologyStack?.buildTools?.[0] && (
<span className="flex items-center gap-1 px-2 py-1 rounded-md bg-orange-500/10 text-orange-600 font-medium">
<Wrench className="h-3 w-3" />
{projectOverview.technologyStack.buildTools[0]}
{projectOverview?.technologyStack?.build_tools?.[0] && (
<span className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-md bg-orange-500/10 text-orange-600 font-medium">
<Wrench className="h-3.5 w-3.5" />
{projectOverview.technologyStack.build_tools[0]}
</span>
)}
</div>
{/* Divider */}
<div className="h-8 w-px bg-border hidden lg:block" />
<div className="h-10 w-px bg-border hidden lg:block" />
{/* Quick Stats */}
<div className="flex items-center gap-4 text-[10px]">
<div className="flex items-center gap-1.5 text-emerald-600">
<Sparkles className="h-3 w-3" />
<div className="flex items-center gap-5 text-xs">
<div className="flex items-center gap-2 text-emerald-600">
<Sparkles className="h-3.5 w-3.5" />
<span className="font-semibold">{projectOverview?.developmentIndex?.feature?.length || 0}</span>
<span className="text-muted-foreground">{formatMessage({ id: 'projectOverview.devIndex.category.features' })}</span>
</div>
<div className="flex items-center gap-1.5 text-amber-600">
<Bug className="h-3 w-3" />
<div className="flex items-center gap-2 text-amber-600">
<Bug className="h-3.5 w-3.5" />
<span className="font-semibold">{projectOverview?.developmentIndex?.bugfix?.length || 0}</span>
<span className="text-muted-foreground">{formatMessage({ id: 'projectOverview.devIndex.category.bugfixes' })}</span>
</div>
<div className="flex items-center gap-1.5 text-blue-600">
<FileCode className="h-3 w-3" />
<div className="flex items-center gap-2 text-blue-600">
<FileCode className="h-3.5 w-3.5" />
<span className="font-semibold">{projectOverview?.developmentIndex?.enhancement?.length || 0}</span>
<span className="text-muted-foreground">{formatMessage({ id: 'projectOverview.devIndex.category.enhancements' })}</span>
</div>
</div>
{/* Date + Expand Button */}
<div className="flex items-center gap-3 text-[10px] text-muted-foreground ml-auto">
<span className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-muted/50">
<Calendar className="h-3 w-3" />
<div className="flex items-center gap-3 text-xs text-muted-foreground ml-auto">
<span className="flex items-center gap-2 px-2.5 py-1.5 rounded-md bg-muted/50">
<Calendar className="h-3.5 w-3.5" />
{projectOverview?.initializedAt ? new Date(projectOverview.initializedAt).toLocaleDateString() : new Date().toLocaleDateString()}
</span>
<Button
variant="ghost"
size="sm"
className="h-6 w-6 p-0 hover:bg-muted"
className="h-7 w-7 p-0 hover:bg-muted"
onClick={() => setProjectExpanded(!projectExpanded)}
>
{projectExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
@@ -366,88 +341,102 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
{/* Expanded Details */}
{projectExpanded && (
<div className="px-3 pb-2 grid grid-cols-4 gap-3 border-t border-border/50 pt-2">
<div className="px-5 pb-4 grid grid-cols-4 gap-6 border-t border-border/50 pt-4">
{/* Architecture */}
<div className="space-y-1">
<h4 className="text-[10px] font-semibold text-muted-foreground flex items-center gap-1">
<Layers className="h-3 w-3" />
<div className="space-y-2">
<h4 className="text-xs font-semibold text-foreground flex items-center gap-1.5">
<Layers className="h-4 w-4 text-primary" />
{formatMessage({ id: 'projectOverview.architecture.title' })}
</h4>
<div className="space-y-0.5">
<p className="text-[10px] text-foreground">{projectOverview?.architecture?.style || 'Modular Monolith'}</p>
<div className="flex flex-wrap gap-1">
{projectOverview?.architecture?.layers?.slice(0, 3).map((layer, i) => (
<span key={i} className="text-[9px] px-1 py-0.5 rounded bg-muted text-muted-foreground">{layer}</span>
<div className="space-y-2">
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-foreground">{projectOverview?.architecture?.style || 'Modular Monolith'}</span>
</div>
<p className="text-xs text-muted-foreground">
{formatMessage({ id: 'projectOverview.architecture.layers' })}:
</p>
<div className="flex flex-wrap gap-1.5">
{(projectOverview?.architecture?.layers || ['CLI Tools', 'Core Services', 'Dashboard UI', 'Data Layer']).slice(0, 5).map((layer, i) => (
<span key={i} className="text-xs px-2 py-1 rounded-md bg-muted text-muted-foreground font-medium">{layer}</span>
))}
</div>
</div>
</div>
{/* Key Components */}
<div className="space-y-1">
<h4 className="text-[10px] font-semibold text-muted-foreground flex items-center gap-1">
<Wrench className="h-3 w-3" />
<div className="space-y-2">
<h4 className="text-xs font-semibold text-foreground flex items-center gap-1.5">
<Wrench className="h-4 w-4 text-orange-500" />
{formatMessage({ id: 'projectOverview.components.title' })}
</h4>
<div className="space-y-0.5">
{projectOverview?.keyComponents?.slice(0, 3).map((comp, i) => (
<p key={i} className="text-[9px] text-foreground truncate">{comp.name}</p>
)) || (
<>
<p className="text-[9px] text-foreground">Session Manager</p>
<p className="text-[9px] text-foreground">Dashboard Generator</p>
<p className="text-[9px] text-foreground">Data Aggregator</p>
</>
)}
<div className="space-y-1.5">
{(projectOverview?.keyComponents || [
{ name: 'Session Manager', description: 'Workflow session lifecycle' },
{ name: 'Dashboard Generator', description: 'Dynamic widget rendering' },
{ name: 'Data Aggregator', description: 'Stats and metrics collection' },
{ name: 'Task Scheduler', description: 'Async task orchestration' },
]).slice(0, 4).map((comp, i) => (
<div key={i} className="flex items-start gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-orange-500 mt-1.5 shrink-0" />
<div className="min-w-0">
<p className="text-xs font-medium text-foreground truncate">{comp.name}</p>
{comp.description && (
<p className="text-[11px] text-muted-foreground truncate">{comp.description}</p>
)}
</div>
</div>
))}
</div>
</div>
{/* Development History */}
<div className="space-y-1">
<h4 className="text-[10px] font-semibold text-muted-foreground flex items-center gap-1">
<FileCode className="h-3 w-3" />
<div className="space-y-2">
<h4 className="text-xs font-semibold text-foreground flex items-center gap-1.5">
<FileCode className="h-4 w-4 text-blue-500" />
{formatMessage({ id: 'projectOverview.devIndex.title' })}
</h4>
<div className="flex flex-wrap gap-1.5">
<span className="flex items-center gap-0.5 text-[9px] text-emerald-600">
<Sparkles className="h-2.5 w-2.5" />
{projectOverview?.developmentIndex?.feature?.length || 0}
</span>
<span className="flex items-center gap-0.5 text-[9px] text-blue-600">
<FileCode className="h-2.5 w-2.5" />
{projectOverview?.developmentIndex?.enhancement?.length || 0}
</span>
<span className="flex items-center gap-0.5 text-[9px] text-amber-600">
<Bug className="h-2.5 w-2.5" />
{projectOverview?.developmentIndex?.bugfix?.length || 0}
</span>
<span className="flex items-center gap-0.5 text-[9px] text-violet-600">
<Wrench className="h-2.5 w-2.5" />
{projectOverview?.developmentIndex?.refactor?.length || 0}
</span>
<span className="flex items-center gap-0.5 text-[9px] text-slate-600">
<BookOpen className="h-2.5 w-2.5" />
{projectOverview?.developmentIndex?.docs?.length || 0}
</span>
<div className="grid grid-cols-2 gap-2">
<div className="flex items-center gap-2 px-2 py-1.5 rounded-md bg-emerald-500/10">
<Sparkles className="h-3.5 w-3.5 text-emerald-600" />
<div>
<p className="text-sm font-semibold text-emerald-600">{projectOverview?.developmentIndex?.feature?.length || 10}</p>
<p className="text-[10px] text-emerald-600/80">{formatMessage({ id: 'projectOverview.devIndex.category.features' })}</p>
</div>
</div>
<div className="flex items-center gap-2 px-2 py-1.5 rounded-md bg-blue-500/10">
<FileCode className="h-3.5 w-3.5 text-blue-600" />
<div>
<p className="text-sm font-semibold text-blue-600">{projectOverview?.developmentIndex?.enhancement?.length || 5}</p>
<p className="text-[10px] text-blue-600/80">{formatMessage({ id: 'projectOverview.devIndex.category.enhancements' })}</p>
</div>
</div>
<div className="flex items-center gap-2 px-2 py-1.5 rounded-md bg-amber-500/10">
<Bug className="h-3.5 w-3.5 text-amber-600" />
<div>
<p className="text-sm font-semibold text-amber-600">{projectOverview?.developmentIndex?.bugfix?.length || 3}</p>
<p className="text-[10px] text-amber-600/80">{formatMessage({ id: 'projectOverview.devIndex.category.bugfixes' })}</p>
</div>
</div>
<div className="flex items-center gap-2 px-2 py-1.5 rounded-md bg-violet-500/10">
<Wrench className="h-3.5 w-3.5 text-violet-600" />
<div>
<p className="text-sm font-semibold text-violet-600">{projectOverview?.developmentIndex?.refactor?.length || 2}</p>
<p className="text-[10px] text-violet-600/80">{formatMessage({ id: 'projectOverview.devIndex.category.refactorings' })}</p>
</div>
</div>
</div>
</div>
{/* Design Patterns */}
<div className="space-y-1">
<h4 className="text-[10px] font-semibold text-muted-foreground flex items-center gap-1">
<GitBranch className="h-3 w-3" />
<div className="space-y-2">
<h4 className="text-xs font-semibold text-foreground flex items-center gap-1.5">
<GitBranch className="h-4 w-4 text-violet-500" />
{formatMessage({ id: 'projectOverview.architecture.patterns' })}
</h4>
<div className="flex flex-wrap gap-1">
{projectOverview?.architecture?.patterns?.slice(0, 4).map((pattern, i) => (
<span key={i} className="text-[9px] px-1 py-0.5 rounded bg-primary/10 text-primary">{pattern}</span>
)) || (
<>
<span className="text-[9px] px-1 py-0.5 rounded bg-primary/10 text-primary">Factory</span>
<span className="text-[9px] px-1 py-0.5 rounded bg-primary/10 text-primary">Strategy</span>
<span className="text-[9px] px-1 py-0.5 rounded bg-primary/10 text-primary">Observer</span>
</>
)}
<div className="flex flex-wrap gap-1.5">
{(projectOverview?.architecture?.patterns || ['Factory', 'Strategy', 'Observer', 'Singleton', 'Decorator']).slice(0, 6).map((pattern, i) => (
<span key={i} className="text-xs px-2.5 py-1 rounded-md bg-primary/10 text-primary font-medium">{pattern}</span>
))}
</div>
</div>
</div>
@@ -457,21 +446,22 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
</Card>
{/* Main content Card: Stats | Workflow+Orchestrator | Task Details */}
<Card className="h-[320px] flex shrink-0 overflow-hidden">
<Card className="h-[400px] flex shrink-0 overflow-hidden">
{/* Compact Stats Section with Sparklines */}
<div className="w-[28%] p-2.5 flex flex-col border-r border-border">
<h3 className="text-xs font-semibold text-foreground mb-2 px-0.5">
<div className="w-[28%] p-3 flex flex-col border-r border-border">
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-1.5">
<BarChart3 className="h-4 w-4" />
{formatMessage({ id: 'home.sections.statistics' })}
</h3>
{statsLoading ? (
<div className="grid grid-cols-2 gap-1.5 flex-1">
<div className="grid grid-cols-2 grid-rows-3 gap-2.5 flex-1">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="h-14 bg-muted rounded animate-pulse" />
<div key={i} className="bg-muted rounded animate-pulse" />
))}
</div>
) : (
<div className="grid grid-cols-2 gap-1.5 flex-1 content-start overflow-auto">
<div className="grid grid-cols-2 grid-rows-3 gap-2.5 flex-1">
<MiniStatCard
icon={FolderKanban}
title={formatMessage({ id: 'home.stats.activeSessions' })}
@@ -518,134 +508,117 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
)}
</div>
{/* Workflow Status + Orchestrator Status Section */}
<div className="w-[26%] p-3 flex flex-col border-r border-border overflow-auto">
{/* Workflow Status */}
<h3 className="text-xs font-semibold text-foreground mb-2">
{/* Workflow Status Section - Pie Chart */}
<div className="w-[22%] p-3 flex flex-col border-r border-border">
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-1.5">
<PieChartIcon className="h-4 w-4" />
{formatMessage({ id: 'home.widgets.workflowStatus' })}
</h3>
{isLoading ? (
<div className="space-y-2">
{[1, 2, 3].map((i) => (
<div key={i} className="h-3 bg-muted rounded animate-pulse" />
))}
<div className="flex-1 flex items-center justify-center">
<div className="w-24 h-24 rounded-full bg-muted animate-pulse" />
</div>
) : (
<div className="space-y-2">
{chartData.map((item) => {
const percentage = total > 0 ? Math.round((item.count / total) * 100) : 0;
const colors = statusColors[item.status] || statusColors.completed;
return (
<div key={item.status} className="space-y-0.5">
<div className="flex items-center justify-between">
<div className="flex items-center gap-1">
<div className={cn('w-1.5 h-1.5 rounded-full', colors.dot)} />
<span className="text-[11px] text-foreground">
{formatMessage({ id: statusLabelKeys[item.status] })}
</span>
<span className="text-[11px] text-muted-foreground">
{item.count}
</span>
</div>
<span className={cn('text-[11px] font-medium', colors.text)}>
<div className="flex-1 flex flex-col">
{/* Mini Donut Chart */}
<div className="flex-1 min-h-0">
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={chartData}
cx="50%"
cy="50%"
innerRadius="55%"
outerRadius="85%"
paddingAngle={2}
dataKey="count"
>
{chartData.map((item) => {
const colors = statusColors[item.status] || statusColors.completed;
const fillColor = colors.dot.replace('bg-', '');
const colorMap: Record<string, string> = {
'emerald-500': '#10b981',
'amber-500': '#f59e0b',
'violet-500': '#8b5cf6',
'slate-400': '#94a3b8',
'slate-300': '#cbd5e1',
};
return (
<Cell key={item.status} fill={colorMap[fillColor] || '#94a3b8'} />
);
})}
</Pie>
</PieChart>
</ResponsiveContainer>
</div>
{/* Compact Legend */}
<div className="grid grid-cols-2 gap-x-3 gap-y-1.5 mt-3">
{chartData.map((item) => {
const colors = statusColors[item.status] || statusColors.completed;
const percentage = total > 0 ? Math.round((item.count / total) * 100) : 0;
return (
<div key={item.status} className="flex items-center gap-1.5 min-w-0">
<div className={cn('w-2.5 h-2.5 rounded-full shrink-0', colors.dot)} />
<span className="text-xs text-muted-foreground truncate">
{formatMessage({ id: statusLabelKeys[item.status] })}
</span>
<span className="text-xs font-medium text-foreground ml-auto">
{percentage}%
</span>
</div>
<Progress
value={percentage}
className="h-1 bg-muted"
indicatorClassName={colors.bg}
/>
</div>
);
})}
);
})}
</div>
</div>
)}
{/* Orchestrator Status Section */}
<div className="mt-3 pt-3 border-t border-border">
<h3 className="text-xs font-semibold text-foreground mb-2">
{formatMessage({ id: 'navigation.main.orchestrator' })}
</h3>
<div className={cn('rounded-lg p-2', orchestratorConfig.bg)}>
<div className="flex items-center gap-2">
<OrchestratorIcon className={cn('h-4 w-4', orchestratorConfig.color, coordinatorState.status === 'running' && 'animate-pulse')} />
<div className="flex-1 min-w-0">
<p className={cn('text-[11px] font-medium', orchestratorConfig.color)}>
{formatMessage({ id: `common.status.${coordinatorState.status}` })}
</p>
{coordinatorState.currentExecutionId && (
<p className="text-[10px] text-muted-foreground truncate">
{coordinatorState.pipelineDetails?.nodes[0]?.name || coordinatorState.currentExecutionId}
</p>
)}
</div>
</div>
{coordinatorState.status !== 'idle' && coordinatorState.commandChain.length > 0 && (
<div className="mt-2 space-y-1">
<div className="flex items-center justify-between text-[10px]">
<span className="text-muted-foreground">
{formatMessage({ id: 'common.labels.progress' })}
</span>
<span className="font-medium">{orchestratorProgress}%</span>
</div>
<Progress value={orchestratorProgress} className="h-1 bg-muted/50" />
<p className="text-[10px] text-muted-foreground">
{coordinatorState.commandChain.filter(n => n.status === 'completed').length}/{coordinatorState.commandChain.length} {formatMessage({ id: 'coordinator.steps' })}
</p>
</div>
)}
</div>
</div>
</div>
{/* Task Details Section: Session Carousel with Task List */}
<div className="w-[46%] p-3 flex flex-col">
<div className="flex-1 p-4 flex flex-col">
{/* Header with navigation */}
<div className="flex items-center justify-between mb-2">
<h3 className="text-xs font-semibold text-foreground flex items-center gap-1">
<ListChecks className="h-3.5 w-3.5" />
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-foreground flex items-center gap-1.5">
<ListChecks className="h-4 w-4" />
{formatMessage({ id: 'home.sections.taskDetails' })}
</h3>
<div className="flex items-center gap-1">
<Button variant="ghost" size="sm" className="h-5 w-5 p-0" onClick={handlePrevSession}>
<ChevronLeft className="h-3 w-3" />
<div className="flex items-center gap-1.5">
<Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={handlePrevSession}>
<ChevronLeft className="h-4 w-4" />
</Button>
<span className="text-[10px] text-muted-foreground min-w-[40px] text-center">
<span className="text-xs text-muted-foreground min-w-[45px] text-center">
{currentSessionIndex + 1} / {MOCK_SESSIONS.length}
</span>
<Button variant="ghost" size="sm" className="h-5 w-5 p-0" onClick={handleNextSession}>
<ChevronRight className="h-3 w-3" />
<Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={handleNextSession}>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
</div>
{/* Session Card (Carousel Item) */}
{currentSession && (
<div className="flex-1 flex flex-col min-h-0 rounded-lg border border-border bg-accent/20 p-2.5 overflow-hidden">
<div className="flex-1 flex flex-col min-h-0 rounded-lg border border-border bg-accent/20 p-3 overflow-hidden">
{/* Session Header */}
<div className="mb-2 pb-2 border-b border-border shrink-0">
<div className="flex items-start gap-2">
<div className={cn('px-1.5 py-0.5 rounded text-[10px] font-medium shrink-0', sessionStatusColors[currentSession.status].bg, sessionStatusColors[currentSession.status].text)}>
<div className={cn('px-2 py-1 rounded text-xs font-medium shrink-0', sessionStatusColors[currentSession.status].bg, sessionStatusColors[currentSession.status].text)}>
{formatMessage({ id: `common.status.${currentSession.status === 'in_progress' ? 'inProgress' : currentSession.status}` })}
</div>
<div className="flex-1 min-w-0">
<p className="text-[11px] font-medium text-foreground truncate">{currentSession.name}</p>
<p className="text-[10px] text-muted-foreground">{currentSession.id}</p>
<p className="text-sm font-medium text-foreground truncate">{currentSession.name}</p>
<p className="text-xs text-muted-foreground">{currentSession.id}</p>
</div>
</div>
{/* Description */}
{currentSession.description && (
<p className="text-[10px] text-muted-foreground mt-1.5 line-clamp-2">
<p className="text-xs text-muted-foreground mt-2 line-clamp-2">
{currentSession.description}
</p>
)}
{/* Progress bar */}
<div className="mt-2 space-y-1">
<div className="flex items-center justify-between text-[10px]">
<div className="mt-2.5 space-y-1">
<div className="flex items-center justify-between text-xs">
<span className="text-muted-foreground">
{formatMessage({ id: 'common.labels.progress' })}
</span>
@@ -655,20 +628,20 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
</div>
<Progress
value={currentSession.tasks.length > 0 ? (currentSession.tasks.filter(t => t.status === 'completed').length / currentSession.tasks.length) * 100 : 0}
className="h-1 bg-muted"
className="h-1.5 bg-muted"
indicatorClassName="bg-success"
/>
</div>
{/* Tags and Date */}
<div className="flex items-center gap-2 mt-1.5 flex-wrap">
<div className="flex items-center gap-2 mt-2 flex-wrap">
{currentSession.tags.map((tag) => (
<span key={tag} className="inline-flex items-center gap-0.5 px-1.5 py-0.5 rounded bg-primary/10 text-primary text-[9px]">
<Tag className="h-2 w-2" />
<span key={tag} className="inline-flex items-center gap-1 px-2 py-0.5 rounded bg-primary/10 text-primary text-[10px]">
<Tag className="h-2.5 w-2.5" />
{tag}
</span>
))}
<span className="inline-flex items-center gap-0.5 text-[9px] text-muted-foreground ml-auto">
<Calendar className="h-2.5 w-2.5" />
<span className="inline-flex items-center gap-1 text-[10px] text-muted-foreground ml-auto">
<Calendar className="h-3 w-3" />
{currentSession.updatedAt}
</span>
</div>
@@ -676,19 +649,23 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
{/* Task List for this Session - Two columns */}
<div className="flex-1 overflow-auto min-h-0">
<div className="grid grid-cols-2 gap-1">
{currentSession.tasks.map((task) => {
<div className="grid grid-cols-2 gap-2 w-full">
{currentSession.tasks.map((task, index) => {
const config = taskStatusColors[task.status];
const StatusIcon = config.icon;
const isLastOdd = currentSession.tasks.length % 2 === 1 && index === currentSession.tasks.length - 1;
return (
<div
key={task.id}
className="flex items-center gap-1.5 p-1.5 rounded hover:bg-background/50 transition-colors cursor-pointer"
className={cn(
'flex items-center gap-2 p-2 rounded hover:bg-background/50 transition-colors cursor-pointer',
isLastOdd && 'col-span-2'
)}
>
<div className={cn('p-0.5 rounded shrink-0', config.bg)}>
<StatusIcon className={cn('h-2.5 w-2.5', config.text)} />
<div className={cn('p-1 rounded shrink-0', config.bg)}>
<StatusIcon className={cn('h-3 w-3', config.text)} />
</div>
<p className={cn('flex-1 text-[10px] font-medium truncate', task.status === 'completed' ? 'text-muted-foreground line-through' : 'text-foreground')}>
<p className={cn('flex-1 text-xs font-medium truncate', task.status === 'completed' ? 'text-muted-foreground line-through' : 'text-foreground')}>
{task.name}
</p>
</div>

View File

@@ -76,18 +76,18 @@ const statusLabelKeys: Record<SessionMetadata['status'], string> = {
paused: 'sessions.status.paused',
};
// Type variant configuration for session type badges
// Type variant configuration for session type badges (unique colors for each type)
const typeVariantConfig: Record<
SessionMetadata['type'],
{ variant: 'default' | 'secondary' | 'destructive' | 'success' | 'warning' | 'info'; icon: React.ElementType }
{ variant: 'default' | 'secondary' | 'destructive' | 'success' | 'warning' | 'info' | 'review'; icon: React.ElementType }
> = {
review: { variant: 'info', icon: Search },
'tdd': { variant: 'success', icon: TestTube },
test: { variant: 'default', icon: FileText },
docs: { variant: 'warning', icon: File },
workflow: { variant: 'secondary', icon: Settings },
'lite-plan': { variant: 'default', icon: FileText },
'lite-fix': { variant: 'warning', icon: Zap },
review: { variant: 'review', icon: Search }, // Purple
'tdd': { variant: 'success', icon: TestTube }, // Green
test: { variant: 'info', icon: FileText }, // Blue
docs: { variant: 'warning', icon: File }, // Orange/Yellow
workflow: { variant: 'default', icon: Settings }, // Primary (blue-violet)
'lite-plan': { variant: 'secondary', icon: FileText }, // Gray/Neutral
'lite-fix': { variant: 'destructive', icon: Zap }, // Red
};
// Type label keys for i18n
@@ -149,6 +149,43 @@ function calculateProgress(tasks: SessionMetadata['tasks']): TaskStatusBreakdown
return { total, completed, failed, pending, inProgress, percentage };
}
/**
* Severity breakdown for review sessions
*/
interface SeverityBreakdown {
total: number;
critical: number;
high: number;
medium: number;
low: number;
}
/**
* Calculate severity breakdown from review dimensions
*/
function calculateSeverityBreakdown(review: SessionMetadata['review']): SeverityBreakdown {
if (!review?.dimensions || review.dimensions.length === 0) {
return { total: 0, critical: 0, high: 0, medium: 0, low: 0 };
}
let critical = 0, high = 0, medium = 0, low = 0;
review.dimensions.forEach(dim => {
if (dim.findings) {
dim.findings.forEach(finding => {
const severity = finding.severity?.toLowerCase();
if (severity === 'critical') critical++;
else if (severity === 'high') high++;
else if (severity === 'medium') medium++;
else if (severity === 'low') low++;
});
}
});
const total = critical + high + medium + low;
return { total, critical, high, medium, low };
}
/**
* SessionCard component for displaying session information
*
@@ -188,6 +225,7 @@ export function SessionCard({
: null;
const progress = calculateProgress(session.tasks);
const severity = calculateSeverityBreakdown(session.review);
const isPlanning = session.status === 'planning';
const isArchived = session.status === 'archived' || session.location === 'archived';
@@ -227,21 +265,24 @@ export function SessionCard({
onClick={handleCardClick}
>
<CardContent className="p-4">
{/* Header - Session ID as title */}
{/* Header - Type badge + Session ID as title */}
<div className="flex items-start justify-between gap-2 mb-2">
<div className="flex-1 min-w-0">
<h3 className="font-bold text-card-foreground text-sm tracking-wide uppercase truncate">
{session.session_id}
</h3>
<div className="flex items-center gap-2 mb-1">
{/* Type badge BEFORE title */}
{typeConfig && typeLabel && (
<Badge variant={typeConfig.variant} className="gap-1 flex-shrink-0">
<typeConfig.icon className="h-3 w-3" />
{typeLabel}
</Badge>
)}
<h3 className="font-bold text-card-foreground text-sm tracking-wide uppercase truncate">
{session.session_id}
</h3>
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<Badge variant={statusVariant}>{statusLabel}</Badge>
{typeConfig && typeLabel && (
<Badge variant={typeConfig.variant} className="gap-1">
<typeConfig.icon className="h-3 w-3" />
{typeLabel}
</Badge>
)}
{showActions && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -291,21 +332,46 @@ export function SessionCard({
</p>
)}
{/* Meta info - enriched */}
{/* Meta info - different based on session type */}
<div className="flex flex-wrap items-center gap-x-4 gap-y-2 text-xs text-muted-foreground">
<span className="flex items-center gap-1">
<Calendar className="h-3.5 w-3.5" />
{formatDate(session.created_at)}
</span>
<span className="flex items-center gap-1">
<ListChecks className="h-3.5 w-3.5" />
{progress.total} {formatMessage({ id: 'sessions.card.tasks' })}
</span>
{progress.total > 0 && (
<span className="flex items-center gap-1">
<CheckCircle2 className="h-3.5 w-3.5 text-success" />
{progress.completed} {formatMessage({ id: 'sessions.card.completed' })}
</span>
{/* Review sessions: Show findings and dimensions */}
{session.type === 'review' ? (
<>
{session.review?.dimensions && session.review.dimensions.length > 0 && (
<span className="flex items-center gap-1">
<Search className="h-3.5 w-3.5" />
{session.review.dimensions.length} {formatMessage({ id: 'sessions.card.dimensions' })}
</span>
)}
{session.review?.findings !== undefined && (
<span className="flex items-center gap-1">
<FileText className="h-3.5 w-3.5" />
{typeof session.review.findings === 'number'
? session.review.findings
: session.review.dimensions?.reduce((sum, dim) => sum + (dim.findings?.length || 0), 0) || 0
} {formatMessage({ id: 'sessions.card.findings' })}
</span>
)}
</>
) : (
<>
{/* Workflow/other sessions: Show tasks */}
<span className="flex items-center gap-1">
<ListChecks className="h-3.5 w-3.5" />
{progress.total} {formatMessage({ id: 'sessions.card.tasks' })}
</span>
{progress.total > 0 && (
<span className="flex items-center gap-1">
<CheckCircle2 className="h-3.5 w-3.5 text-success" />
{progress.completed} {formatMessage({ id: 'sessions.card.completed' })}
</span>
)}
</>
)}
{session.updated_at && session.updated_at !== session.created_at && (
<span className="flex items-center gap-1">
@@ -315,15 +381,9 @@ export function SessionCard({
)}
</div>
{/* Task status badges */}
{progress.total > 0 && (
{/* Task status badges - only for non-review sessions */}
{session.type !== 'review' && progress.total > 0 && (
<div className="flex flex-wrap items-center gap-1.5 mt-2">
{progress.pending > 0 && (
<Badge variant="warning" className="gap-1 px-1.5 py-0 text-[10px]">
<Clock className="h-3 w-3" />
{progress.pending} {formatMessage({ id: 'sessions.taskStatus.pending' })}
</Badge>
)}
{progress.inProgress > 0 && (
<Badge variant="info" className="gap-1 px-1.5 py-0 text-[10px]">
<RefreshCw className="h-3 w-3" />
@@ -345,8 +405,38 @@ export function SessionCard({
</div>
)}
{/* Progress bar (only show if not planning and has tasks) */}
{progress.total > 0 && !isPlanning && (
{/* Severity badges - only for review sessions */}
{session.type === 'review' && severity.total > 0 && (
<div className="flex flex-wrap items-center gap-1.5 mt-2">
{severity.critical > 0 && (
<Badge variant="destructive" className="gap-1 px-1.5 py-0 text-[10px]">
<AlertCircle className="h-3 w-3" />
{severity.critical} Critical
</Badge>
)}
{severity.high > 0 && (
<Badge variant="warning" className="gap-1 px-1.5 py-0 text-[10px]">
<AlertCircle className="h-3 w-3" />
{severity.high} High
</Badge>
)}
{severity.medium > 0 && (
<Badge variant="info" className="gap-1 px-1.5 py-0 text-[10px]">
<Search className="h-3 w-3" />
{severity.medium} Medium
</Badge>
)}
{severity.low > 0 && (
<Badge variant="secondary" className="gap-1 px-1.5 py-0 text-[10px]">
<FileText className="h-3 w-3" />
{severity.low} Low
</Badge>
)}
</div>
)}
{/* Progress bar (only show for non-review sessions with tasks) */}
{session.type !== 'review' && progress.total > 0 && !isPlanning && (
<div className="mt-3">
<div className="flex items-center justify-between text-xs mb-1">
<span className="text-muted-foreground">{formatMessage({ id: 'sessions.card.progress' })}</span>