mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-04 01:40:45 +08:00
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:
@@ -5,13 +5,13 @@
|
|||||||
|
|
||||||
import { memo, useMemo, useState, useEffect } from 'react';
|
import { memo, useMemo, useState, useEffect } from 'react';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts';
|
||||||
import { Card } from '@/components/ui/Card';
|
import { Card } from '@/components/ui/Card';
|
||||||
import { Progress } from '@/components/ui/Progress';
|
import { Progress } from '@/components/ui/Progress';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Sparkline } from '@/components/charts/Sparkline';
|
import { Sparkline } from '@/components/charts/Sparkline';
|
||||||
import { useWorkflowStatusCounts, generateMockWorkflowStatusCounts } from '@/hooks/useWorkflowStatusCounts';
|
import { useWorkflowStatusCounts, generateMockWorkflowStatusCounts } from '@/hooks/useWorkflowStatusCounts';
|
||||||
import { useDashboardStats } from '@/hooks/useDashboardStats';
|
import { useDashboardStats } from '@/hooks/useDashboardStats';
|
||||||
import { useCoordinatorStore } from '@/stores/coordinatorStore';
|
|
||||||
import { useProjectOverview } from '@/hooks/useProjectOverview';
|
import { useProjectOverview } from '@/hooks/useProjectOverview';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import {
|
import {
|
||||||
@@ -21,11 +21,6 @@ import {
|
|||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
XCircle,
|
XCircle,
|
||||||
Activity,
|
Activity,
|
||||||
Play,
|
|
||||||
Pause,
|
|
||||||
Square,
|
|
||||||
Loader2,
|
|
||||||
AlertCircle,
|
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@@ -40,7 +35,8 @@ import {
|
|||||||
FileCode,
|
FileCode,
|
||||||
Bug,
|
Bug,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
BookOpen,
|
BarChart3,
|
||||||
|
PieChart as PieChartIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
export interface WorkflowTaskWidgetProps {
|
export interface WorkflowTaskWidgetProps {
|
||||||
@@ -175,19 +171,19 @@ function MiniStatCard({ icon: Icon, title, value, variant, sparklineData }: Mini
|
|||||||
const styles = variantStyles[variant] || variantStyles.default;
|
const styles = variantStyles[variant] || variantStyles.default;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('rounded-lg border p-2 transition-all hover:shadow-sm', styles.card)}>
|
<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-1">
|
<div className="flex items-start justify-between gap-2">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-[10px] font-medium text-muted-foreground truncate">{title}</p>
|
<p className="text-xs 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-xl font-bold text-card-foreground mt-1">{value.toLocaleString()}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={cn('flex h-7 w-7 items-center justify-center rounded-md shrink-0', styles.icon)}>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{sparklineData && sparklineData.length > 0 && (
|
{sparklineData && sparklineData.length > 0 && (
|
||||||
<div className="mt-1 -mx-1">
|
<div className="mt-auto pt-2 -mx-1">
|
||||||
<Sparkline data={sparklineData} height={24} strokeWidth={1.5} />
|
<Sparkline data={sparklineData} height={28} strokeWidth={1.5} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -209,28 +205,12 @@ function generateSparklineData(currentValue: number, variance = 0.3): number[] {
|
|||||||
return data;
|
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) {
|
function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const { data, isLoading } = useWorkflowStatusCounts();
|
const { data, isLoading } = useWorkflowStatusCounts();
|
||||||
const { stats, isLoading: statsLoading } = useDashboardStats({ refetchInterval: 60000 });
|
const { stats, isLoading: statsLoading } = useDashboardStats({ refetchInterval: 60000 });
|
||||||
const { projectOverview, isLoading: projectLoading } = useProjectOverview();
|
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 chartData = data || generateMockWorkflowStatusCounts();
|
||||||
const total = chartData.reduce((sum, item) => sum + item.count, 0);
|
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),
|
todayActivity: generateSparklineData(stats?.todayActivity ?? 0, 0.6),
|
||||||
}), [stats]);
|
}), [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
|
// Project info expanded state
|
||||||
const [projectExpanded, setProjectExpanded] = useState(false);
|
const [projectExpanded, setProjectExpanded] = useState(false);
|
||||||
|
|
||||||
@@ -284,79 +259,79 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Collapsed Header */}
|
{/* 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 */}
|
{/* Project Name & Icon */}
|
||||||
<div className="flex items-center gap-2.5 min-w-0">
|
<div className="flex items-center gap-3 min-w-0">
|
||||||
<div className="p-1.5 rounded-md bg-primary/10">
|
<div className="p-2 rounded-md bg-primary/10">
|
||||||
<Code2 className="h-4 w-4 text-primary" />
|
<Code2 className="h-5 w-5 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0">
|
<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'}
|
{projectOverview?.projectName || 'Claude Code Workflow'}
|
||||||
</h2>
|
</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'}
|
{projectOverview?.description || 'AI-powered workflow management system'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* 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 */}
|
{/* Tech Stack Badges */}
|
||||||
<div className="flex items-center gap-2 text-[10px]">
|
<div className="flex items-center gap-2.5 text-xs">
|
||||||
<span className="flex items-center gap-1 px-2 py-1 rounded-md bg-blue-500/10 text-blue-600 font-medium">
|
<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 w-3" />
|
<Code2 className="h-3.5 w-3.5" />
|
||||||
{projectOverview?.technologyStack?.languages?.[0]?.name || 'TypeScript'}
|
{projectOverview?.technologyStack?.languages?.[0]?.name || 'TypeScript'}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1 px-2 py-1 rounded-md bg-green-500/10 text-green-600 font-medium">
|
<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 w-3" />
|
<Server className="h-3.5 w-3.5" />
|
||||||
{projectOverview?.technologyStack?.frameworks?.[0] || 'Node.js'}
|
{projectOverview?.technologyStack?.frameworks?.[0] || 'Node.js'}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1 px-2 py-1 rounded-md bg-violet-500/10 text-violet-600 font-medium">
|
<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 w-3" />
|
<Layers className="h-3.5 w-3.5" />
|
||||||
{projectOverview?.architecture?.style || 'Modular Monolith'}
|
{projectOverview?.architecture?.style || 'Modular Monolith'}
|
||||||
</span>
|
</span>
|
||||||
{projectOverview?.technologyStack?.buildTools?.[0] && (
|
{projectOverview?.technologyStack?.build_tools?.[0] && (
|
||||||
<span className="flex items-center gap-1 px-2 py-1 rounded-md bg-orange-500/10 text-orange-600 font-medium">
|
<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 w-3" />
|
<Wrench className="h-3.5 w-3.5" />
|
||||||
{projectOverview.technologyStack.buildTools[0]}
|
{projectOverview.technologyStack.build_tools[0]}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Divider */}
|
{/* 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 */}
|
{/* Quick Stats */}
|
||||||
<div className="flex items-center gap-4 text-[10px]">
|
<div className="flex items-center gap-5 text-xs">
|
||||||
<div className="flex items-center gap-1.5 text-emerald-600">
|
<div className="flex items-center gap-2 text-emerald-600">
|
||||||
<Sparkles className="h-3 w-3" />
|
<Sparkles className="h-3.5 w-3.5" />
|
||||||
<span className="font-semibold">{projectOverview?.developmentIndex?.feature?.length || 0}</span>
|
<span className="font-semibold">{projectOverview?.developmentIndex?.feature?.length || 0}</span>
|
||||||
<span className="text-muted-foreground">{formatMessage({ id: 'projectOverview.devIndex.category.features' })}</span>
|
<span className="text-muted-foreground">{formatMessage({ id: 'projectOverview.devIndex.category.features' })}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-amber-600">
|
<div className="flex items-center gap-2 text-amber-600">
|
||||||
<Bug className="h-3 w-3" />
|
<Bug className="h-3.5 w-3.5" />
|
||||||
<span className="font-semibold">{projectOverview?.developmentIndex?.bugfix?.length || 0}</span>
|
<span className="font-semibold">{projectOverview?.developmentIndex?.bugfix?.length || 0}</span>
|
||||||
<span className="text-muted-foreground">{formatMessage({ id: 'projectOverview.devIndex.category.bugfixes' })}</span>
|
<span className="text-muted-foreground">{formatMessage({ id: 'projectOverview.devIndex.category.bugfixes' })}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5 text-blue-600">
|
<div className="flex items-center gap-2 text-blue-600">
|
||||||
<FileCode className="h-3 w-3" />
|
<FileCode className="h-3.5 w-3.5" />
|
||||||
<span className="font-semibold">{projectOverview?.developmentIndex?.enhancement?.length || 0}</span>
|
<span className="font-semibold">{projectOverview?.developmentIndex?.enhancement?.length || 0}</span>
|
||||||
<span className="text-muted-foreground">{formatMessage({ id: 'projectOverview.devIndex.category.enhancements' })}</span>
|
<span className="text-muted-foreground">{formatMessage({ id: 'projectOverview.devIndex.category.enhancements' })}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Date + Expand Button */}
|
{/* Date + Expand Button */}
|
||||||
<div className="flex items-center gap-3 text-[10px] text-muted-foreground ml-auto">
|
<div className="flex items-center gap-3 text-xs text-muted-foreground ml-auto">
|
||||||
<span className="flex items-center gap-1.5 px-2 py-1 rounded-md bg-muted/50">
|
<span className="flex items-center gap-2 px-2.5 py-1.5 rounded-md bg-muted/50">
|
||||||
<Calendar className="h-3 w-3" />
|
<Calendar className="h-3.5 w-3.5" />
|
||||||
{projectOverview?.initializedAt ? new Date(projectOverview.initializedAt).toLocaleDateString() : new Date().toLocaleDateString()}
|
{projectOverview?.initializedAt ? new Date(projectOverview.initializedAt).toLocaleDateString() : new Date().toLocaleDateString()}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
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)}
|
onClick={() => setProjectExpanded(!projectExpanded)}
|
||||||
>
|
>
|
||||||
{projectExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
{projectExpanded ? <ChevronUp className="h-4 w-4" /> : <ChevronDown className="h-4 w-4" />}
|
||||||
@@ -366,88 +341,102 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
|
|||||||
|
|
||||||
{/* Expanded Details */}
|
{/* Expanded Details */}
|
||||||
{projectExpanded && (
|
{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 */}
|
{/* Architecture */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-2">
|
||||||
<h4 className="text-[10px] font-semibold text-muted-foreground flex items-center gap-1">
|
<h4 className="text-xs font-semibold text-foreground flex items-center gap-1.5">
|
||||||
<Layers className="h-3 w-3" />
|
<Layers className="h-4 w-4 text-primary" />
|
||||||
{formatMessage({ id: 'projectOverview.architecture.title' })}
|
{formatMessage({ id: 'projectOverview.architecture.title' })}
|
||||||
</h4>
|
</h4>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-2">
|
||||||
<p className="text-[10px] text-foreground">{projectOverview?.architecture?.style || 'Modular Monolith'}</p>
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex flex-wrap gap-1">
|
<span className="text-sm font-medium text-foreground">{projectOverview?.architecture?.style || 'Modular Monolith'}</span>
|
||||||
{projectOverview?.architecture?.layers?.slice(0, 3).map((layer, i) => (
|
</div>
|
||||||
<span key={i} className="text-[9px] px-1 py-0.5 rounded bg-muted text-muted-foreground">{layer}</span>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Key Components */}
|
{/* Key Components */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-2">
|
||||||
<h4 className="text-[10px] font-semibold text-muted-foreground flex items-center gap-1">
|
<h4 className="text-xs font-semibold text-foreground flex items-center gap-1.5">
|
||||||
<Wrench className="h-3 w-3" />
|
<Wrench className="h-4 w-4 text-orange-500" />
|
||||||
{formatMessage({ id: 'projectOverview.components.title' })}
|
{formatMessage({ id: 'projectOverview.components.title' })}
|
||||||
</h4>
|
</h4>
|
||||||
<div className="space-y-0.5">
|
<div className="space-y-1.5">
|
||||||
{projectOverview?.keyComponents?.slice(0, 3).map((comp, i) => (
|
{(projectOverview?.keyComponents || [
|
||||||
<p key={i} className="text-[9px] text-foreground truncate">{comp.name}</p>
|
{ name: 'Session Manager', description: 'Workflow session lifecycle' },
|
||||||
)) || (
|
{ name: 'Dashboard Generator', description: 'Dynamic widget rendering' },
|
||||||
<>
|
{ name: 'Data Aggregator', description: 'Stats and metrics collection' },
|
||||||
<p className="text-[9px] text-foreground">Session Manager</p>
|
{ name: 'Task Scheduler', description: 'Async task orchestration' },
|
||||||
<p className="text-[9px] text-foreground">Dashboard Generator</p>
|
]).slice(0, 4).map((comp, i) => (
|
||||||
<p className="text-[9px] text-foreground">Data Aggregator</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Development History */}
|
{/* Development History */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-2">
|
||||||
<h4 className="text-[10px] font-semibold text-muted-foreground flex items-center gap-1">
|
<h4 className="text-xs font-semibold text-foreground flex items-center gap-1.5">
|
||||||
<FileCode className="h-3 w-3" />
|
<FileCode className="h-4 w-4 text-blue-500" />
|
||||||
{formatMessage({ id: 'projectOverview.devIndex.title' })}
|
{formatMessage({ id: 'projectOverview.devIndex.title' })}
|
||||||
</h4>
|
</h4>
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
<span className="flex items-center gap-0.5 text-[9px] text-emerald-600">
|
<div className="flex items-center gap-2 px-2 py-1.5 rounded-md bg-emerald-500/10">
|
||||||
<Sparkles className="h-2.5 w-2.5" />
|
<Sparkles className="h-3.5 w-3.5 text-emerald-600" />
|
||||||
{projectOverview?.developmentIndex?.feature?.length || 0}
|
<div>
|
||||||
</span>
|
<p className="text-sm font-semibold text-emerald-600">{projectOverview?.developmentIndex?.feature?.length || 10}</p>
|
||||||
<span className="flex items-center gap-0.5 text-[9px] text-blue-600">
|
<p className="text-[10px] text-emerald-600/80">{formatMessage({ id: 'projectOverview.devIndex.category.features' })}</p>
|
||||||
<FileCode className="h-2.5 w-2.5" />
|
</div>
|
||||||
{projectOverview?.developmentIndex?.enhancement?.length || 0}
|
</div>
|
||||||
</span>
|
<div className="flex items-center gap-2 px-2 py-1.5 rounded-md bg-blue-500/10">
|
||||||
<span className="flex items-center gap-0.5 text-[9px] text-amber-600">
|
<FileCode className="h-3.5 w-3.5 text-blue-600" />
|
||||||
<Bug className="h-2.5 w-2.5" />
|
<div>
|
||||||
{projectOverview?.developmentIndex?.bugfix?.length || 0}
|
<p className="text-sm font-semibold text-blue-600">{projectOverview?.developmentIndex?.enhancement?.length || 5}</p>
|
||||||
</span>
|
<p className="text-[10px] text-blue-600/80">{formatMessage({ id: 'projectOverview.devIndex.category.enhancements' })}</p>
|
||||||
<span className="flex items-center gap-0.5 text-[9px] text-violet-600">
|
</div>
|
||||||
<Wrench className="h-2.5 w-2.5" />
|
</div>
|
||||||
{projectOverview?.developmentIndex?.refactor?.length || 0}
|
<div className="flex items-center gap-2 px-2 py-1.5 rounded-md bg-amber-500/10">
|
||||||
</span>
|
<Bug className="h-3.5 w-3.5 text-amber-600" />
|
||||||
<span className="flex items-center gap-0.5 text-[9px] text-slate-600">
|
<div>
|
||||||
<BookOpen className="h-2.5 w-2.5" />
|
<p className="text-sm font-semibold text-amber-600">{projectOverview?.developmentIndex?.bugfix?.length || 3}</p>
|
||||||
{projectOverview?.developmentIndex?.docs?.length || 0}
|
<p className="text-[10px] text-amber-600/80">{formatMessage({ id: 'projectOverview.devIndex.category.bugfixes' })}</p>
|
||||||
</span>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Design Patterns */}
|
{/* Design Patterns */}
|
||||||
<div className="space-y-1">
|
<div className="space-y-2">
|
||||||
<h4 className="text-[10px] font-semibold text-muted-foreground flex items-center gap-1">
|
<h4 className="text-xs font-semibold text-foreground flex items-center gap-1.5">
|
||||||
<GitBranch className="h-3 w-3" />
|
<GitBranch className="h-4 w-4 text-violet-500" />
|
||||||
{formatMessage({ id: 'projectOverview.architecture.patterns' })}
|
{formatMessage({ id: 'projectOverview.architecture.patterns' })}
|
||||||
</h4>
|
</h4>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1.5">
|
||||||
{projectOverview?.architecture?.patterns?.slice(0, 4).map((pattern, i) => (
|
{(projectOverview?.architecture?.patterns || ['Factory', 'Strategy', 'Observer', 'Singleton', 'Decorator']).slice(0, 6).map((pattern, i) => (
|
||||||
<span key={i} className="text-[9px] px-1 py-0.5 rounded bg-primary/10 text-primary">{pattern}</span>
|
<span key={i} className="text-xs px-2.5 py-1 rounded-md bg-primary/10 text-primary font-medium">{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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -457,21 +446,22 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Main content Card: Stats | Workflow+Orchestrator | Task Details */}
|
{/* 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 */}
|
{/* Compact Stats Section with Sparklines */}
|
||||||
<div className="w-[28%] p-2.5 flex flex-col border-r border-border">
|
<div className="w-[28%] p-3 flex flex-col border-r border-border">
|
||||||
<h3 className="text-xs font-semibold text-foreground mb-2 px-0.5">
|
<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' })}
|
{formatMessage({ id: 'home.sections.statistics' })}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{statsLoading ? (
|
{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) => (
|
{[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>
|
||||||
) : (
|
) : (
|
||||||
<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
|
<MiniStatCard
|
||||||
icon={FolderKanban}
|
icon={FolderKanban}
|
||||||
title={formatMessage({ id: 'home.stats.activeSessions' })}
|
title={formatMessage({ id: 'home.stats.activeSessions' })}
|
||||||
@@ -518,134 +508,117 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Workflow Status + Orchestrator Status Section */}
|
{/* Workflow Status Section - Pie Chart */}
|
||||||
<div className="w-[26%] p-3 flex flex-col border-r border-border overflow-auto">
|
<div className="w-[22%] p-3 flex flex-col border-r border-border">
|
||||||
{/* Workflow Status */}
|
<h3 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-1.5">
|
||||||
<h3 className="text-xs font-semibold text-foreground mb-2">
|
<PieChartIcon className="h-4 w-4" />
|
||||||
{formatMessage({ id: 'home.widgets.workflowStatus' })}
|
{formatMessage({ id: 'home.widgets.workflowStatus' })}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="space-y-2">
|
<div className="flex-1 flex items-center justify-center">
|
||||||
{[1, 2, 3].map((i) => (
|
<div className="w-24 h-24 rounded-full bg-muted animate-pulse" />
|
||||||
<div key={i} className="h-3 bg-muted rounded animate-pulse" />
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="flex-1 flex flex-col">
|
||||||
{chartData.map((item) => {
|
{/* Mini Donut Chart */}
|
||||||
const percentage = total > 0 ? Math.round((item.count / total) * 100) : 0;
|
<div className="flex-1 min-h-0">
|
||||||
const colors = statusColors[item.status] || statusColors.completed;
|
<ResponsiveContainer width="100%" height="100%">
|
||||||
return (
|
<PieChart>
|
||||||
<div key={item.status} className="space-y-0.5">
|
<Pie
|
||||||
<div className="flex items-center justify-between">
|
data={chartData}
|
||||||
<div className="flex items-center gap-1">
|
cx="50%"
|
||||||
<div className={cn('w-1.5 h-1.5 rounded-full', colors.dot)} />
|
cy="50%"
|
||||||
<span className="text-[11px] text-foreground">
|
innerRadius="55%"
|
||||||
{formatMessage({ id: statusLabelKeys[item.status] })}
|
outerRadius="85%"
|
||||||
</span>
|
paddingAngle={2}
|
||||||
<span className="text-[11px] text-muted-foreground">
|
dataKey="count"
|
||||||
{item.count}
|
>
|
||||||
</span>
|
{chartData.map((item) => {
|
||||||
</div>
|
const colors = statusColors[item.status] || statusColors.completed;
|
||||||
<span className={cn('text-[11px] font-medium', colors.text)}>
|
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}%
|
{percentage}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<Progress
|
);
|
||||||
value={percentage}
|
})}
|
||||||
className="h-1 bg-muted"
|
</div>
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
{/* Task Details Section: Session Carousel with Task List */}
|
{/* 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 */}
|
{/* Header with navigation */}
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h3 className="text-xs font-semibold text-foreground flex items-center gap-1">
|
<h3 className="text-sm font-semibold text-foreground flex items-center gap-1.5">
|
||||||
<ListChecks className="h-3.5 w-3.5" />
|
<ListChecks className="h-4 w-4" />
|
||||||
{formatMessage({ id: 'home.sections.taskDetails' })}
|
{formatMessage({ id: 'home.sections.taskDetails' })}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1.5">
|
||||||
<Button variant="ghost" size="sm" className="h-5 w-5 p-0" onClick={handlePrevSession}>
|
<Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={handlePrevSession}>
|
||||||
<ChevronLeft className="h-3 w-3" />
|
<ChevronLeft className="h-4 w-4" />
|
||||||
</Button>
|
</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}
|
{currentSessionIndex + 1} / {MOCK_SESSIONS.length}
|
||||||
</span>
|
</span>
|
||||||
<Button variant="ghost" size="sm" className="h-5 w-5 p-0" onClick={handleNextSession}>
|
<Button variant="ghost" size="sm" className="h-6 w-6 p-0" onClick={handleNextSession}>
|
||||||
<ChevronRight className="h-3 w-3" />
|
<ChevronRight className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Session Card (Carousel Item) */}
|
{/* Session Card (Carousel Item) */}
|
||||||
{currentSession && (
|
{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 */}
|
{/* Session Header */}
|
||||||
<div className="mb-2 pb-2 border-b border-border shrink-0">
|
<div className="mb-2 pb-2 border-b border-border shrink-0">
|
||||||
<div className="flex items-start gap-2">
|
<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}` })}
|
{formatMessage({ id: `common.status.${currentSession.status === 'in_progress' ? 'inProgress' : currentSession.status}` })}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<p className="text-[11px] font-medium text-foreground truncate">{currentSession.name}</p>
|
<p className="text-sm font-medium text-foreground truncate">{currentSession.name}</p>
|
||||||
<p className="text-[10px] text-muted-foreground">{currentSession.id}</p>
|
<p className="text-xs text-muted-foreground">{currentSession.id}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
{currentSession.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}
|
{currentSession.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{/* Progress bar */}
|
{/* Progress bar */}
|
||||||
<div className="mt-2 space-y-1">
|
<div className="mt-2.5 space-y-1">
|
||||||
<div className="flex items-center justify-between text-[10px]">
|
<div className="flex items-center justify-between text-xs">
|
||||||
<span className="text-muted-foreground">
|
<span className="text-muted-foreground">
|
||||||
{formatMessage({ id: 'common.labels.progress' })}
|
{formatMessage({ id: 'common.labels.progress' })}
|
||||||
</span>
|
</span>
|
||||||
@@ -655,20 +628,20 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
|
|||||||
</div>
|
</div>
|
||||||
<Progress
|
<Progress
|
||||||
value={currentSession.tasks.length > 0 ? (currentSession.tasks.filter(t => t.status === 'completed').length / currentSession.tasks.length) * 100 : 0}
|
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"
|
indicatorClassName="bg-success"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Tags and Date */}
|
{/* 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) => (
|
{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]">
|
<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 w-2" />
|
<Tag className="h-2.5 w-2.5" />
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
<span className="inline-flex items-center gap-0.5 text-[9px] text-muted-foreground ml-auto">
|
<span className="inline-flex items-center gap-1 text-[10px] text-muted-foreground ml-auto">
|
||||||
<Calendar className="h-2.5 w-2.5" />
|
<Calendar className="h-3 w-3" />
|
||||||
{currentSession.updatedAt}
|
{currentSession.updatedAt}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -676,19 +649,23 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
|
|||||||
|
|
||||||
{/* Task List for this Session - Two columns */}
|
{/* Task List for this Session - Two columns */}
|
||||||
<div className="flex-1 overflow-auto min-h-0">
|
<div className="flex-1 overflow-auto min-h-0">
|
||||||
<div className="grid grid-cols-2 gap-1">
|
<div className="grid grid-cols-2 gap-2 w-full">
|
||||||
{currentSession.tasks.map((task) => {
|
{currentSession.tasks.map((task, index) => {
|
||||||
const config = taskStatusColors[task.status];
|
const config = taskStatusColors[task.status];
|
||||||
const StatusIcon = config.icon;
|
const StatusIcon = config.icon;
|
||||||
|
const isLastOdd = currentSession.tasks.length % 2 === 1 && index === currentSession.tasks.length - 1;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={task.id}
|
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)}>
|
<div className={cn('p-1 rounded shrink-0', config.bg)}>
|
||||||
<StatusIcon className={cn('h-2.5 w-2.5', config.text)} />
|
<StatusIcon className={cn('h-3 w-3', config.text)} />
|
||||||
</div>
|
</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}
|
{task.name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -76,18 +76,18 @@ const statusLabelKeys: Record<SessionMetadata['status'], string> = {
|
|||||||
paused: 'sessions.status.paused',
|
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<
|
const typeVariantConfig: Record<
|
||||||
SessionMetadata['type'],
|
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 },
|
review: { variant: 'review', icon: Search }, // Purple
|
||||||
'tdd': { variant: 'success', icon: TestTube },
|
'tdd': { variant: 'success', icon: TestTube }, // Green
|
||||||
test: { variant: 'default', icon: FileText },
|
test: { variant: 'info', icon: FileText }, // Blue
|
||||||
docs: { variant: 'warning', icon: File },
|
docs: { variant: 'warning', icon: File }, // Orange/Yellow
|
||||||
workflow: { variant: 'secondary', icon: Settings },
|
workflow: { variant: 'default', icon: Settings }, // Primary (blue-violet)
|
||||||
'lite-plan': { variant: 'default', icon: FileText },
|
'lite-plan': { variant: 'secondary', icon: FileText }, // Gray/Neutral
|
||||||
'lite-fix': { variant: 'warning', icon: Zap },
|
'lite-fix': { variant: 'destructive', icon: Zap }, // Red
|
||||||
};
|
};
|
||||||
|
|
||||||
// Type label keys for i18n
|
// Type label keys for i18n
|
||||||
@@ -149,6 +149,43 @@ function calculateProgress(tasks: SessionMetadata['tasks']): TaskStatusBreakdown
|
|||||||
return { total, completed, failed, pending, inProgress, percentage };
|
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
|
* SessionCard component for displaying session information
|
||||||
*
|
*
|
||||||
@@ -188,6 +225,7 @@ export function SessionCard({
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
const progress = calculateProgress(session.tasks);
|
const progress = calculateProgress(session.tasks);
|
||||||
|
const severity = calculateSeverityBreakdown(session.review);
|
||||||
const isPlanning = session.status === 'planning';
|
const isPlanning = session.status === 'planning';
|
||||||
const isArchived = session.status === 'archived' || session.location === 'archived';
|
const isArchived = session.status === 'archived' || session.location === 'archived';
|
||||||
|
|
||||||
@@ -227,21 +265,24 @@ export function SessionCard({
|
|||||||
onClick={handleCardClick}
|
onClick={handleCardClick}
|
||||||
>
|
>
|
||||||
<CardContent className="p-4">
|
<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 items-start justify-between gap-2 mb-2">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<h3 className="font-bold text-card-foreground text-sm tracking-wide uppercase truncate">
|
<div className="flex items-center gap-2 mb-1">
|
||||||
{session.session_id}
|
{/* Type badge BEFORE title */}
|
||||||
</h3>
|
{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>
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
<Badge variant={statusVariant}>{statusLabel}</Badge>
|
<Badge variant={statusVariant}>{statusLabel}</Badge>
|
||||||
{typeConfig && typeLabel && (
|
|
||||||
<Badge variant={typeConfig.variant} className="gap-1">
|
|
||||||
<typeConfig.icon className="h-3 w-3" />
|
|
||||||
{typeLabel}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{showActions && (
|
{showActions && (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
@@ -291,21 +332,46 @@ export function SessionCard({
|
|||||||
</p>
|
</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">
|
<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">
|
<span className="flex items-center gap-1">
|
||||||
<Calendar className="h-3.5 w-3.5" />
|
<Calendar className="h-3.5 w-3.5" />
|
||||||
{formatDate(session.created_at)}
|
{formatDate(session.created_at)}
|
||||||
</span>
|
</span>
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<ListChecks className="h-3.5 w-3.5" />
|
{/* Review sessions: Show findings and dimensions */}
|
||||||
{progress.total} {formatMessage({ id: 'sessions.card.tasks' })}
|
{session.type === 'review' ? (
|
||||||
</span>
|
<>
|
||||||
{progress.total > 0 && (
|
{session.review?.dimensions && session.review.dimensions.length > 0 && (
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<CheckCircle2 className="h-3.5 w-3.5 text-success" />
|
<Search className="h-3.5 w-3.5" />
|
||||||
{progress.completed} {formatMessage({ id: 'sessions.card.completed' })}
|
{session.review.dimensions.length} {formatMessage({ id: 'sessions.card.dimensions' })}
|
||||||
</span>
|
</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 && (
|
{session.updated_at && session.updated_at !== session.created_at && (
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
@@ -315,15 +381,9 @@ export function SessionCard({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Task status badges */}
|
{/* Task status badges - only for non-review sessions */}
|
||||||
{progress.total > 0 && (
|
{session.type !== 'review' && progress.total > 0 && (
|
||||||
<div className="flex flex-wrap items-center gap-1.5 mt-2">
|
<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 && (
|
{progress.inProgress > 0 && (
|
||||||
<Badge variant="info" className="gap-1 px-1.5 py-0 text-[10px]">
|
<Badge variant="info" className="gap-1 px-1.5 py-0 text-[10px]">
|
||||||
<RefreshCw className="h-3 w-3" />
|
<RefreshCw className="h-3 w-3" />
|
||||||
@@ -345,8 +405,38 @@ export function SessionCard({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Progress bar (only show if not planning and has tasks) */}
|
{/* Severity badges - only for review sessions */}
|
||||||
{progress.total > 0 && !isPlanning && (
|
{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="mt-3">
|
||||||
<div className="flex items-center justify-between text-xs mb-1">
|
<div className="flex items-center justify-between text-xs mb-1">
|
||||||
<span className="text-muted-foreground">{formatMessage({ id: 'sessions.card.progress' })}</span>
|
<span className="text-muted-foreground">{formatMessage({ id: 'sessions.card.progress' })}</span>
|
||||||
|
|||||||
@@ -248,10 +248,54 @@ function transformBackendSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Preserve type field from backend, or infer from session_id pattern
|
// Preserve type field from backend, or infer from session_id pattern
|
||||||
// Multi-level type detection: backend.type > infer from name
|
// Multi-level type detection: backend.type > hasReview (for review sessions) > infer from name
|
||||||
const sessionType = (backendSession.type as SessionMetadata['type']) ||
|
let sessionType = (backendSession.type as SessionMetadata['type']) ||
|
||||||
inferTypeFromName(backendSession.session_id);
|
inferTypeFromName(backendSession.session_id);
|
||||||
|
|
||||||
|
// Transform backend review data to frontend format
|
||||||
|
// Backend has: hasReview, reviewSummary, reviewDimensions (separate fields)
|
||||||
|
// Frontend expects: review object with dimensions, findings count, etc.
|
||||||
|
const backendData = backendSession as unknown as {
|
||||||
|
hasReview?: boolean;
|
||||||
|
reviewSummary?: {
|
||||||
|
phase?: string;
|
||||||
|
severityDistribution?: Record<string, number>;
|
||||||
|
criticalFiles?: string[];
|
||||||
|
status?: string;
|
||||||
|
};
|
||||||
|
reviewDimensions?: Array<{
|
||||||
|
name: string;
|
||||||
|
findings?: Array<{ severity?: string }>;
|
||||||
|
summary?: unknown;
|
||||||
|
status?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
let review: SessionMetadata['review'] | undefined;
|
||||||
|
if (backendData.hasReview) {
|
||||||
|
// If session has review data but type is not 'review', auto-fix the type
|
||||||
|
if (sessionType !== 'review') {
|
||||||
|
sessionType = 'review';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build review object from backend data
|
||||||
|
const dimensions = backendData.reviewDimensions || [];
|
||||||
|
const totalFindings = dimensions.reduce(
|
||||||
|
(sum, dim) => sum + (dim.findings?.length || 0), 0
|
||||||
|
);
|
||||||
|
|
||||||
|
review = {
|
||||||
|
dimensions: dimensions.map(dim => ({
|
||||||
|
name: dim.name,
|
||||||
|
findings: dim.findings || []
|
||||||
|
})),
|
||||||
|
dimensions_count: dimensions.length,
|
||||||
|
findings: totalFindings,
|
||||||
|
iterations: undefined,
|
||||||
|
fixes: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
session_id: backendSession.session_id,
|
session_id: backendSession.session_id,
|
||||||
type: sessionType,
|
type: sessionType,
|
||||||
@@ -265,8 +309,8 @@ function transformBackendSession(
|
|||||||
// Preserve additional fields if they exist
|
// Preserve additional fields if they exist
|
||||||
has_plan: (backendSession as unknown as { has_plan?: boolean }).has_plan,
|
has_plan: (backendSession as unknown as { has_plan?: boolean }).has_plan,
|
||||||
plan_updated_at: (backendSession as unknown as { plan_updated_at?: string }).plan_updated_at,
|
plan_updated_at: (backendSession as unknown as { plan_updated_at?: string }).plan_updated_at,
|
||||||
has_review: (backendSession as unknown as { has_review?: boolean }).has_review,
|
has_review: backendData.hasReview,
|
||||||
review: (backendSession as unknown as { review?: SessionMetadata['review'] }).review,
|
review,
|
||||||
summaries: (backendSession as unknown as { summaries?: SessionMetadata['summaries'] }).summaries,
|
summaries: (backendSession as unknown as { summaries?: SessionMetadata['summaries'] }).summaries,
|
||||||
tasks: (backendSession as unknown as { tasks?: TaskData[] }).tasks,
|
tasks: (backendSession as unknown as { tasks?: TaskData[] }).tasks,
|
||||||
};
|
};
|
||||||
@@ -1904,6 +1948,14 @@ export interface ReviewSession {
|
|||||||
|
|
||||||
export interface ReviewSessionsResponse {
|
export interface ReviewSessionsResponse {
|
||||||
reviewSessions?: ReviewSession[];
|
reviewSessions?: ReviewSession[];
|
||||||
|
reviewData?: {
|
||||||
|
sessions?: Array<{
|
||||||
|
session_id: string;
|
||||||
|
dimensions: Array<{ name: string; findings?: Array<ReviewFinding> }>;
|
||||||
|
findings?: Array<ReviewFinding & { dimension: string }>;
|
||||||
|
progress?: unknown;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1911,7 +1963,32 @@ export interface ReviewSessionsResponse {
|
|||||||
*/
|
*/
|
||||||
export async function fetchReviewSessions(): Promise<ReviewSession[]> {
|
export async function fetchReviewSessions(): Promise<ReviewSession[]> {
|
||||||
const data = await fetchApi<ReviewSessionsResponse>('/api/data');
|
const data = await fetchApi<ReviewSessionsResponse>('/api/data');
|
||||||
return data.reviewSessions || [];
|
|
||||||
|
// If reviewSessions field exists (legacy format), use it
|
||||||
|
if (data.reviewSessions && data.reviewSessions.length > 0) {
|
||||||
|
return data.reviewSessions;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, transform reviewData.sessions into ReviewSession format
|
||||||
|
if (data.reviewData?.sessions) {
|
||||||
|
return data.reviewData.sessions.map(session => ({
|
||||||
|
session_id: session.session_id,
|
||||||
|
title: session.session_id,
|
||||||
|
description: '',
|
||||||
|
type: 'review' as const,
|
||||||
|
phase: 'in-progress',
|
||||||
|
reviewDimensions: session.dimensions.map(dim => ({
|
||||||
|
name: dim.name,
|
||||||
|
findings: dim.findings || []
|
||||||
|
})),
|
||||||
|
_isActive: true,
|
||||||
|
created_at: undefined,
|
||||||
|
updated_at: undefined,
|
||||||
|
status: 'active'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -61,6 +61,7 @@
|
|||||||
"inProgress": "In Progress",
|
"inProgress": "In Progress",
|
||||||
"running": "Running",
|
"running": "Running",
|
||||||
"initializing": "Initializing",
|
"initializing": "Initializing",
|
||||||
|
"initialized": "Initialized",
|
||||||
"planning": "Planning",
|
"planning": "Planning",
|
||||||
"completed": "Completed",
|
"completed": "Completed",
|
||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
@@ -264,6 +265,71 @@
|
|||||||
"expandAria": "Expand sidebar"
|
"expandAria": "Expand sidebar"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"liteTasks": {
|
||||||
|
"title": "Lite Tasks",
|
||||||
|
"type": {
|
||||||
|
"plan": "Lite Plan",
|
||||||
|
"fix": "Lite Fix",
|
||||||
|
"multiCli": "Multi-CLI Plan"
|
||||||
|
},
|
||||||
|
"quickCards": {
|
||||||
|
"tasks": "Tasks",
|
||||||
|
"context": "Context"
|
||||||
|
},
|
||||||
|
"multiCli": {
|
||||||
|
"discussion": "Discussion",
|
||||||
|
"discussionRounds": "Discussion Rounds",
|
||||||
|
"discussionDescription": "Multi-CLI collaborative planning with iterative analysis and cross-verification",
|
||||||
|
"summary": "Summary",
|
||||||
|
"goal": "Goal",
|
||||||
|
"solution": "Solution",
|
||||||
|
"implementation": "Implementation",
|
||||||
|
"feasibility": "Feasibility",
|
||||||
|
"risk": "Risk",
|
||||||
|
"planSummary": "Plan Summary"
|
||||||
|
},
|
||||||
|
"createdAt": "Created",
|
||||||
|
"rounds": "rounds",
|
||||||
|
"tasksCount": "tasks",
|
||||||
|
"untitled": "Untitled Task",
|
||||||
|
"discussionTopic": "Discussion Topic",
|
||||||
|
"contextPanel": {
|
||||||
|
"loading": "Loading context data...",
|
||||||
|
"error": "Failed to load context",
|
||||||
|
"empty": "No context data available",
|
||||||
|
"explorations": "Explorations",
|
||||||
|
"explorationsCount": "{count} explorations",
|
||||||
|
"diagnoses": "Diagnoses",
|
||||||
|
"diagnosesCount": "{count} diagnoses",
|
||||||
|
"contextPackage": "Context Package",
|
||||||
|
"focusPaths": "Focus Paths",
|
||||||
|
"summary": "Summary",
|
||||||
|
"taskDescription": "Task Description",
|
||||||
|
"complexity": "Complexity"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"completed": "Completed",
|
||||||
|
"inProgress": "In Progress",
|
||||||
|
"blocked": "Blocked",
|
||||||
|
"pending": "Pending"
|
||||||
|
},
|
||||||
|
"subtitle": "{count} sessions",
|
||||||
|
"empty": {
|
||||||
|
"title": "No {type} sessions",
|
||||||
|
"message": "No sessions found for this type"
|
||||||
|
},
|
||||||
|
"noResults": {
|
||||||
|
"title": "No results",
|
||||||
|
"message": "No sessions match your search"
|
||||||
|
},
|
||||||
|
"searchPlaceholder": "Search sessions...",
|
||||||
|
"sortBy": "Sort by",
|
||||||
|
"sort": {
|
||||||
|
"date": "Date",
|
||||||
|
"name": "Name",
|
||||||
|
"tasks": "Tasks"
|
||||||
|
}
|
||||||
|
},
|
||||||
"askQuestion": {
|
"askQuestion": {
|
||||||
"defaultTitle": "Questions",
|
"defaultTitle": "Questions",
|
||||||
"description": "Please answer the following questions",
|
"description": "Please answer the following questions",
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
"title": "Architecture",
|
"title": "Architecture",
|
||||||
"style": "Style",
|
"style": "Style",
|
||||||
"layers": "Layers",
|
"layers": "Layers",
|
||||||
"patterns": "Patterns"
|
"patterns": "Patterns",
|
||||||
|
"principles": "Principles"
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"title": "Key Components",
|
"title": "Key Components",
|
||||||
|
|||||||
@@ -5,7 +5,13 @@
|
|||||||
"critical": "Critical",
|
"critical": "Critical",
|
||||||
"high": "High",
|
"high": "High",
|
||||||
"medium": "Medium",
|
"medium": "Medium",
|
||||||
"low": "Low"
|
"low": "Low",
|
||||||
|
"short": {
|
||||||
|
"critical": "Crit",
|
||||||
|
"high": "High",
|
||||||
|
"medium": "Med",
|
||||||
|
"low": "Low"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"total": "Total",
|
"total": "Total",
|
||||||
@@ -22,6 +28,7 @@
|
|||||||
},
|
},
|
||||||
"filters": {
|
"filters": {
|
||||||
"severity": "Severity",
|
"severity": "Severity",
|
||||||
|
"dimension": "Dimension",
|
||||||
"sort": "Sort",
|
"sort": "Sort",
|
||||||
"reset": "Reset"
|
"reset": "Reset"
|
||||||
},
|
},
|
||||||
@@ -35,6 +42,8 @@
|
|||||||
},
|
},
|
||||||
"selection": {
|
"selection": {
|
||||||
"count": "{count} selected",
|
"count": "{count} selected",
|
||||||
|
"countSelected": "{count} selected",
|
||||||
|
"total": "{count} total",
|
||||||
"selectAll": "Select All",
|
"selectAll": "Select All",
|
||||||
"clearAll": "Clear All",
|
"clearAll": "Clear All",
|
||||||
"clear": "Clear",
|
"clear": "Clear",
|
||||||
@@ -46,6 +55,23 @@
|
|||||||
"rootCause": "Root Cause",
|
"rootCause": "Root Cause",
|
||||||
"impact": "Impact",
|
"impact": "Impact",
|
||||||
"recommendations": "Recommendations",
|
"recommendations": "Recommendations",
|
||||||
|
"findingsList": {
|
||||||
|
"count": "{count} findings"
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"empty": "Click on a finding to preview details",
|
||||||
|
"emptyTitle": "Select a Finding",
|
||||||
|
"emptyTipSeverity": "Filter by severity",
|
||||||
|
"emptyTipFile": "Group by file",
|
||||||
|
"location": "Location",
|
||||||
|
"description": "Description",
|
||||||
|
"codeContext": "Code Context",
|
||||||
|
"recommendations": "Recommendations",
|
||||||
|
"rootCause": "Root Cause",
|
||||||
|
"impact": "Impact",
|
||||||
|
"selected": "Selected",
|
||||||
|
"selectForFix": "Select"
|
||||||
|
},
|
||||||
"fixProgress": {
|
"fixProgress": {
|
||||||
"title": "Fix Progress",
|
"title": "Fix Progress",
|
||||||
"phase": {
|
"phase": {
|
||||||
|
|||||||
@@ -65,6 +65,7 @@
|
|||||||
"inProgress": "进行中",
|
"inProgress": "进行中",
|
||||||
"running": "运行中",
|
"running": "运行中",
|
||||||
"initializing": "初始化中",
|
"initializing": "初始化中",
|
||||||
|
"initialized": "已初始化",
|
||||||
"planning": "规划中",
|
"planning": "规划中",
|
||||||
"completed": "已完成",
|
"completed": "已完成",
|
||||||
"failed": "失败",
|
"failed": "失败",
|
||||||
@@ -258,6 +259,71 @@
|
|||||||
"expandAria": "展开侧边栏"
|
"expandAria": "展开侧边栏"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"liteTasks": {
|
||||||
|
"title": "轻量任务",
|
||||||
|
"type": {
|
||||||
|
"plan": "轻量规划",
|
||||||
|
"fix": "轻量修复",
|
||||||
|
"multiCli": "多CLI规划"
|
||||||
|
},
|
||||||
|
"quickCards": {
|
||||||
|
"tasks": "任务",
|
||||||
|
"context": "上下文"
|
||||||
|
},
|
||||||
|
"multiCli": {
|
||||||
|
"discussion": "讨论",
|
||||||
|
"discussionRounds": "讨论轮次",
|
||||||
|
"discussionDescription": "多CLI协作规划,迭代分析与交叉验证",
|
||||||
|
"summary": "摘要",
|
||||||
|
"goal": "目标",
|
||||||
|
"solution": "解决方案",
|
||||||
|
"implementation": "实现方式",
|
||||||
|
"feasibility": "可行性",
|
||||||
|
"risk": "风险",
|
||||||
|
"planSummary": "规划摘要"
|
||||||
|
},
|
||||||
|
"createdAt": "创建时间",
|
||||||
|
"rounds": "轮",
|
||||||
|
"tasksCount": "个任务",
|
||||||
|
"untitled": "未命名任务",
|
||||||
|
"discussionTopic": "讨论主题",
|
||||||
|
"contextPanel": {
|
||||||
|
"loading": "加载上下文数据中...",
|
||||||
|
"error": "加载上下文失败",
|
||||||
|
"empty": "无可用上下文数据",
|
||||||
|
"explorations": "探索",
|
||||||
|
"explorationsCount": "{count} 个探索",
|
||||||
|
"diagnoses": "诊断",
|
||||||
|
"diagnosesCount": "{count} 个诊断",
|
||||||
|
"contextPackage": "上下文包",
|
||||||
|
"focusPaths": "关注路径",
|
||||||
|
"summary": "摘要",
|
||||||
|
"taskDescription": "任务描述",
|
||||||
|
"complexity": "复杂度"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"completed": "已完成",
|
||||||
|
"inProgress": "进行中",
|
||||||
|
"blocked": "已阻塞",
|
||||||
|
"pending": "待处理"
|
||||||
|
},
|
||||||
|
"subtitle": "{count} 个会话",
|
||||||
|
"empty": {
|
||||||
|
"title": "无 {type} 会话",
|
||||||
|
"message": "未找到该类型的会话"
|
||||||
|
},
|
||||||
|
"noResults": {
|
||||||
|
"title": "无结果",
|
||||||
|
"message": "没有符合搜索条件的会话"
|
||||||
|
},
|
||||||
|
"searchPlaceholder": "搜索会话...",
|
||||||
|
"sortBy": "排序方式",
|
||||||
|
"sort": {
|
||||||
|
"date": "日期",
|
||||||
|
"name": "名称",
|
||||||
|
"tasks": "任务"
|
||||||
|
}
|
||||||
|
},
|
||||||
"askQuestion": {
|
"askQuestion": {
|
||||||
"defaultTitle": "问题",
|
"defaultTitle": "问题",
|
||||||
"description": "请回答以下问题",
|
"description": "请回答以下问题",
|
||||||
|
|||||||
@@ -17,7 +17,8 @@
|
|||||||
"title": "架构",
|
"title": "架构",
|
||||||
"style": "架构风格",
|
"style": "架构风格",
|
||||||
"layers": "分层",
|
"layers": "分层",
|
||||||
"patterns": "设计模式"
|
"patterns": "设计模式",
|
||||||
|
"principles": "设计原则"
|
||||||
},
|
},
|
||||||
"components": {
|
"components": {
|
||||||
"title": "核心组件",
|
"title": "核心组件",
|
||||||
|
|||||||
@@ -5,12 +5,33 @@
|
|||||||
"critical": "严重",
|
"critical": "严重",
|
||||||
"high": "高",
|
"high": "高",
|
||||||
"medium": "中",
|
"medium": "中",
|
||||||
"low": "低"
|
"low": "低",
|
||||||
|
"short": {
|
||||||
|
"critical": "严重",
|
||||||
|
"high": "高",
|
||||||
|
"medium": "中",
|
||||||
|
"low": "低"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"total": "总发现",
|
"total": "总发现",
|
||||||
"dimensions": "维度"
|
"dimensions": "维度"
|
||||||
},
|
},
|
||||||
|
"progress": {
|
||||||
|
"title": "审查进度",
|
||||||
|
"totalFindings": "总发现",
|
||||||
|
"critical": "严重",
|
||||||
|
"high": "高"
|
||||||
|
},
|
||||||
|
"dimensionTabs": {
|
||||||
|
"all": "全部"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"severity": "严重程度",
|
||||||
|
"dimension": "维度",
|
||||||
|
"sort": "排序",
|
||||||
|
"reset": "重置"
|
||||||
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"placeholder": "搜索发现..."
|
"placeholder": "搜索发现..."
|
||||||
},
|
},
|
||||||
@@ -21,18 +42,59 @@
|
|||||||
},
|
},
|
||||||
"selection": {
|
"selection": {
|
||||||
"count": "已选择 {count} 项",
|
"count": "已选择 {count} 项",
|
||||||
|
"countSelected": "已选 {count} 项",
|
||||||
|
"total": "共 {count} 项",
|
||||||
"selectAll": "全选",
|
"selectAll": "全选",
|
||||||
"clearAll": "清除全部",
|
"clearAll": "清除全部",
|
||||||
"clear": "清除"
|
"clear": "清除",
|
||||||
|
"selectVisible": "可见",
|
||||||
|
"selectCritical": "严重"
|
||||||
},
|
},
|
||||||
"export": "导出修复 JSON",
|
"export": "导出修复 JSON",
|
||||||
"codeContext": "代码上下文",
|
"codeContext": "代码上下文",
|
||||||
"rootCause": "根本原因",
|
"rootCause": "根本原因",
|
||||||
"impact": "影响",
|
"impact": "影响",
|
||||||
"recommendations": "建议",
|
"recommendations": "建议",
|
||||||
|
"findingsList": {
|
||||||
|
"count": "{count} 条发现"
|
||||||
|
},
|
||||||
|
"preview": {
|
||||||
|
"empty": "点击发现以预览详情",
|
||||||
|
"emptyTitle": "选择一个发现",
|
||||||
|
"emptyTipSeverity": "按严重程度筛选",
|
||||||
|
"emptyTipFile": "按文件分组",
|
||||||
|
"location": "位置",
|
||||||
|
"description": "描述",
|
||||||
|
"codeContext": "代码上下文",
|
||||||
|
"recommendations": "建议",
|
||||||
|
"rootCause": "根本原因",
|
||||||
|
"impact": "影响",
|
||||||
|
"selected": "已选择",
|
||||||
|
"selectForFix": "选择"
|
||||||
|
},
|
||||||
|
"fixProgress": {
|
||||||
|
"title": "修复进度",
|
||||||
|
"phase": {
|
||||||
|
"planning": "规划",
|
||||||
|
"execution": "执行",
|
||||||
|
"completion": "完成"
|
||||||
|
},
|
||||||
|
"stats": {
|
||||||
|
"total": "总数",
|
||||||
|
"fixed": "已修复",
|
||||||
|
"failed": "失败",
|
||||||
|
"pending": "待处理"
|
||||||
|
},
|
||||||
|
"activeAgents": "活跃代理",
|
||||||
|
"activeAgentsPlural": "活跃代理",
|
||||||
|
"stage": "阶段",
|
||||||
|
"complete": "完成 {percent}%",
|
||||||
|
"working": "工作中..."
|
||||||
|
},
|
||||||
"empty": {
|
"empty": {
|
||||||
"title": "未找到发现",
|
"title": "未找到发现",
|
||||||
"message": "尝试调整筛选条件或搜索查询。"
|
"message": "尝试调整筛选条件或搜索查询。",
|
||||||
|
"noFixProgress": "无修复进度数据"
|
||||||
},
|
},
|
||||||
"notFound": {
|
"notFound": {
|
||||||
"title": "未找到审查会话",
|
"title": "未找到审查会话",
|
||||||
|
|||||||
@@ -428,49 +428,34 @@ export function LiteTaskDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right: Meta Information */}
|
{/* Right: Meta Information */}
|
||||||
<div className="flex flex-col items-end gap-2 text-xs text-muted-foreground flex-shrink-0">
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
{/* Row 1: Status Badge */}
|
{/* Dependencies - show task IDs */}
|
||||||
<Badge
|
{task.context?.depends_on && task.context.depends_on.length > 0 && (
|
||||||
variant={task.status === 'completed' ? 'success' : task.status === 'in_progress' ? 'warning' : task.status === 'blocked' ? 'destructive' : 'secondary'}
|
<div className="flex items-center gap-1">
|
||||||
className="w-fit"
|
<span className="text-xs text-muted-foreground">→</span>
|
||||||
>
|
{task.context.depends_on.map((depId, idx) => (
|
||||||
{task.status}
|
<Badge key={idx} variant="outline" className="h-6 px-2 py-0.5 text-xs font-mono border-primary/30 text-primary">
|
||||||
</Badge>
|
{depId}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Row 2: Metadata */}
|
{/* Target Files Count */}
|
||||||
<div className="flex items-center gap-3 flex-wrap justify-end">
|
{task.flow_control?.target_files && task.flow_control.target_files.length > 0 && (
|
||||||
{/* Dependencies Count */}
|
<Badge variant="secondary" className="h-5 px-1.5 py-0 text-[10px] gap-0.5">
|
||||||
{task.context?.depends_on && task.context.depends_on.length > 0 && (
|
<span className="font-semibold">{task.flow_control.target_files.length}</span>
|
||||||
<span className="flex items-center gap-1 px-2 py-0.5 rounded bg-muted/50">
|
<span>file{task.flow_control.target_files.length > 1 ? 's' : ''}</span>
|
||||||
<span className="font-mono font-semibold text-foreground">{task.context.depends_on.length}</span>
|
</Badge>
|
||||||
<span>dep{task.context.depends_on.length > 1 ? 's' : ''}</span>
|
)}
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Target Files Count */}
|
{/* Implementation Steps Count */}
|
||||||
{task.flow_control?.target_files && task.flow_control.target_files.length > 0 && (
|
{task.flow_control?.implementation_approach && task.flow_control.implementation_approach.length > 0 && (
|
||||||
<span className="flex items-center gap-1 px-2 py-0.5 rounded bg-muted/50">
|
<Badge variant="secondary" className="h-5 px-1.5 py-0 text-[10px] gap-0.5">
|
||||||
<span className="font-mono font-semibold text-foreground">{task.flow_control.target_files.length}</span>
|
<span className="font-semibold">{task.flow_control.implementation_approach.length}</span>
|
||||||
<span>file{task.flow_control.target_files.length > 1 ? 's' : ''}</span>
|
<span>step{task.flow_control.implementation_approach.length > 1 ? 's' : ''}</span>
|
||||||
</span>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Focus Paths Count */}
|
|
||||||
{task.context?.focus_paths && task.context.focus_paths.length > 0 && (
|
|
||||||
<span className="flex items-center gap-1 px-2 py-0.5 rounded bg-muted/50">
|
|
||||||
<span className="font-mono font-semibold text-foreground">{task.context.focus_paths.length}</span>
|
|
||||||
<span>focus</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Acceptance Criteria Count */}
|
|
||||||
{task.context?.acceptance && task.context.acceptance.length > 0 && (
|
|
||||||
<span className="flex items-center gap-1 px-2 py-0.5 rounded bg-muted/50">
|
|
||||||
<span className="font-mono font-semibold text-foreground">{task.context.acceptance.length}</span>
|
|
||||||
<span>criteria</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ import {
|
|||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Clock,
|
Clock,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
|
Target,
|
||||||
|
FileCode,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useLiteTasks } from '@/hooks/useLiteTasks';
|
import { useLiteTasks } from '@/hooks/useLiteTasks';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
@@ -146,25 +148,36 @@ function ExpandedSessionPanel({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CardContent className="p-3">
|
<CardContent className="p-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<Badge className="text-xs font-mono shrink-0 bg-primary/10 text-primary border-primary/20">
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
{task.task_id || `#${index + 1}`}
|
<Badge className="text-xs font-mono shrink-0 bg-primary/10 text-primary border-primary/20">
|
||||||
</Badge>
|
{task.task_id || `#${index + 1}`}
|
||||||
<h4 className="text-sm font-medium text-foreground flex-1 line-clamp-1">
|
|
||||||
{task.title || formatMessage({ id: 'liteTasks.untitled' })}
|
|
||||||
</h4>
|
|
||||||
{task.status && (
|
|
||||||
<Badge
|
|
||||||
variant={
|
|
||||||
task.status === 'completed' ? 'success' :
|
|
||||||
task.status === 'in_progress' ? 'warning' :
|
|
||||||
task.status === 'blocked' ? 'destructive' : 'secondary'
|
|
||||||
}
|
|
||||||
className="text-[10px]"
|
|
||||||
>
|
|
||||||
{task.status}
|
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
<h4 className="text-sm font-medium text-foreground flex-1 line-clamp-1">
|
||||||
|
{task.title || formatMessage({ id: 'liteTasks.untitled' })}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
{/* Right: Meta info */}
|
||||||
|
<div className="flex items-center gap-2 flex-shrink-0">
|
||||||
|
{/* Dependencies - show task IDs */}
|
||||||
|
{task.context?.depends_on && task.context.depends_on.length > 0 && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<span className="text-xs text-muted-foreground">→</span>
|
||||||
|
{task.context.depends_on.map((depId, idx) => (
|
||||||
|
<Badge key={idx} variant="outline" className="h-5 px-2 py-0.5 text-xs font-mono border-primary/30 text-primary">
|
||||||
|
{depId}
|
||||||
|
</Badge>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{/* Target Files Count */}
|
||||||
|
{task.flow_control?.target_files && task.flow_control.target_files.length > 0 && (
|
||||||
|
<Badge variant="secondary" className="h-4 px-1.5 py-0 text-[10px] gap-0.5">
|
||||||
|
<span className="font-semibold">{task.flow_control.target_files.length}</span>
|
||||||
|
<span>file{task.flow_control.target_files.length > 1 ? 's' : ''}</span>
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{task.description && (
|
{task.description && (
|
||||||
<p className="text-xs text-muted-foreground mt-1.5 pl-[calc(1.5rem+0.75rem)] line-clamp-2">
|
<p className="text-xs text-muted-foreground mt-1.5 pl-[calc(1.5rem+0.75rem)] line-clamp-2">
|
||||||
@@ -392,6 +405,407 @@ function ContextSection({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MultiCliExpandedTab = 'tasks' | 'discussion' | 'context' | 'summary';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ExpandedMultiCliPanel - Multi-tab panel shown when a multi-cli session is expanded
|
||||||
|
*/
|
||||||
|
function ExpandedMultiCliPanel({
|
||||||
|
session,
|
||||||
|
onTaskClick,
|
||||||
|
}: {
|
||||||
|
session: LiteTaskSession;
|
||||||
|
onTaskClick: (task: LiteTask) => void;
|
||||||
|
}) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
const [activeTab, setActiveTab] = React.useState<MultiCliExpandedTab>('tasks');
|
||||||
|
const [contextData, setContextData] = React.useState<LiteSessionContext | null>(null);
|
||||||
|
const [contextLoading, setContextLoading] = React.useState(false);
|
||||||
|
const [contextError, setContextError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
|
const tasks = session.tasks || [];
|
||||||
|
const taskCount = tasks.length;
|
||||||
|
const synthesis = session.latestSynthesis || {};
|
||||||
|
const plan = session.plan || {};
|
||||||
|
const roundCount = session.roundCount || (session.metadata?.roundId as number) || 1;
|
||||||
|
|
||||||
|
// Get i18n text helper
|
||||||
|
const getI18nTextLocal = (text: string | { en?: string; zh?: string } | undefined): string => {
|
||||||
|
if (!text) return '';
|
||||||
|
if (typeof text === 'string') return text;
|
||||||
|
return text.en || text.zh || '';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build implementation chain from task dependencies
|
||||||
|
const buildImplementationChain = (): string => {
|
||||||
|
if (tasks.length === 0) return '';
|
||||||
|
|
||||||
|
// Find tasks with no dependencies (starting tasks)
|
||||||
|
const taskDeps: Record<string, string[]> = {};
|
||||||
|
const taskIds = new Set<string>();
|
||||||
|
|
||||||
|
tasks.forEach(t => {
|
||||||
|
const id = t.task_id || t.id;
|
||||||
|
taskIds.add(id);
|
||||||
|
taskDeps[id] = t.context?.depends_on || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Find starting tasks (no deps or deps not in task list)
|
||||||
|
const startingTasks = tasks.filter(t => {
|
||||||
|
const deps = t.context?.depends_on || [];
|
||||||
|
return deps.length === 0 || deps.every(d => !taskIds.has(d));
|
||||||
|
}).map(t => t.task_id || t.id);
|
||||||
|
|
||||||
|
// Group parallel tasks
|
||||||
|
const parallelStart = startingTasks.length > 1
|
||||||
|
? `(${startingTasks.join(' | ')})`
|
||||||
|
: startingTasks[0] || '';
|
||||||
|
|
||||||
|
// Find subsequent tasks in order
|
||||||
|
const processed = new Set(startingTasks);
|
||||||
|
const chain: string[] = [parallelStart];
|
||||||
|
|
||||||
|
let iterations = 0;
|
||||||
|
while (processed.size < tasks.length && iterations < 20) {
|
||||||
|
iterations++;
|
||||||
|
const nextBatch: string[] = [];
|
||||||
|
|
||||||
|
tasks.forEach(t => {
|
||||||
|
const id = t.task_id || t.id;
|
||||||
|
if (processed.has(id)) return;
|
||||||
|
|
||||||
|
const deps = t.context?.depends_on || [];
|
||||||
|
if (deps.every(d => processed.has(d) || !taskIds.has(d))) {
|
||||||
|
nextBatch.push(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (nextBatch.length === 0) break;
|
||||||
|
|
||||||
|
nextBatch.forEach(id => processed.add(id));
|
||||||
|
if (nextBatch.length > 1) {
|
||||||
|
chain.push(`(${nextBatch.join(' | ')})`);
|
||||||
|
} else {
|
||||||
|
chain.push(nextBatch[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return chain.filter(Boolean).join(' → ');
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load context data lazily
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (activeTab !== 'context') return;
|
||||||
|
if (contextData || contextLoading) return;
|
||||||
|
if (!session.path) {
|
||||||
|
setContextError('No session path available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setContextLoading(true);
|
||||||
|
fetchLiteSessionContext(session.path)
|
||||||
|
.then((data) => {
|
||||||
|
setContextData(data);
|
||||||
|
setContextError(null);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
setContextError(err.message || 'Failed to load context');
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setContextLoading(false);
|
||||||
|
});
|
||||||
|
}, [activeTab, session.path, contextData, contextLoading]);
|
||||||
|
|
||||||
|
const implementationChain = buildImplementationChain();
|
||||||
|
const goal = getI18nTextLocal(plan.goal as string | { en?: string; zh?: string }) ||
|
||||||
|
getI18nTextLocal(synthesis.title as string | { en?: string; zh?: string }) || '';
|
||||||
|
const solution = getI18nTextLocal(plan.solution as string | { en?: string; zh?: string }) || '';
|
||||||
|
const feasibility = (plan.feasibility as number) || 0;
|
||||||
|
const effort = (plan.effort as string) || '';
|
||||||
|
const risk = (plan.risk as string) || '';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mt-2 ml-6 pb-2">
|
||||||
|
{/* Session Info Header */}
|
||||||
|
<div className="flex items-center gap-4 text-xs text-muted-foreground mb-3 pb-2 border-b border-border/50">
|
||||||
|
{session.createdAt && (
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Calendar className="h-3.5 w-3.5" />
|
||||||
|
{formatMessage({ id: 'liteTasks.createdAt' })}: {new Date(session.createdAt).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<ListChecks className="h-3.5 w-3.5" />
|
||||||
|
{formatMessage({ id: 'liteTasks.quickCards.tasks' })}: {taskCount} {formatMessage({ id: 'liteTasks.tasksCount' })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tab Buttons */}
|
||||||
|
<div className="flex flex-wrap gap-2 mb-3">
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); setActiveTab('tasks'); }}
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
|
||||||
|
activeTab === 'tasks'
|
||||||
|
? 'bg-primary/10 text-primary border-primary/30'
|
||||||
|
: 'bg-muted/50 text-muted-foreground border-border hover:bg-muted'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ListChecks className="h-3.5 w-3.5" />
|
||||||
|
{formatMessage({ id: 'liteTasks.quickCards.tasks' })}
|
||||||
|
<Badge variant="secondary" className="ml-1 text-[10px] px-1.5 py-0">
|
||||||
|
{taskCount}
|
||||||
|
</Badge>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); setActiveTab('discussion'); }}
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
|
||||||
|
activeTab === 'discussion'
|
||||||
|
? 'bg-primary/10 text-primary border-primary/30'
|
||||||
|
: 'bg-muted/50 text-muted-foreground border-border hover:bg-muted'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<MessagesSquare className="h-3.5 w-3.5" />
|
||||||
|
{formatMessage({ id: 'liteTasks.multiCli.discussion' })}
|
||||||
|
<Badge variant="secondary" className="ml-1 text-[10px] px-1.5 py-0">
|
||||||
|
{roundCount}
|
||||||
|
</Badge>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); setActiveTab('context'); }}
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
|
||||||
|
activeTab === 'context'
|
||||||
|
? 'bg-primary/10 text-primary border-primary/30'
|
||||||
|
: 'bg-muted/50 text-muted-foreground border-border hover:bg-muted'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Package className="h-3.5 w-3.5" />
|
||||||
|
{formatMessage({ id: 'liteTasks.quickCards.context' })}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); setActiveTab('summary'); }}
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-xs font-medium border transition-colors ${
|
||||||
|
activeTab === 'summary'
|
||||||
|
? 'bg-primary/10 text-primary border-primary/30'
|
||||||
|
: 'bg-muted/50 text-muted-foreground border-border hover:bg-muted'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FileText className="h-3.5 w-3.5" />
|
||||||
|
{formatMessage({ id: 'liteTasks.multiCli.summary' })}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tasks Tab */}
|
||||||
|
{activeTab === 'tasks' && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{/* Goal/Solution/Implementation Header */}
|
||||||
|
{(goal || solution || implementationChain) && (
|
||||||
|
<Card className="border-border bg-muted/30">
|
||||||
|
<CardContent className="p-3 space-y-2">
|
||||||
|
{goal && (
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="text-muted-foreground">{formatMessage({ id: 'liteTasks.multiCli.goal' })}:</span>
|
||||||
|
<span className="ml-2 text-foreground">{goal}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{solution && (
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="text-muted-foreground">{formatMessage({ id: 'liteTasks.multiCli.solution' })}:</span>
|
||||||
|
<span className="ml-2 text-foreground">{solution}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{implementationChain && (
|
||||||
|
<div className="text-sm">
|
||||||
|
<span className="text-muted-foreground">{formatMessage({ id: 'liteTasks.multiCli.implementation' })}:</span>
|
||||||
|
<code className="ml-2 px-2 py-0.5 rounded bg-background border border-border text-xs font-mono">
|
||||||
|
{implementationChain}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(feasibility > 0 || effort || risk) && (
|
||||||
|
<div className="flex items-center gap-2 pt-1">
|
||||||
|
{feasibility > 0 && (
|
||||||
|
<Badge variant="success" className="text-[10px]">{feasibility}%</Badge>
|
||||||
|
)}
|
||||||
|
{effort && (
|
||||||
|
<Badge variant="warning" className="text-[10px]">{effort}</Badge>
|
||||||
|
)}
|
||||||
|
{risk && (
|
||||||
|
<Badge variant={risk === 'high' ? 'destructive' : risk === 'medium' ? 'warning' : 'success'} className="text-[10px]">
|
||||||
|
{risk} {formatMessage({ id: 'liteTasks.multiCli.risk' })}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Task List */}
|
||||||
|
{tasks.map((task, index) => {
|
||||||
|
const filesCount = task.flow_control?.target_files?.length || 0;
|
||||||
|
const stepsCount = task.flow_control?.implementation_approach?.length || 0;
|
||||||
|
const criteriaCount = task.context?.acceptance?.length || 0;
|
||||||
|
const depsCount = task.context?.depends_on?.length || 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={task.id || index}
|
||||||
|
className="cursor-pointer hover:shadow-sm hover:border-primary/50 transition-all border-border border-l-4 border-l-primary/50"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onTaskClick(task);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent className="p-3">
|
||||||
|
<div className="flex items-start justify-between gap-3">
|
||||||
|
<div className="flex items-start gap-3 flex-1 min-w-0">
|
||||||
|
<Badge className="text-xs font-mono shrink-0 bg-primary/10 text-primary border-primary/20">
|
||||||
|
{task.task_id || `T${index + 1}`}
|
||||||
|
</Badge>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h4 className="text-sm font-medium text-foreground line-clamp-1">
|
||||||
|
{task.title || formatMessage({ id: 'liteTasks.untitled' })}
|
||||||
|
</h4>
|
||||||
|
{/* Meta badges */}
|
||||||
|
<div className="flex flex-wrap items-center gap-1.5 mt-1.5">
|
||||||
|
{task.meta?.type && (
|
||||||
|
<Badge variant="info" className="text-[10px] px-1.5 py-0">{task.meta.type}</Badge>
|
||||||
|
)}
|
||||||
|
{filesCount > 0 && (
|
||||||
|
<Badge variant="secondary" className="text-[10px] px-1.5 py-0 gap-0.5">
|
||||||
|
<FileCode className="h-2.5 w-2.5" />
|
||||||
|
{filesCount} files
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{stepsCount > 0 && (
|
||||||
|
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
|
||||||
|
{stepsCount} steps
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{criteriaCount > 0 && (
|
||||||
|
<Badge variant="secondary" className="text-[10px] px-1.5 py-0">
|
||||||
|
{criteriaCount} criteria
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{depsCount > 0 && (
|
||||||
|
<Badge variant="outline" className="text-[10px] px-1.5 py-0 border-primary/30 text-primary">
|
||||||
|
{depsCount} deps
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Discussion Tab */}
|
||||||
|
{activeTab === 'discussion' && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Card className="border-border">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<MessagesSquare className="h-5 w-5 text-primary" />
|
||||||
|
<h4 className="font-medium text-foreground">
|
||||||
|
{formatMessage({ id: 'liteTasks.multiCli.discussionRounds' })}
|
||||||
|
</h4>
|
||||||
|
<Badge variant="secondary" className="text-xs">{roundCount} {formatMessage({ id: 'liteTasks.rounds' })}</Badge>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{formatMessage({ id: 'liteTasks.multiCli.discussionDescription' })}
|
||||||
|
</p>
|
||||||
|
{goal && (
|
||||||
|
<div className="mt-3 p-3 bg-muted/50 rounded-lg">
|
||||||
|
<p className="text-sm text-foreground">{goal}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Context Tab */}
|
||||||
|
{activeTab === 'context' && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
{contextLoading && (
|
||||||
|
<div className="flex items-center justify-center py-8 text-muted-foreground">
|
||||||
|
<Loader2 className="h-5 w-5 animate-spin mr-2" />
|
||||||
|
<span className="text-sm">{formatMessage({ id: 'liteTasks.contextPanel.loading' })}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{contextError && !contextLoading && (
|
||||||
|
<div className="flex items-center gap-2 p-3 rounded-lg bg-destructive/10 border border-destructive/30 text-destructive text-sm">
|
||||||
|
<XCircle className="h-4 w-4 flex-shrink-0" />
|
||||||
|
{formatMessage({ id: 'liteTasks.contextPanel.error' })}: {contextError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!contextLoading && !contextError && contextData && (
|
||||||
|
<ContextContent contextData={contextData} session={session} />
|
||||||
|
)}
|
||||||
|
{!contextLoading && !contextError && !contextData && !session.path && (
|
||||||
|
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||||
|
<Package className="h-8 w-8 text-muted-foreground mb-2" />
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{formatMessage({ id: 'liteTasks.contextPanel.empty' })}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Summary Tab */}
|
||||||
|
{activeTab === 'summary' && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Card className="border-border">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Target className="h-5 w-5 text-primary" />
|
||||||
|
<h4 className="font-medium text-foreground">
|
||||||
|
{formatMessage({ id: 'liteTasks.multiCli.planSummary' })}
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
{goal && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'liteTasks.multiCli.goal' })}</p>
|
||||||
|
<p className="text-sm text-foreground">{goal}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{solution && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'liteTasks.multiCli.solution' })}</p>
|
||||||
|
<p className="text-sm text-foreground">{solution}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{implementationChain && (
|
||||||
|
<div className="mb-3">
|
||||||
|
<p className="text-xs text-muted-foreground mb-1">{formatMessage({ id: 'liteTasks.multiCli.implementation' })}</p>
|
||||||
|
<code className="block px-3 py-2 rounded bg-muted border border-border text-xs font-mono">
|
||||||
|
{implementationChain}
|
||||||
|
</code>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center gap-2 pt-2 border-t border-border/50">
|
||||||
|
<span className="text-xs text-muted-foreground">{formatMessage({ id: 'liteTasks.quickCards.tasks' })}:</span>
|
||||||
|
<Badge variant="secondary" className="text-xs">{taskCount}</Badge>
|
||||||
|
{feasibility > 0 && (
|
||||||
|
<>
|
||||||
|
<span className="text-xs text-muted-foreground ml-2">{formatMessage({ id: 'liteTasks.multiCli.feasibility' })}:</span>
|
||||||
|
<Badge variant="success" className="text-xs">{feasibility}%</Badge>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* LiteTasksPage component - Display lite-plan and lite-fix sessions with expandable tasks
|
* LiteTasksPage component - Display lite-plan and lite-fix sessions with expandable tasks
|
||||||
*/
|
*/
|
||||||
@@ -486,18 +900,15 @@ export function LiteTasksPage() {
|
|||||||
const taskCount = session.tasks?.length || 0;
|
const taskCount = session.tasks?.length || 0;
|
||||||
const isExpanded = expandedSessionId === session.id;
|
const isExpanded = expandedSessionId === session.id;
|
||||||
|
|
||||||
// Calculate task status distribution
|
// Calculate task status distribution (no useMemo - this is a render function, not a component)
|
||||||
const taskStats = React.useMemo(() => {
|
const tasks = session.tasks || [];
|
||||||
const tasks = session.tasks || [];
|
const taskStats = {
|
||||||
return {
|
completed: tasks.filter((t) => t.status === 'completed').length,
|
||||||
completed: tasks.filter((t) => t.status === 'completed').length,
|
inProgress: tasks.filter((t) => t.status === 'in_progress').length,
|
||||||
inProgress: tasks.filter((t) => t.status === 'in_progress').length,
|
blocked: tasks.filter((t) => t.status === 'blocked').length,
|
||||||
blocked: tasks.filter((t) => t.status === 'blocked').length,
|
};
|
||||||
pending: tasks.filter((t) => !t.status || t.status === 'pending').length,
|
|
||||||
};
|
|
||||||
}, [session.tasks]);
|
|
||||||
|
|
||||||
const firstTask = session.tasks?.[0];
|
const firstTask = tasks[0];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={session.id}>
|
<div key={session.id}>
|
||||||
@@ -552,12 +963,6 @@ export function LiteTasksPage() {
|
|||||||
{taskStats.blocked} {formatMessage({ id: 'liteTasks.status.blocked' })}
|
{taskStats.blocked} {formatMessage({ id: 'liteTasks.status.blocked' })}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{taskStats.pending > 0 && (
|
|
||||||
<Badge variant="secondary" className="gap-1 text-xs">
|
|
||||||
<Activity className="h-3 w-3" />
|
|
||||||
{taskStats.pending} {formatMessage({ id: 'liteTasks.status.pending' })}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Date and task count */}
|
{/* Date and task count */}
|
||||||
@@ -600,96 +1005,98 @@ export function LiteTasksPage() {
|
|||||||
const status = latestSynthesis.status || session.status || 'analyzing';
|
const status = latestSynthesis.status || session.status || 'analyzing';
|
||||||
const createdAt = (metadata.timestamp as string) || session.createdAt || '';
|
const createdAt = (metadata.timestamp as string) || session.createdAt || '';
|
||||||
|
|
||||||
// Calculate task status distribution
|
// Calculate task status distribution (no useMemo - this is a render function, not a component)
|
||||||
const taskStats = React.useMemo(() => {
|
const tasks = session.tasks || [];
|
||||||
const tasks = session.tasks || [];
|
const taskStats = {
|
||||||
return {
|
completed: tasks.filter((t) => t.status === 'completed').length,
|
||||||
completed: tasks.filter((t) => t.status === 'completed').length,
|
inProgress: tasks.filter((t) => t.status === 'in_progress').length,
|
||||||
inProgress: tasks.filter((t) => t.status === 'in_progress').length,
|
blocked: tasks.filter((t) => t.status === 'blocked').length,
|
||||||
blocked: tasks.filter((t) => t.status === 'blocked').length,
|
total: tasks.length,
|
||||||
pending: tasks.filter((t) => !t.status || t.status === 'pending').length,
|
};
|
||||||
total: tasks.length,
|
|
||||||
};
|
const isExpanded = expandedSessionId === session.id;
|
||||||
}, [session.tasks]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<div key={session.id}>
|
||||||
key={session.id}
|
<Card
|
||||||
className="cursor-pointer hover:shadow-md transition-shadow"
|
className="cursor-pointer hover:shadow-md transition-shadow"
|
||||||
onClick={() => setExpandedSessionId(expandedSessionId === session.id ? null : session.id)}
|
onClick={() => setExpandedSessionId(isExpanded ? null : session.id)}
|
||||||
>
|
>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-4">
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
{expandedSessionId === session.id ? (
|
{isExpanded ? (
|
||||||
<ChevronDown className="h-5 w-5 text-muted-foreground" />
|
<ChevronDown className="h-5 w-5 text-muted-foreground" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight className="h-5 w-5 text-muted-foreground" />
|
<ChevronRight className="h-5 w-5 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<h3 className="font-bold text-foreground text-sm tracking-wide uppercase">{session.id}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Badge variant="secondary" className="gap-1 flex-shrink-0">
|
||||||
|
<MessagesSquare className="h-3 w-3" />
|
||||||
|
{formatMessage({ id: 'liteTasks.type.multiCli' })}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-3">
|
||||||
|
<MessageCircle className="h-4 w-4" />
|
||||||
|
<span className="line-clamp-1">{topicTitle}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Task status distribution for multi-cli */}
|
||||||
|
{taskStats.total > 0 && (
|
||||||
|
<div className="flex items-center flex-wrap gap-2 mb-3">
|
||||||
|
{taskStats.completed > 0 && (
|
||||||
|
<Badge variant="success" className="gap-1 text-xs">
|
||||||
|
<CheckCircle2 className="h-3 w-3" />
|
||||||
|
{taskStats.completed} {formatMessage({ id: 'liteTasks.status.completed' })}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{taskStats.inProgress > 0 && (
|
||||||
|
<Badge variant="warning" className="gap-1 text-xs">
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
{taskStats.inProgress} {formatMessage({ id: 'liteTasks.status.inProgress' })}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
{taskStats.blocked > 0 && (
|
||||||
|
<Badge variant="destructive" className="gap-1 text-xs">
|
||||||
|
<AlertCircle className="h-3 w-3" />
|
||||||
|
{taskStats.blocked} {formatMessage({ id: 'liteTasks.status.blocked' })}
|
||||||
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<h3 className="font-bold text-foreground text-sm tracking-wide uppercase">{session.id}</h3>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Badge variant="secondary" className="gap-1 flex-shrink-0">
|
|
||||||
<MessagesSquare className="h-3 w-3" />
|
|
||||||
{formatMessage({ id: 'liteTasks.type.multiCli' })}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-1 text-xs text-muted-foreground mb-3">
|
|
||||||
<MessageCircle className="h-4 w-4" />
|
|
||||||
<span className="line-clamp-1">{topicTitle}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Task status distribution for multi-cli */}
|
|
||||||
{taskStats.total > 0 && (
|
|
||||||
<div className="flex items-center flex-wrap gap-2 mb-3">
|
|
||||||
{taskStats.completed > 0 && (
|
|
||||||
<Badge variant="success" className="gap-1 text-xs">
|
|
||||||
<CheckCircle2 className="h-3 w-3" />
|
|
||||||
{taskStats.completed} {formatMessage({ id: 'liteTasks.status.completed' })}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{taskStats.inProgress > 0 && (
|
|
||||||
<Badge variant="warning" className="gap-1 text-xs">
|
|
||||||
<Clock className="h-3 w-3" />
|
|
||||||
{taskStats.inProgress} {formatMessage({ id: 'liteTasks.status.inProgress' })}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{taskStats.blocked > 0 && (
|
|
||||||
<Badge variant="destructive" className="gap-1 text-xs">
|
|
||||||
<AlertCircle className="h-3 w-3" />
|
|
||||||
{taskStats.blocked} {formatMessage({ id: 'liteTasks.status.blocked' })}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{taskStats.pending > 0 && (
|
|
||||||
<Badge variant="secondary" className="gap-1 text-xs">
|
|
||||||
<Activity className="h-3 w-3" />
|
|
||||||
{taskStats.pending} {formatMessage({ id: 'liteTasks.status.pending' })}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
|
||||||
{createdAt && (
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Calendar className="h-3.5 w-3.5" />
|
|
||||||
{new Date(createdAt).toLocaleDateString()}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Repeat className="h-3.5 w-3.5" />
|
<div className="flex items-center gap-3 text-xs text-muted-foreground">
|
||||||
{roundCount} {formatMessage({ id: 'liteTasks.rounds' })}
|
{createdAt && (
|
||||||
</span>
|
<span className="flex items-center gap-1">
|
||||||
<Badge variant={getStatusColor(status) as 'success' | 'info' | 'warning' | 'destructive' | 'secondary'} className="gap-1">
|
<Calendar className="h-3.5 w-3.5" />
|
||||||
<Activity className="h-3 w-3" />
|
{new Date(createdAt).toLocaleDateString()}
|
||||||
{status}
|
</span>
|
||||||
</Badge>
|
)}
|
||||||
</div>
|
<span className="flex items-center gap-1">
|
||||||
</CardContent>
|
<Repeat className="h-3.5 w-3.5" />
|
||||||
</Card>
|
{roundCount} {formatMessage({ id: 'liteTasks.rounds' })}
|
||||||
|
</span>
|
||||||
|
<Badge variant={getStatusColor(status) as 'success' | 'info' | 'warning' | 'destructive' | 'secondary'} className="gap-1">
|
||||||
|
<Activity className="h-3 w-3" />
|
||||||
|
{status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Expanded multi-cli panel with tabs */}
|
||||||
|
{isExpanded && (
|
||||||
|
<ExpandedMultiCliPanel
|
||||||
|
session={session}
|
||||||
|
onTaskClick={setSelectedTask}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -649,26 +649,26 @@ export function ProjectOverviewPage() {
|
|||||||
{/* Guidelines */}
|
{/* Guidelines */}
|
||||||
{guidelines && (
|
{guidelines && (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4">
|
<CardContent className="p-6">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<h3 className="text-sm font-semibold text-foreground flex items-center gap-1.5">
|
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
||||||
<ScrollText className="w-4 h-4" />
|
<ScrollText className="w-5 h-5" />
|
||||||
{formatMessage({ id: 'projectOverview.guidelines.title' })}
|
{formatMessage({ id: 'projectOverview.guidelines.title' })}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex gap-1.5">
|
<div className="flex gap-2">
|
||||||
{!isEditMode ? (
|
{!isEditMode ? (
|
||||||
<Button variant="outline" size="sm" className="h-7 text-xs px-2" onClick={handleEditStart}>
|
<Button variant="outline" size="sm" onClick={handleEditStart}>
|
||||||
<Edit className="w-3 h-3 mr-1" />
|
<Edit className="w-4 h-4 mr-1" />
|
||||||
{formatMessage({ id: 'projectOverview.guidelines.edit' })}
|
{formatMessage({ id: 'projectOverview.guidelines.edit' })}
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Button variant="outline" size="sm" className="h-7 text-xs px-2" onClick={handleEditCancel} disabled={isUpdating}>
|
<Button variant="outline" size="sm" onClick={handleEditCancel} disabled={isUpdating}>
|
||||||
<X className="w-3 h-3 mr-1" />
|
<X className="w-4 h-4 mr-1" />
|
||||||
{formatMessage({ id: 'projectOverview.guidelines.cancel' })}
|
{formatMessage({ id: 'projectOverview.guidelines.cancel' })}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="default" size="sm" className="h-7 text-xs px-2" onClick={handleSave} disabled={isUpdating}>
|
<Button variant="default" size="sm" onClick={handleSave} disabled={isUpdating}>
|
||||||
<Save className="w-3 h-3 mr-1" />
|
<Save className="w-4 h-4 mr-1" />
|
||||||
{isUpdating ? formatMessage({ id: 'projectOverview.guidelines.saving' }) : formatMessage({ id: 'projectOverview.guidelines.save' })}
|
{isUpdating ? formatMessage({ id: 'projectOverview.guidelines.saving' }) : formatMessage({ id: 'projectOverview.guidelines.save' })}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
@@ -676,17 +676,17 @@ export function ProjectOverviewPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-6">
|
||||||
{!isEditMode ? (
|
{!isEditMode ? (
|
||||||
<>
|
<>
|
||||||
{/* Read-only Mode - Conventions */}
|
{/* Read-only Mode - Conventions */}
|
||||||
{guidelines.conventions && (
|
{guidelines.conventions && (
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-xs font-semibold text-foreground mb-2 flex items-center gap-1.5">
|
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||||
<BookMarked className="w-3.5 h-3.5" />
|
<BookMarked className="w-4 h-4" />
|
||||||
<span>{formatMessage({ id: 'projectOverview.guidelines.conventions' })}</span>
|
<span>{formatMessage({ id: 'projectOverview.guidelines.conventions' })}</span>
|
||||||
</h4>
|
</h4>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-2">
|
||||||
{Object.entries(guidelines.conventions).map(([key, items]) => {
|
{Object.entries(guidelines.conventions).map(([key, items]) => {
|
||||||
const itemList = Array.isArray(items) ? items : [];
|
const itemList = Array.isArray(items) ? items : [];
|
||||||
if (itemList.length === 0) return null;
|
if (itemList.length === 0) return null;
|
||||||
@@ -695,12 +695,12 @@ export function ProjectOverviewPage() {
|
|||||||
{itemList.map((item: string, i: number) => (
|
{itemList.map((item: string, i: number) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="flex items-start gap-2 p-2 bg-background border border-border rounded"
|
className="flex items-start gap-3 p-3 bg-background border border-border rounded-lg"
|
||||||
>
|
>
|
||||||
<span className="text-[10px] px-1.5 py-0.5 bg-muted text-muted-foreground rounded">
|
<span className="text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded">
|
||||||
{key}
|
{key}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-foreground">{item}</span>
|
<span className="text-sm text-foreground">{item}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -713,11 +713,11 @@ export function ProjectOverviewPage() {
|
|||||||
{/* Read-only Mode - Constraints */}
|
{/* Read-only Mode - Constraints */}
|
||||||
{guidelines.constraints && (
|
{guidelines.constraints && (
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-xs font-semibold text-foreground mb-2 flex items-center gap-1.5">
|
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||||
<ShieldAlert className="w-3.5 h-3.5" />
|
<ShieldAlert className="w-4 h-4" />
|
||||||
<span>{formatMessage({ id: 'projectOverview.guidelines.constraints' })}</span>
|
<span>{formatMessage({ id: 'projectOverview.guidelines.constraints' })}</span>
|
||||||
</h4>
|
</h4>
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-2">
|
||||||
{Object.entries(guidelines.constraints).map(([key, items]) => {
|
{Object.entries(guidelines.constraints).map(([key, items]) => {
|
||||||
const itemList = Array.isArray(items) ? items : [];
|
const itemList = Array.isArray(items) ? items : [];
|
||||||
if (itemList.length === 0) return null;
|
if (itemList.length === 0) return null;
|
||||||
@@ -726,12 +726,12 @@ export function ProjectOverviewPage() {
|
|||||||
{itemList.map((item: string, i: number) => (
|
{itemList.map((item: string, i: number) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="flex items-start gap-2 p-2 bg-background border border-border rounded"
|
className="flex items-start gap-3 p-3 bg-background border border-border rounded-lg"
|
||||||
>
|
>
|
||||||
<span className="text-[10px] px-1.5 py-0.5 bg-muted text-muted-foreground rounded">
|
<span className="text-xs px-2 py-0.5 bg-muted text-muted-foreground rounded">
|
||||||
{key}
|
{key}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs text-foreground">{item}</span>
|
<span className="text-sm text-foreground">{item}</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -328,13 +328,9 @@ export function ReviewSessionPage() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleSelectAll = () => {
|
const selectAllFindings = () => {
|
||||||
const validIds = filteredFindings.map(f => f.id).filter((id): id is string => id !== undefined);
|
const validIds = filteredFindings.map(f => f.id).filter((id): id is string => id !== undefined);
|
||||||
if (selectedFindings.size === validIds.length) {
|
setSelectedFindings(new Set(validIds));
|
||||||
setSelectedFindings(new Set());
|
|
||||||
} else {
|
|
||||||
setSelectedFindings(new Set(validIds));
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectVisibleFindings = () => {
|
const selectVisibleFindings = () => {
|
||||||
@@ -343,16 +339,20 @@ export function ReviewSessionPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const selectBySeverity = (severity: FindingWithSelection['severity']) => {
|
const selectBySeverity = (severity: FindingWithSelection['severity']) => {
|
||||||
const criticalIds = flattenedFindings
|
const severityIds = flattenedFindings
|
||||||
.filter(f => f.severity === severity && f.id !== undefined)
|
.filter(f => f.severity === severity && f.id !== undefined)
|
||||||
.map(f => f.id!);
|
.map(f => f.id!);
|
||||||
setSelectedFindings(prev => {
|
setSelectedFindings(prev => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
criticalIds.forEach(id => next.add(id));
|
severityIds.forEach(id => next.add(id));
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const clearSelection = () => {
|
||||||
|
setSelectedFindings(new Set());
|
||||||
|
};
|
||||||
|
|
||||||
const toggleExpandFinding = (findingId: string) => {
|
const toggleExpandFinding = (findingId: string) => {
|
||||||
setExpandedFindings(prev => {
|
setExpandedFindings(prev => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
@@ -600,49 +600,19 @@ export function ReviewSessionPage() {
|
|||||||
{/* Fix Progress Carousel */}
|
{/* Fix Progress Carousel */}
|
||||||
{sessionId && <FixProgressCarousel sessionId={sessionId} />}
|
{sessionId && <FixProgressCarousel sessionId={sessionId} />}
|
||||||
|
|
||||||
{/* Filters and Controls */}
|
{/* Unified Filter Card with Dimension Tabs */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-4 space-y-4">
|
<CardContent className="p-4 space-y-4">
|
||||||
{/* Checkbox-style Severity Filters */}
|
{/* Top Bar: Search + Sort + Reset */}
|
||||||
<div className="space-y-3">
|
<div className="flex flex-wrap gap-3 items-center">
|
||||||
<div className="text-sm font-medium">{formatMessage({ id: 'reviewSession.filters.severity' })}</div>
|
<div className="relative flex-1 min-w-[180px] max-w-md">
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{(['critical', 'high', 'medium', 'low'] as const).map(severity => {
|
|
||||||
const isEnabled = severityFilter.has(severity);
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
key={severity}
|
|
||||||
className={`inline-flex items-center gap-2 px-3 py-1.5 rounded-full border cursor-pointer transition-colors ${
|
|
||||||
isEnabled
|
|
||||||
? 'bg-primary text-primary-foreground border-primary'
|
|
||||||
: 'bg-background border-border hover:bg-muted'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={isEnabled}
|
|
||||||
onChange={() => toggleSeverity(severity)}
|
|
||||||
className="sr-only"
|
|
||||||
/>
|
|
||||||
<span className="text-sm font-medium">
|
|
||||||
{formatMessage({ id: `reviewSession.severity.${severity}` })}
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Search and Sort */}
|
|
||||||
<div className="flex flex-wrap gap-3">
|
|
||||||
<div className="relative flex-1 min-w-[200px]">
|
|
||||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={formatMessage({ id: 'reviewSession.search.placeholder' })}
|
placeholder={formatMessage({ id: 'reviewSession.search.placeholder' })}
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={e => setSearchQuery(e.target.value)}
|
onChange={e => setSearchQuery(e.target.value)}
|
||||||
className="w-full pl-10 pr-4 py-2 text-sm bg-background border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-primary"
|
className="w-full pl-10 pr-4 py-2 text-sm bg-background border border-border rounded-md focus:outline-none focus:ring-2 focus:ring-primary focus:border-transparent"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<select
|
<select
|
||||||
@@ -658,81 +628,128 @@ export function ReviewSessionPage() {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
|
onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
|
||||||
|
className="w-9 p-0"
|
||||||
>
|
>
|
||||||
{sortOrder === 'asc' ? '↑' : '↓'}
|
{sortOrder === 'asc' ? '↑' : '↓'}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="sm" onClick={resetFilters}>
|
<Button variant="ghost" size="sm" onClick={resetFilters} className="text-muted-foreground hover:text-foreground">
|
||||||
{formatMessage({ id: 'reviewSession.filters.reset' })}
|
✕ {formatMessage({ id: 'reviewSession.filters.reset' })}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Selection Controls */}
|
{/* Middle Row: Dimension Tabs + Severity Filters */}
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-col sm:flex-row gap-4">
|
||||||
<div className="flex items-center gap-2">
|
{/* Dimension Tabs - Horizontal Scrollable */}
|
||||||
<span className="text-sm text-muted-foreground">
|
<div className="flex-1">
|
||||||
{formatMessage({ id: 'reviewSession.selection.count' }, { count: selectedFindings.size })}
|
<div className="text-xs font-medium text-muted-foreground mb-2">
|
||||||
|
{formatMessage({ id: 'reviewSession.filters.dimension' })}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1.5 overflow-x-auto pb-1 scrollbar-thin">
|
||||||
|
<button
|
||||||
|
className={`px-3 py-1.5 rounded-md text-xs font-medium whitespace-nowrap transition-colors ${
|
||||||
|
dimensionFilter === 'all'
|
||||||
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
|
: 'bg-muted/50 text-muted-foreground hover:bg-muted'
|
||||||
|
}`}
|
||||||
|
onClick={() => setDimensionFilter('all')}
|
||||||
|
>
|
||||||
|
All ({dimensionCounts.all || 0})
|
||||||
|
</button>
|
||||||
|
{dimensions.map(dim => (
|
||||||
|
<button
|
||||||
|
key={dim.name}
|
||||||
|
className={`px-3 py-1.5 rounded-md text-xs font-medium whitespace-nowrap transition-colors ${
|
||||||
|
dimensionFilter === dim.name
|
||||||
|
? 'bg-primary text-primary-foreground shadow-sm'
|
||||||
|
: 'bg-muted/50 text-muted-foreground hover:bg-muted'
|
||||||
|
}`}
|
||||||
|
onClick={() => setDimensionFilter(dim.name)}
|
||||||
|
>
|
||||||
|
{dim.name} ({dim.findings?.length || 0})
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Severity Filters - Compact Pills */}
|
||||||
|
<div className="sm:w-auto">
|
||||||
|
<div className="text-xs font-medium text-muted-foreground mb-2">
|
||||||
|
{formatMessage({ id: 'reviewSession.filters.severity' })}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1.5 flex-wrap">
|
||||||
|
{(['critical', 'high', 'medium', 'low'] as const).map(severity => {
|
||||||
|
const isEnabled = severityFilter.has(severity);
|
||||||
|
const colors = {
|
||||||
|
critical: isEnabled ? 'bg-red-500 text-white border-red-500' : 'bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 border-red-200 dark:border-red-800',
|
||||||
|
high: isEnabled ? 'bg-orange-500 text-white border-orange-500' : 'bg-orange-50 dark:bg-orange-900/20 text-orange-600 dark:text-orange-400 border-orange-200 dark:border-orange-800',
|
||||||
|
medium: isEnabled ? 'bg-blue-500 text-white border-blue-500' : 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 border-blue-200 dark:border-blue-800',
|
||||||
|
low: isEnabled ? 'bg-gray-500 text-white border-gray-500' : 'bg-gray-50 dark:bg-gray-900/20 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-800',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
key={severity}
|
||||||
|
className={`inline-flex items-center gap-1 px-2.5 py-1 rounded-md border text-xs font-medium cursor-pointer transition-all ${
|
||||||
|
colors[severity]
|
||||||
|
} ${isEnabled ? 'shadow-sm' : 'hover:opacity-80'}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isEnabled}
|
||||||
|
onChange={() => toggleSeverity(severity)}
|
||||||
|
className="sr-only"
|
||||||
|
/>
|
||||||
|
<span>{formatMessage({ id: `reviewSession.severity.short.${severity}` })}</span>
|
||||||
|
<span className="opacity-70">({flattenedFindings.filter(f => f.severity === severity).length})</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom Bar: Selection Actions + Export */}
|
||||||
|
<div className="flex flex-wrap items-center justify-between gap-3 pt-3 border-t border-border">
|
||||||
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
|
<span className="text-xs text-muted-foreground px-2 py-1 bg-muted rounded-md">
|
||||||
|
{selectedFindings.size > 0
|
||||||
|
? formatMessage({ id: 'reviewSession.selection.countSelected' }, { count: selectedFindings.size })
|
||||||
|
: formatMessage({ id: 'reviewSession.selection.total' }, { count: filteredFindings.length })
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
<Button variant="outline" size="sm" onClick={toggleSelectAll}>
|
{selectedFindings.size > 0 && (
|
||||||
{selectedFindings.size === filteredFindings.length
|
<>
|
||||||
? formatMessage({ id: 'reviewSession.selection.clearAll' })
|
<Button variant="outline" size="sm" onClick={clearSelection} className="h-8 text-xs">
|
||||||
: formatMessage({ id: 'reviewSession.selection.selectAll' })}
|
{formatMessage({ id: 'reviewSession.selection.clear' })}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="sm" onClick={selectVisibleFindings}>
|
<Button variant="outline" size="sm" onClick={() => selectBySeverity('critical')} className="h-8 text-xs">
|
||||||
{formatMessage({ id: 'reviewSession.selection.selectVisible' })}
|
🔥 {formatMessage({ id: 'reviewSession.selection.selectCritical' })}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="sm" onClick={() => selectBySeverity('critical')}>
|
<Button variant="outline" size="sm" onClick={selectVisibleFindings} className="h-8 text-xs">
|
||||||
{formatMessage({ id: 'reviewSession.selection.selectCritical' })}
|
{formatMessage({ id: 'reviewSession.selection.selectVisible' })}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
</>
|
||||||
variant="outline"
|
)}
|
||||||
size="sm"
|
{selectedFindings.size === 0 && (
|
||||||
onClick={() => setSelectedFindings(new Set())}
|
<Button variant="outline" size="sm" onClick={selectAllFindings} className="h-8 text-xs">
|
||||||
>
|
{formatMessage({ id: 'reviewSession.selection.selectAll' })}
|
||||||
{formatMessage({ id: 'reviewSession.selection.clear' })}
|
</Button>
|
||||||
</Button>
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant={selectedFindings.size > 0 ? 'default' : 'outline'}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={exportSelectedAsJson}
|
onClick={exportSelectedAsJson}
|
||||||
disabled={selectedFindings.size === 0}
|
disabled={selectedFindings.size === 0}
|
||||||
className="gap-2"
|
className="h-8 gap-1.5 text-xs"
|
||||||
>
|
>
|
||||||
<Download className="h-4 w-4" />
|
<Download className="h-3.5 w-3.5" />
|
||||||
🔧 {formatMessage({ id: 'reviewSession.export' })}
|
🔧 {formatMessage({ id: 'reviewSession.export' })}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* Dimension Tabs */}
|
{/* Split Panel: Findings List + Preview */}
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
<button
|
|
||||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
||||||
dimensionFilter === 'all'
|
|
||||||
? 'bg-primary text-primary-foreground'
|
|
||||||
: 'bg-muted text-muted-foreground hover:bg-muted/80'
|
|
||||||
}`}
|
|
||||||
onClick={() => setDimensionFilter('all')}
|
|
||||||
>
|
|
||||||
{formatMessage({ id: 'reviewSession.dimensionTabs.all' })} ({dimensionCounts.all || 0})
|
|
||||||
</button>
|
|
||||||
{dimensions.map(dim => (
|
|
||||||
<button
|
|
||||||
key={dim.name}
|
|
||||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
|
||||||
dimensionFilter === dim.name
|
|
||||||
? 'bg-primary text-primary-foreground'
|
|
||||||
: 'bg-muted text-muted-foreground hover:bg-muted/80'
|
|
||||||
}`}
|
|
||||||
onClick={() => setDimensionFilter(dim.name)}
|
|
||||||
>
|
|
||||||
{dim.name} ({dim.findings?.length || 0})
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Findings List */}
|
|
||||||
{filteredFindings.length === 0 ? (
|
{filteredFindings.length === 0 ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-12 text-center">
|
<CardContent className="p-12 text-center">
|
||||||
@@ -746,118 +763,274 @@ export function ReviewSessionPage() {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="grid grid-cols-1 md:grid-cols-[minmax(0,45fr)_minmax(0,55fr)] gap-4">
|
||||||
{filteredFindings.filter(f => f.id !== undefined).map(finding => {
|
{/* Left Panel: Findings List */}
|
||||||
const findingId = finding.id!;
|
<Card>
|
||||||
const isExpanded = expandedFindings.has(findingId);
|
<CardContent className="p-4">
|
||||||
const isSelected = selectedFindings.has(findingId);
|
<div className="flex items-center justify-between mb-3">
|
||||||
const badge = getSeverityBadge(finding.severity);
|
<span className="text-sm font-medium">
|
||||||
const BadgeIcon = badge.icon;
|
{formatMessage({ id: 'reviewSession.findingsList.count' }, { count: filteredFindings.length })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 max-h-[600px] overflow-y-auto">
|
||||||
|
{filteredFindings.filter(f => f.id !== undefined).map(finding => {
|
||||||
|
const findingId = finding.id!;
|
||||||
|
const isSelected = selectedFindings.has(findingId);
|
||||||
|
const isPreviewing = selectedFindingId === findingId;
|
||||||
|
const badge = getSeverityBadge(finding.severity);
|
||||||
|
const BadgeIcon = badge.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card key={findingId} className={isSelected ? 'ring-2 ring-primary' : ''}>
|
<div
|
||||||
<CardContent className="p-4">
|
key={findingId}
|
||||||
<div className="flex items-start gap-3">
|
className={`p-3 rounded-lg border cursor-pointer transition-colors ${
|
||||||
{/* Checkbox */}
|
isPreviewing
|
||||||
<input
|
? 'bg-primary/10 border-primary'
|
||||||
type="checkbox"
|
: 'bg-background border-border hover:bg-muted'
|
||||||
checked={isSelected}
|
}`}
|
||||||
onChange={() => toggleSelectFinding(findingId)}
|
onClick={() => handleFindingClick(findingId)}
|
||||||
className="mt-1"
|
>
|
||||||
/>
|
<div className="flex items-start gap-2">
|
||||||
|
{/* Checkbox */}
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isSelected}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleSelectFinding(findingId);
|
||||||
|
}}
|
||||||
|
className="mt-0.5 flex-shrink-0"
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
{/* Compact Finding Content */}
|
||||||
{/* Header */}
|
|
||||||
<div
|
|
||||||
className="flex items-start justify-between gap-3 cursor-pointer"
|
|
||||||
onClick={() => toggleExpandFinding(findingId)}
|
|
||||||
>
|
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
<div className="flex items-center gap-1.5 mb-1 flex-wrap">
|
||||||
<Badge variant={badge.variant} className="gap-1">
|
<Badge variant={badge.variant} className="gap-1 text-xs">
|
||||||
|
<BadgeIcon className="h-2.5 w-2.5" />
|
||||||
|
{badge.label}
|
||||||
|
</Badge>
|
||||||
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{finding.dimension}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-medium text-foreground truncate">
|
||||||
|
{finding.title}
|
||||||
|
</p>
|
||||||
|
{finding.file && (
|
||||||
|
<p className="text-xs text-muted-foreground font-mono truncate mt-0.5">
|
||||||
|
{finding.file}:{finding.line || '?'}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Right Panel: Enhanced Preview */}
|
||||||
|
<Card className="sticky top-4 self-start">
|
||||||
|
<CardContent className="p-0 h-full min-h-[500px]">
|
||||||
|
{!selectedFindingId ? (
|
||||||
|
// Enhanced Empty State
|
||||||
|
<div className="flex flex-col items-center justify-center h-full min-h-[400px] p-8 text-center bg-gradient-to-br from-muted/30 to-muted/10">
|
||||||
|
<div className="w-20 h-20 rounded-full bg-muted flex items-center justify-center mb-4">
|
||||||
|
<Search className="h-10 w-10 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-base font-semibold text-foreground mb-2">
|
||||||
|
{formatMessage({ id: 'reviewSession.preview.emptyTitle' })}
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground mb-4 max-w-[250px]">
|
||||||
|
{formatMessage({ id: 'reviewSession.preview.empty' })}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2 justify-center">
|
||||||
|
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-background rounded-lg border text-xs">
|
||||||
|
<span className="w-2 h-2 rounded-full bg-destructive"></span>
|
||||||
|
{formatMessage({ id: 'reviewSession.preview.emptyTipSeverity' })}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-background rounded-lg border text-xs">
|
||||||
|
<span>📁</span>
|
||||||
|
{formatMessage({ id: 'reviewSession.preview.emptyTipFile' })}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// Preview Content
|
||||||
|
(() => {
|
||||||
|
const finding = flattenedFindings.find(f => f.id === selectedFindingId);
|
||||||
|
if (!finding) return null;
|
||||||
|
|
||||||
|
const badge = getSeverityBadge(finding.severity);
|
||||||
|
const BadgeIcon = badge.icon;
|
||||||
|
const isSelected = selectedFindings.has(selectedFindingId);
|
||||||
|
|
||||||
|
// Find adjacent findings for navigation
|
||||||
|
const findingIndex = filteredFindings.findIndex(f => f.id === selectedFindingId);
|
||||||
|
const prevFinding = findingIndex > 0 ? filteredFindings[findingIndex - 1] : null;
|
||||||
|
const nextFinding = findingIndex < filteredFindings.length - 1 ? filteredFindings[findingIndex + 1] : null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
{/* Sticky Header */}
|
||||||
|
<div className="sticky top-0 z-10 bg-background border-b border-border p-4 space-y-3">
|
||||||
|
{/* Navigation + Badges Row */}
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
{/* Navigation Buttons */}
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => prevFinding && handleFindingClick(prevFinding.id!)}
|
||||||
|
disabled={!prevFinding}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4 rotate-180" />
|
||||||
|
</Button>
|
||||||
|
<span className="text-xs text-muted-foreground px-2">
|
||||||
|
{findingIndex + 1} / {filteredFindings.length}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => nextFinding && handleFindingClick(nextFinding.id!)}
|
||||||
|
disabled={!nextFinding}
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Badges */}
|
||||||
|
<div className="flex items-center gap-1.5 flex-wrap">
|
||||||
|
<Badge variant={badge.variant} className="gap-1 text-xs">
|
||||||
<BadgeIcon className="h-3 w-3" />
|
<BadgeIcon className="h-3 w-3" />
|
||||||
{badge.label}
|
{badge.label}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="outline" className="text-xs">
|
<Badge variant="outline" className="text-xs">
|
||||||
{finding.dimension}
|
{finding.dimension}
|
||||||
</Badge>
|
</Badge>
|
||||||
{finding.file && (
|
|
||||||
<span className="text-xs text-muted-foreground font-mono">
|
|
||||||
{finding.file}:{finding.line || '?'}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<h4 className="font-medium text-foreground text-sm">{finding.title}</h4>
|
|
||||||
{finding.description && (
|
{/* Select Button */}
|
||||||
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">
|
<Button
|
||||||
{finding.description}
|
variant={isSelected ? 'default' : 'outline'}
|
||||||
</p>
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
toggleSelectFinding(selectedFindingId);
|
||||||
|
}}
|
||||||
|
className="h-8 text-xs"
|
||||||
|
>
|
||||||
|
{isSelected ? (
|
||||||
|
<>
|
||||||
|
<CheckCircle className="h-3.5 w-3.5 mr-1" />
|
||||||
|
{formatMessage({ id: 'reviewSession.preview.selected' })}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="mr-1">⊕</span>
|
||||||
|
{formatMessage({ id: 'reviewSession.preview.selectForFix' })}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Title */}
|
||||||
|
<h3 className="text-base font-semibold text-foreground line-clamp-2">
|
||||||
|
{finding.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Quick Info Bar */}
|
||||||
|
<div className="flex items-center gap-3 text-xs">
|
||||||
|
{finding.file && (
|
||||||
|
<div className="flex items-center gap-1 text-muted-foreground flex-1 min-w-0">
|
||||||
|
<span className="flex-shrink-0">📁</span>
|
||||||
|
<code className="truncate">{finding.file}:{finding.line || '?'}</code>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="sm" className="flex-shrink-0">
|
|
||||||
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Expanded Content */}
|
{/* Scrollable Content */}
|
||||||
{isExpanded && (
|
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||||
<div className="mt-4 pt-4 border-t border-border space-y-3">
|
{/* Description */}
|
||||||
{/* Code Context */}
|
{finding.description && (
|
||||||
{finding.code_context && (
|
<div className="bg-muted/30 rounded-lg p-3">
|
||||||
<div>
|
<div className="text-xs font-semibold text-foreground mb-1.5 flex items-center gap-1.5">
|
||||||
<h5 className="text-xs font-semibold text-foreground mb-1">
|
<span>📝</span>
|
||||||
{formatMessage({ id: 'reviewSession.codeContext' })}
|
{formatMessage({ id: 'reviewSession.preview.description' })}
|
||||||
</h5>
|
|
||||||
<pre className="text-xs bg-muted p-2 rounded overflow-x-auto">
|
|
||||||
<code>{finding.code_context}</code>
|
|
||||||
</pre>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<p className="text-sm text-foreground leading-relaxed">
|
||||||
|
{finding.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Root Cause */}
|
{/* Code Context */}
|
||||||
{finding.root_cause && (
|
{finding.code_context && (
|
||||||
<div>
|
<div>
|
||||||
<h5 className="text-xs font-semibold text-foreground mb-1">
|
<div className="text-xs font-semibold text-foreground mb-1.5 flex items-center gap-1.5">
|
||||||
{formatMessage({ id: 'reviewSession.rootCause' })}
|
<span>💻</span>
|
||||||
</h5>
|
{formatMessage({ id: 'reviewSession.preview.codeContext' })}
|
||||||
<p className="text-xs text-muted-foreground">{finding.root_cause}</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<pre className="text-xs bg-muted p-3 rounded-lg overflow-x-auto border border-border">
|
||||||
|
<code className="text-foreground">{finding.code_context}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Impact */}
|
{/* Root Cause */}
|
||||||
{finding.impact && (
|
{finding.root_cause && (
|
||||||
<div>
|
<div>
|
||||||
<h5 className="text-xs font-semibold text-foreground mb-1">
|
<div className="text-xs font-semibold text-foreground mb-1.5 flex items-center gap-1.5">
|
||||||
{formatMessage({ id: 'reviewSession.impact' })}
|
<span>🎯</span>
|
||||||
</h5>
|
{formatMessage({ id: 'reviewSession.preview.rootCause' })}
|
||||||
<p className="text-xs text-muted-foreground">{finding.impact}</p>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<p className="text-sm text-foreground bg-muted/30 rounded-lg p-3 leading-relaxed">
|
||||||
|
{finding.root_cause}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Recommendations */}
|
{/* Impact */}
|
||||||
{finding.recommendations && finding.recommendations.length > 0 && (
|
{finding.impact && (
|
||||||
<div>
|
<div>
|
||||||
<h5 className="text-xs font-semibold text-foreground mb-1">
|
<div className="text-xs font-semibold text-foreground mb-1.5 flex items-center gap-1.5">
|
||||||
{formatMessage({ id: 'reviewSession.recommendations' })}
|
<span>⚠️</span>
|
||||||
</h5>
|
{formatMessage({ id: 'reviewSession.preview.impact' })}
|
||||||
<ul className="space-y-1">
|
|
||||||
{finding.recommendations.map((rec, idx) => (
|
|
||||||
<li key={idx} className="text-xs text-muted-foreground flex items-start gap-2">
|
|
||||||
<span className="text-primary">•</span>
|
|
||||||
<span>{rec}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<p className="text-sm text-foreground bg-orange-50 dark:bg-orange-900/20 rounded-lg p-3 leading-relaxed border border-orange-200 dark:border-orange-800">
|
||||||
</div>
|
{finding.impact}
|
||||||
)}
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Recommendations */}
|
||||||
|
{finding.recommendations && finding.recommendations.length > 0 && (
|
||||||
|
<div>
|
||||||
|
<div className="text-xs font-semibold text-foreground mb-1.5 flex items-center gap-1.5">
|
||||||
|
<span>✅</span>
|
||||||
|
{formatMessage({ id: 'reviewSession.preview.recommendations' })}
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{finding.recommendations.map((rec, idx) => (
|
||||||
|
<li key={idx} className="text-sm text-foreground flex items-start gap-2 bg-green-50 dark:bg-green-900/20 rounded-lg p-3 border border-green-200 dark:border-green-800">
|
||||||
|
<span className="text-green-600 dark:text-green-400 flex-shrink-0 mt-0.5">✓</span>
|
||||||
|
<span className="leading-relaxed">{rec}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
</CardContent>
|
})()
|
||||||
</Card>
|
)}
|
||||||
);
|
</CardContent>
|
||||||
})}
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,10 +28,19 @@ import { TaskDrawer } from '@/components/shared/TaskDrawer';
|
|||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
|
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
|
||||||
import type { TaskData } from '@/types/store';
|
import type { TaskData, SessionMetadata } from '@/types/store';
|
||||||
|
|
||||||
type TabValue = 'tasks' | 'context' | 'summary' | 'impl-plan' | 'conflict' | 'review';
|
type TabValue = 'tasks' | 'context' | 'summary' | 'impl-plan' | 'conflict' | 'review';
|
||||||
|
|
||||||
|
// Status label keys for i18n (maps snake_case status to camelCase translation keys)
|
||||||
|
const statusLabelKeys: Record<SessionMetadata['status'], string> = {
|
||||||
|
planning: 'sessions.status.planning',
|
||||||
|
in_progress: 'sessions.status.inProgress',
|
||||||
|
completed: 'sessions.status.completed',
|
||||||
|
archived: 'sessions.status.archived',
|
||||||
|
paused: 'sessions.status.paused',
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SessionDetailPage component - Main session detail page with tabs
|
* SessionDetailPage component - Main session detail page with tabs
|
||||||
*/
|
*/
|
||||||
@@ -159,7 +168,7 @@ export function SessionDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant={session.status === 'completed' ? 'success' : 'secondary'}>
|
<Badge variant={session.status === 'completed' ? 'success' : 'secondary'}>
|
||||||
{formatMessage({ id: `sessions.status.${session.status}` })}
|
{formatMessage({ id: statusLabelKeys[session.status] })}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,15 @@ import type { SessionMetadata } from '@/types/store';
|
|||||||
|
|
||||||
type LocationFilter = 'all' | 'active' | 'archived';
|
type LocationFilter = 'all' | 'active' | 'archived';
|
||||||
|
|
||||||
|
// Status label keys for i18n (maps snake_case status to camelCase translation keys)
|
||||||
|
const statusLabelKeys: Record<SessionMetadata['status'], string> = {
|
||||||
|
planning: 'sessions.status.planning',
|
||||||
|
in_progress: 'sessions.status.inProgress',
|
||||||
|
completed: 'sessions.status.completed',
|
||||||
|
archived: 'sessions.status.archived',
|
||||||
|
paused: 'sessions.status.paused',
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SessionsPage component - Sessions list with CRUD operations
|
* SessionsPage component - Sessions list with CRUD operations
|
||||||
*/
|
*/
|
||||||
@@ -88,8 +97,13 @@ export function SessionsPage() {
|
|||||||
const isMutating = isArchiving || isDeleting;
|
const isMutating = isArchiving || isDeleting;
|
||||||
|
|
||||||
// Handlers
|
// Handlers
|
||||||
const handleSessionClick = (sessionId: string) => {
|
const handleSessionClick = (sessionId: string, sessionType?: SessionMetadata['type']) => {
|
||||||
navigate(`/sessions/${sessionId}`);
|
// Route review sessions to the dedicated review page
|
||||||
|
if (sessionType === 'review') {
|
||||||
|
navigate(`/sessions/${sessionId}/review`);
|
||||||
|
} else {
|
||||||
|
navigate(`/sessions/${sessionId}`);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleArchive = async (sessionId: string) => {
|
const handleArchive = async (sessionId: string) => {
|
||||||
@@ -225,7 +239,7 @@ export function SessionsPage() {
|
|||||||
onClick={() => toggleStatusFilter(status)}
|
onClick={() => toggleStatusFilter(status)}
|
||||||
className="justify-between"
|
className="justify-between"
|
||||||
>
|
>
|
||||||
<span>{formatMessage({ id: `sessions.status.${status}` })}</span>
|
<span>{formatMessage({ id: statusLabelKeys[status] })}</span>
|
||||||
{statusFilter.includes(status) && (
|
{statusFilter.includes(status) && (
|
||||||
<span className="text-primary">✓</span>
|
<span className="text-primary">✓</span>
|
||||||
)}
|
)}
|
||||||
@@ -254,7 +268,7 @@ export function SessionsPage() {
|
|||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => toggleStatusFilter(status)}
|
onClick={() => toggleStatusFilter(status)}
|
||||||
>
|
>
|
||||||
{formatMessage({ id: `sessions.status.${status}` })}
|
{formatMessage({ id: statusLabelKeys[status] })}
|
||||||
<X className="ml-1 h-3 w-3" />
|
<X className="ml-1 h-3 w-3" />
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
@@ -304,8 +318,8 @@ export function SessionsPage() {
|
|||||||
<SessionCard
|
<SessionCard
|
||||||
key={session.session_id}
|
key={session.session_id}
|
||||||
session={session}
|
session={session}
|
||||||
onClick={handleSessionClick}
|
onClick={(sessionId) => handleSessionClick(sessionId, session.type)}
|
||||||
onView={handleSessionClick}
|
onView={(sessionId) => handleSessionClick(sessionId, session.type)}
|
||||||
onArchive={handleArchive}
|
onArchive={handleArchive}
|
||||||
onDelete={handleDeleteClick}
|
onDelete={handleDeleteClick}
|
||||||
actionsDisabled={isMutating}
|
actionsDisabled={isMutating}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
ListChecks,
|
ListChecks,
|
||||||
Code,
|
Code,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
Zap,
|
|
||||||
Calendar,
|
Calendar,
|
||||||
FileCode,
|
FileCode,
|
||||||
Layers,
|
Layers,
|
||||||
@@ -198,15 +197,6 @@ export function TaskListTab({ session, onTaskClick }: TaskListTabProps) {
|
|||||||
// Cast to extended type to access all possible fields
|
// Cast to extended type to access all possible fields
|
||||||
const extTask = task as unknown as ExtendedTask;
|
const extTask = task as unknown as ExtendedTask;
|
||||||
|
|
||||||
// Priority config
|
|
||||||
const priorityConfig: Record<string, { label: string; variant: 'default' | 'secondary' | 'destructive' | 'warning' | 'info' }> = {
|
|
||||||
critical: { label: formatMessage({ id: 'sessionDetail.tasks.priority.critical' }), variant: 'destructive' },
|
|
||||||
high: { label: formatMessage({ id: 'sessionDetail.tasks.priority.high' }), variant: 'warning' },
|
|
||||||
medium: { label: formatMessage({ id: 'sessionDetail.tasks.priority.medium' }), variant: 'info' },
|
|
||||||
low: { label: formatMessage({ id: 'sessionDetail.tasks.priority.low' }), variant: 'secondary' },
|
|
||||||
};
|
|
||||||
const priority = extTask.priority ? priorityConfig[extTask.priority] : null;
|
|
||||||
|
|
||||||
// Get depends_on from either root level or context
|
// Get depends_on from either root level or context
|
||||||
const dependsOn = extTask.depends_on || extTask.context?.depends_on || [];
|
const dependsOn = extTask.depends_on || extTask.context?.depends_on || [];
|
||||||
const dependsCount = dependsOn.length;
|
const dependsCount = dependsOn.length;
|
||||||
|
|||||||
@@ -182,9 +182,11 @@ export interface SessionMetadata {
|
|||||||
plan_updated_at?: string;
|
plan_updated_at?: string;
|
||||||
has_review?: boolean;
|
has_review?: boolean;
|
||||||
review?: {
|
review?: {
|
||||||
dimensions: string[];
|
dimensions: Array<{ name: string; findings?: Array<{ severity?: string }> }>;
|
||||||
iterations: string[];
|
dimensions_count?: number;
|
||||||
fixes: string[];
|
findings?: number;
|
||||||
|
iterations?: string[];
|
||||||
|
fixes?: string[];
|
||||||
};
|
};
|
||||||
summaries?: Array<{ task_id: string; content: unknown }>;
|
summaries?: Array<{ task_id: string; content: unknown }>;
|
||||||
tasks?: TaskData[];
|
tasks?: TaskData[];
|
||||||
|
|||||||
Reference in New Issue
Block a user