mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-03-01 10:53:25 +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:
@@ -428,49 +428,34 @@ export function LiteTaskDetailPage() {
|
||||
</div>
|
||||
|
||||
{/* Right: Meta Information */}
|
||||
<div className="flex flex-col items-end gap-2 text-xs text-muted-foreground flex-shrink-0">
|
||||
{/* Row 1: Status Badge */}
|
||||
<Badge
|
||||
variant={task.status === 'completed' ? 'success' : task.status === 'in_progress' ? 'warning' : task.status === 'blocked' ? 'destructive' : 'secondary'}
|
||||
className="w-fit"
|
||||
>
|
||||
{task.status}
|
||||
</Badge>
|
||||
<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-6 px-2 py-0.5 text-xs font-mono border-primary/30 text-primary">
|
||||
{depId}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Row 2: Metadata */}
|
||||
<div className="flex items-center gap-3 flex-wrap justify-end">
|
||||
{/* Dependencies Count */}
|
||||
{task.context?.depends_on && task.context.depends_on.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.depends_on.length}</span>
|
||||
<span>dep{task.context.depends_on.length > 1 ? 's' : ''}</span>
|
||||
</span>
|
||||
)}
|
||||
{/* Target Files Count */}
|
||||
{task.flow_control?.target_files && task.flow_control.target_files.length > 0 && (
|
||||
<Badge variant="secondary" className="h-5 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>
|
||||
)}
|
||||
|
||||
{/* Target Files Count */}
|
||||
{task.flow_control?.target_files && task.flow_control.target_files.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.flow_control.target_files.length}</span>
|
||||
<span>file{task.flow_control.target_files.length > 1 ? 's' : ''}</span>
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* 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>
|
||||
{/* Implementation Steps Count */}
|
||||
{task.flow_control?.implementation_approach && task.flow_control.implementation_approach.length > 0 && (
|
||||
<Badge variant="secondary" className="h-5 px-1.5 py-0 text-[10px] gap-0.5">
|
||||
<span className="font-semibold">{task.flow_control.implementation_approach.length}</span>
|
||||
<span>step{task.flow_control.implementation_approach.length > 1 ? 's' : ''}</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
@@ -33,6 +33,8 @@ import {
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
AlertCircle,
|
||||
Target,
|
||||
FileCode,
|
||||
} from 'lucide-react';
|
||||
import { useLiteTasks } from '@/hooks/useLiteTasks';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
@@ -146,25 +148,36 @@ function ExpandedSessionPanel({
|
||||
}}
|
||||
>
|
||||
<CardContent className="p-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<Badge className="text-xs font-mono shrink-0 bg-primary/10 text-primary border-primary/20">
|
||||
{task.task_id || `#${index + 1}`}
|
||||
</Badge>
|
||||
<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}
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center 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 || `#${index + 1}`}
|
||||
</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>
|
||||
{task.description && (
|
||||
<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
|
||||
*/
|
||||
@@ -486,18 +900,15 @@ export function LiteTasksPage() {
|
||||
const taskCount = session.tasks?.length || 0;
|
||||
const isExpanded = expandedSessionId === session.id;
|
||||
|
||||
// Calculate task status distribution
|
||||
const taskStats = React.useMemo(() => {
|
||||
const tasks = session.tasks || [];
|
||||
return {
|
||||
completed: tasks.filter((t) => t.status === 'completed').length,
|
||||
inProgress: tasks.filter((t) => t.status === 'in_progress').length,
|
||||
blocked: tasks.filter((t) => t.status === 'blocked').length,
|
||||
pending: tasks.filter((t) => !t.status || t.status === 'pending').length,
|
||||
};
|
||||
}, [session.tasks]);
|
||||
// Calculate task status distribution (no useMemo - this is a render function, not a component)
|
||||
const tasks = session.tasks || [];
|
||||
const taskStats = {
|
||||
completed: tasks.filter((t) => t.status === 'completed').length,
|
||||
inProgress: tasks.filter((t) => t.status === 'in_progress').length,
|
||||
blocked: tasks.filter((t) => t.status === 'blocked').length,
|
||||
};
|
||||
|
||||
const firstTask = session.tasks?.[0];
|
||||
const firstTask = tasks[0];
|
||||
|
||||
return (
|
||||
<div key={session.id}>
|
||||
@@ -552,12 +963,6 @@ export function LiteTasksPage() {
|
||||
{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>
|
||||
|
||||
{/* Date and task count */}
|
||||
@@ -600,96 +1005,98 @@ export function LiteTasksPage() {
|
||||
const status = latestSynthesis.status || session.status || 'analyzing';
|
||||
const createdAt = (metadata.timestamp as string) || session.createdAt || '';
|
||||
|
||||
// Calculate task status distribution
|
||||
const taskStats = React.useMemo(() => {
|
||||
const tasks = session.tasks || [];
|
||||
return {
|
||||
completed: tasks.filter((t) => t.status === 'completed').length,
|
||||
inProgress: tasks.filter((t) => t.status === 'in_progress').length,
|
||||
blocked: tasks.filter((t) => t.status === 'blocked').length,
|
||||
pending: tasks.filter((t) => !t.status || t.status === 'pending').length,
|
||||
total: tasks.length,
|
||||
};
|
||||
}, [session.tasks]);
|
||||
// Calculate task status distribution (no useMemo - this is a render function, not a component)
|
||||
const tasks = session.tasks || [];
|
||||
const taskStats = {
|
||||
completed: tasks.filter((t) => t.status === 'completed').length,
|
||||
inProgress: tasks.filter((t) => t.status === 'in_progress').length,
|
||||
blocked: tasks.filter((t) => t.status === 'blocked').length,
|
||||
total: tasks.length,
|
||||
};
|
||||
|
||||
const isExpanded = expandedSessionId === session.id;
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={session.id}
|
||||
className="cursor-pointer hover:shadow-md transition-shadow"
|
||||
onClick={() => setExpandedSessionId(expandedSessionId === session.id ? null : session.id)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div className="flex-shrink-0">
|
||||
{expandedSessionId === session.id ? (
|
||||
<ChevronDown className="h-5 w-5 text-muted-foreground" />
|
||||
) : (
|
||||
<ChevronRight className="h-5 w-5 text-muted-foreground" />
|
||||
<div key={session.id}>
|
||||
<Card
|
||||
className="cursor-pointer hover:shadow-md transition-shadow"
|
||||
onClick={() => setExpandedSessionId(isExpanded ? null : session.id)}
|
||||
>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<div className="flex-shrink-0">
|
||||
{isExpanded ? (
|
||||
<ChevronDown 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 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" />
|
||||
{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>
|
||||
|
||||
<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" />
|
||||
{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 && (
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-semibold text-foreground flex items-center gap-1.5">
|
||||
<ScrollText className="w-4 h-4" />
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-foreground flex items-center gap-2">
|
||||
<ScrollText className="w-5 h-5" />
|
||||
{formatMessage({ id: 'projectOverview.guidelines.title' })}
|
||||
</h3>
|
||||
<div className="flex gap-1.5">
|
||||
<div className="flex gap-2">
|
||||
{!isEditMode ? (
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs px-2" onClick={handleEditStart}>
|
||||
<Edit className="w-3 h-3 mr-1" />
|
||||
<Button variant="outline" size="sm" onClick={handleEditStart}>
|
||||
<Edit className="w-4 h-4 mr-1" />
|
||||
{formatMessage({ id: 'projectOverview.guidelines.edit' })}
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button variant="outline" size="sm" className="h-7 text-xs px-2" onClick={handleEditCancel} disabled={isUpdating}>
|
||||
<X className="w-3 h-3 mr-1" />
|
||||
<Button variant="outline" size="sm" onClick={handleEditCancel} disabled={isUpdating}>
|
||||
<X className="w-4 h-4 mr-1" />
|
||||
{formatMessage({ id: 'projectOverview.guidelines.cancel' })}
|
||||
</Button>
|
||||
<Button variant="default" size="sm" className="h-7 text-xs px-2" onClick={handleSave} disabled={isUpdating}>
|
||||
<Save className="w-3 h-3 mr-1" />
|
||||
<Button variant="default" size="sm" onClick={handleSave} disabled={isUpdating}>
|
||||
<Save className="w-4 h-4 mr-1" />
|
||||
{isUpdating ? formatMessage({ id: 'projectOverview.guidelines.saving' }) : formatMessage({ id: 'projectOverview.guidelines.save' })}
|
||||
</Button>
|
||||
</>
|
||||
@@ -676,17 +676,17 @@ export function ProjectOverviewPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-6">
|
||||
{!isEditMode ? (
|
||||
<>
|
||||
{/* Read-only Mode - Conventions */}
|
||||
{guidelines.conventions && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-foreground mb-2 flex items-center gap-1.5">
|
||||
<BookMarked className="w-3.5 h-3.5" />
|
||||
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||
<BookMarked className="w-4 h-4" />
|
||||
<span>{formatMessage({ id: 'projectOverview.guidelines.conventions' })}</span>
|
||||
</h4>
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-2">
|
||||
{Object.entries(guidelines.conventions).map(([key, items]) => {
|
||||
const itemList = Array.isArray(items) ? items : [];
|
||||
if (itemList.length === 0) return null;
|
||||
@@ -695,12 +695,12 @@ export function ProjectOverviewPage() {
|
||||
{itemList.map((item: string, i: number) => (
|
||||
<div
|
||||
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}
|
||||
</span>
|
||||
<span className="text-xs text-foreground">{item}</span>
|
||||
<span className="text-sm text-foreground">{item}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -713,11 +713,11 @@ export function ProjectOverviewPage() {
|
||||
{/* Read-only Mode - Constraints */}
|
||||
{guidelines.constraints && (
|
||||
<div>
|
||||
<h4 className="text-xs font-semibold text-foreground mb-2 flex items-center gap-1.5">
|
||||
<ShieldAlert className="w-3.5 h-3.5" />
|
||||
<h4 className="text-sm font-semibold text-foreground mb-3 flex items-center gap-2">
|
||||
<ShieldAlert className="w-4 h-4" />
|
||||
<span>{formatMessage({ id: 'projectOverview.guidelines.constraints' })}</span>
|
||||
</h4>
|
||||
<div className="space-y-1.5">
|
||||
<div className="space-y-2">
|
||||
{Object.entries(guidelines.constraints).map(([key, items]) => {
|
||||
const itemList = Array.isArray(items) ? items : [];
|
||||
if (itemList.length === 0) return null;
|
||||
@@ -726,12 +726,12 @@ export function ProjectOverviewPage() {
|
||||
{itemList.map((item: string, i: number) => (
|
||||
<div
|
||||
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}
|
||||
</span>
|
||||
<span className="text-xs text-foreground">{item}</span>
|
||||
<span className="text-sm text-foreground">{item}</span>
|
||||
</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);
|
||||
if (selectedFindings.size === validIds.length) {
|
||||
setSelectedFindings(new Set());
|
||||
} else {
|
||||
setSelectedFindings(new Set(validIds));
|
||||
}
|
||||
setSelectedFindings(new Set(validIds));
|
||||
};
|
||||
|
||||
const selectVisibleFindings = () => {
|
||||
@@ -343,16 +339,20 @@ export function ReviewSessionPage() {
|
||||
};
|
||||
|
||||
const selectBySeverity = (severity: FindingWithSelection['severity']) => {
|
||||
const criticalIds = flattenedFindings
|
||||
const severityIds = flattenedFindings
|
||||
.filter(f => f.severity === severity && f.id !== undefined)
|
||||
.map(f => f.id!);
|
||||
setSelectedFindings(prev => {
|
||||
const next = new Set(prev);
|
||||
criticalIds.forEach(id => next.add(id));
|
||||
severityIds.forEach(id => next.add(id));
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const clearSelection = () => {
|
||||
setSelectedFindings(new Set());
|
||||
};
|
||||
|
||||
const toggleExpandFinding = (findingId: string) => {
|
||||
setExpandedFindings(prev => {
|
||||
const next = new Set(prev);
|
||||
@@ -600,49 +600,19 @@ export function ReviewSessionPage() {
|
||||
{/* Fix Progress Carousel */}
|
||||
{sessionId && <FixProgressCarousel sessionId={sessionId} />}
|
||||
|
||||
{/* Filters and Controls */}
|
||||
{/* Unified Filter Card with Dimension Tabs */}
|
||||
<Card>
|
||||
<CardContent className="p-4 space-y-4">
|
||||
{/* Checkbox-style Severity Filters */}
|
||||
<div className="space-y-3">
|
||||
<div className="text-sm font-medium">{formatMessage({ id: 'reviewSession.filters.severity' })}</div>
|
||||
<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]">
|
||||
{/* Top Bar: Search + Sort + Reset */}
|
||||
<div className="flex flex-wrap gap-3 items-center">
|
||||
<div className="relative flex-1 min-w-[180px] max-w-md">
|
||||
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder={formatMessage({ id: 'reviewSession.search.placeholder' })}
|
||||
value={searchQuery}
|
||||
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>
|
||||
<select
|
||||
@@ -658,81 +628,128 @@ export function ReviewSessionPage() {
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc')}
|
||||
className="w-9 p-0"
|
||||
>
|
||||
{sortOrder === 'asc' ? '↑' : '↓'}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={resetFilters}>
|
||||
{formatMessage({ id: 'reviewSession.filters.reset' })}
|
||||
<Button variant="ghost" size="sm" onClick={resetFilters} className="text-muted-foreground hover:text-foreground">
|
||||
✕ {formatMessage({ id: 'reviewSession.filters.reset' })}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Selection Controls */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{formatMessage({ id: 'reviewSession.selection.count' }, { count: selectedFindings.size })}
|
||||
{/* Middle Row: Dimension Tabs + Severity Filters */}
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
{/* Dimension Tabs - Horizontal Scrollable */}
|
||||
<div className="flex-1">
|
||||
<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>
|
||||
<Button variant="outline" size="sm" onClick={toggleSelectAll}>
|
||||
{selectedFindings.size === filteredFindings.length
|
||||
? formatMessage({ id: 'reviewSession.selection.clearAll' })
|
||||
: formatMessage({ id: 'reviewSession.selection.selectAll' })}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={selectVisibleFindings}>
|
||||
{formatMessage({ id: 'reviewSession.selection.selectVisible' })}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => selectBySeverity('critical')}>
|
||||
{formatMessage({ id: 'reviewSession.selection.selectCritical' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setSelectedFindings(new Set())}
|
||||
>
|
||||
{formatMessage({ id: 'reviewSession.selection.clear' })}
|
||||
</Button>
|
||||
{selectedFindings.size > 0 && (
|
||||
<>
|
||||
<Button variant="outline" size="sm" onClick={clearSelection} className="h-8 text-xs">
|
||||
{formatMessage({ id: 'reviewSession.selection.clear' })}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => selectBySeverity('critical')} className="h-8 text-xs">
|
||||
🔥 {formatMessage({ id: 'reviewSession.selection.selectCritical' })}
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={selectVisibleFindings} className="h-8 text-xs">
|
||||
{formatMessage({ id: 'reviewSession.selection.selectVisible' })}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{selectedFindings.size === 0 && (
|
||||
<Button variant="outline" size="sm" onClick={selectAllFindings} className="h-8 text-xs">
|
||||
{formatMessage({ id: 'reviewSession.selection.selectAll' })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="default"
|
||||
variant={selectedFindings.size > 0 ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={exportSelectedAsJson}
|
||||
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' })}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Dimension Tabs */}
|
||||
<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 */}
|
||||
{/* Split Panel: Findings List + Preview */}
|
||||
{filteredFindings.length === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="p-12 text-center">
|
||||
@@ -746,118 +763,274 @@ export function ReviewSessionPage() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{filteredFindings.filter(f => f.id !== undefined).map(finding => {
|
||||
const findingId = finding.id!;
|
||||
const isExpanded = expandedFindings.has(findingId);
|
||||
const isSelected = selectedFindings.has(findingId);
|
||||
const badge = getSeverityBadge(finding.severity);
|
||||
const BadgeIcon = badge.icon;
|
||||
<div className="grid grid-cols-1 md:grid-cols-[minmax(0,45fr)_minmax(0,55fr)] gap-4">
|
||||
{/* Left Panel: Findings List */}
|
||||
<Card>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-sm font-medium">
|
||||
{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 (
|
||||
<Card key={findingId} className={isSelected ? 'ring-2 ring-primary' : ''}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Checkbox */}
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isSelected}
|
||||
onChange={() => toggleSelectFinding(findingId)}
|
||||
className="mt-1"
|
||||
/>
|
||||
return (
|
||||
<div
|
||||
key={findingId}
|
||||
className={`p-3 rounded-lg border cursor-pointer transition-colors ${
|
||||
isPreviewing
|
||||
? 'bg-primary/10 border-primary'
|
||||
: 'bg-background border-border hover:bg-muted'
|
||||
}`}
|
||||
onClick={() => handleFindingClick(findingId)}
|
||||
>
|
||||
<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">
|
||||
{/* Header */}
|
||||
<div
|
||||
className="flex items-start justify-between gap-3 cursor-pointer"
|
||||
onClick={() => toggleExpandFinding(findingId)}
|
||||
>
|
||||
{/* Compact Finding Content */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
||||
<Badge variant={badge.variant} className="gap-1">
|
||||
<div className="flex items-center gap-1.5 mb-1 flex-wrap">
|
||||
<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" />
|
||||
{badge.label}
|
||||
</Badge>
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{finding.dimension}
|
||||
</Badge>
|
||||
{finding.file && (
|
||||
<span className="text-xs text-muted-foreground font-mono">
|
||||
{finding.file}:{finding.line || '?'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h4 className="font-medium text-foreground text-sm">{finding.title}</h4>
|
||||
{finding.description && (
|
||||
<p className="text-sm text-muted-foreground mt-1 line-clamp-2">
|
||||
{finding.description}
|
||||
</p>
|
||||
|
||||
{/* Select Button */}
|
||||
<Button
|
||||
variant={isSelected ? 'default' : 'outline'}
|
||||
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>
|
||||
<Button variant="ghost" size="sm" className="flex-shrink-0">
|
||||
{isExpanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Expanded Content */}
|
||||
{isExpanded && (
|
||||
<div className="mt-4 pt-4 border-t border-border space-y-3">
|
||||
{/* Code Context */}
|
||||
{finding.code_context && (
|
||||
<div>
|
||||
<h5 className="text-xs font-semibold text-foreground mb-1">
|
||||
{formatMessage({ id: 'reviewSession.codeContext' })}
|
||||
</h5>
|
||||
<pre className="text-xs bg-muted p-2 rounded overflow-x-auto">
|
||||
<code>{finding.code_context}</code>
|
||||
</pre>
|
||||
{/* Scrollable Content */}
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
||||
{/* Description */}
|
||||
{finding.description && (
|
||||
<div className="bg-muted/30 rounded-lg p-3">
|
||||
<div className="text-xs font-semibold text-foreground mb-1.5 flex items-center gap-1.5">
|
||||
<span>📝</span>
|
||||
{formatMessage({ id: 'reviewSession.preview.description' })}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-sm text-foreground leading-relaxed">
|
||||
{finding.description}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Root Cause */}
|
||||
{finding.root_cause && (
|
||||
<div>
|
||||
<h5 className="text-xs font-semibold text-foreground mb-1">
|
||||
{formatMessage({ id: 'reviewSession.rootCause' })}
|
||||
</h5>
|
||||
<p className="text-xs text-muted-foreground">{finding.root_cause}</p>
|
||||
{/* Code Context */}
|
||||
{finding.code_context && (
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-foreground mb-1.5 flex items-center gap-1.5">
|
||||
<span>💻</span>
|
||||
{formatMessage({ id: 'reviewSession.preview.codeContext' })}
|
||||
</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 */}
|
||||
{finding.impact && (
|
||||
<div>
|
||||
<h5 className="text-xs font-semibold text-foreground mb-1">
|
||||
{formatMessage({ id: 'reviewSession.impact' })}
|
||||
</h5>
|
||||
<p className="text-xs text-muted-foreground">{finding.impact}</p>
|
||||
{/* Root Cause */}
|
||||
{finding.root_cause && (
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-foreground mb-1.5 flex items-center gap-1.5">
|
||||
<span>🎯</span>
|
||||
{formatMessage({ id: 'reviewSession.preview.rootCause' })}
|
||||
</div>
|
||||
)}
|
||||
<p className="text-sm text-foreground bg-muted/30 rounded-lg p-3 leading-relaxed">
|
||||
{finding.root_cause}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recommendations */}
|
||||
{finding.recommendations && finding.recommendations.length > 0 && (
|
||||
<div>
|
||||
<h5 className="text-xs font-semibold text-foreground mb-1">
|
||||
{formatMessage({ id: 'reviewSession.recommendations' })}
|
||||
</h5>
|
||||
<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>
|
||||
{/* Impact */}
|
||||
{finding.impact && (
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-foreground mb-1.5 flex items-center gap-1.5">
|
||||
<span>⚠️</span>
|
||||
{formatMessage({ id: 'reviewSession.preview.impact' })}
|
||||
</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">
|
||||
{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>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -28,10 +28,19 @@ import { TaskDrawer } from '@/components/shared/TaskDrawer';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Badge } from '@/components/ui/Badge';
|
||||
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';
|
||||
|
||||
// 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
|
||||
*/
|
||||
@@ -159,7 +168,7 @@ export function SessionDetailPage() {
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={session.status === 'completed' ? 'success' : 'secondary'}>
|
||||
{formatMessage({ id: `sessions.status.${session.status}` })}
|
||||
{formatMessage({ id: statusLabelKeys[session.status] })}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -46,6 +46,15 @@ import type { SessionMetadata } from '@/types/store';
|
||||
|
||||
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
|
||||
*/
|
||||
@@ -88,8 +97,13 @@ export function SessionsPage() {
|
||||
const isMutating = isArchiving || isDeleting;
|
||||
|
||||
// Handlers
|
||||
const handleSessionClick = (sessionId: string) => {
|
||||
navigate(`/sessions/${sessionId}`);
|
||||
const handleSessionClick = (sessionId: string, sessionType?: SessionMetadata['type']) => {
|
||||
// Route review sessions to the dedicated review page
|
||||
if (sessionType === 'review') {
|
||||
navigate(`/sessions/${sessionId}/review`);
|
||||
} else {
|
||||
navigate(`/sessions/${sessionId}`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleArchive = async (sessionId: string) => {
|
||||
@@ -225,7 +239,7 @@ export function SessionsPage() {
|
||||
onClick={() => toggleStatusFilter(status)}
|
||||
className="justify-between"
|
||||
>
|
||||
<span>{formatMessage({ id: `sessions.status.${status}` })}</span>
|
||||
<span>{formatMessage({ id: statusLabelKeys[status] })}</span>
|
||||
{statusFilter.includes(status) && (
|
||||
<span className="text-primary">✓</span>
|
||||
)}
|
||||
@@ -254,7 +268,7 @@ export function SessionsPage() {
|
||||
className="cursor-pointer"
|
||||
onClick={() => toggleStatusFilter(status)}
|
||||
>
|
||||
{formatMessage({ id: `sessions.status.${status}` })}
|
||||
{formatMessage({ id: statusLabelKeys[status] })}
|
||||
<X className="ml-1 h-3 w-3" />
|
||||
</Badge>
|
||||
))}
|
||||
@@ -304,8 +318,8 @@ export function SessionsPage() {
|
||||
<SessionCard
|
||||
key={session.session_id}
|
||||
session={session}
|
||||
onClick={handleSessionClick}
|
||||
onView={handleSessionClick}
|
||||
onClick={(sessionId) => handleSessionClick(sessionId, session.type)}
|
||||
onView={(sessionId) => handleSessionClick(sessionId, session.type)}
|
||||
onArchive={handleArchive}
|
||||
onDelete={handleDeleteClick}
|
||||
actionsDisabled={isMutating}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
ListChecks,
|
||||
Code,
|
||||
GitBranch,
|
||||
Zap,
|
||||
Calendar,
|
||||
FileCode,
|
||||
Layers,
|
||||
@@ -198,15 +197,6 @@ export function TaskListTab({ session, onTaskClick }: TaskListTabProps) {
|
||||
// Cast to extended type to access all possible fields
|
||||
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
|
||||
const dependsOn = extTask.depends_on || extTask.context?.depends_on || [];
|
||||
const dependsCount = dependsOn.length;
|
||||
|
||||
Reference in New Issue
Block a user