From a8385e2ea5357cb027240c7756ec1ab4d36091c6 Mon Sep 17 00:00:00 2001 From: catlog22 Date: Tue, 3 Feb 2026 20:58:03 +0800 Subject: [PATCH] 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. --- .../dashboard/widgets/WorkflowTaskWidget.tsx | 437 ++++++------ .../src/components/shared/SessionCard.tsx | 168 +++-- ccw/frontend/src/lib/api.ts | 87 ++- ccw/frontend/src/locales/en/common.json | 66 ++ .../src/locales/en/project-overview.json | 3 +- .../src/locales/en/review-session.json | 28 +- ccw/frontend/src/locales/zh/common.json | 66 ++ .../src/locales/zh/project-overview.json | 3 +- .../src/locales/zh/review-session.json | 68 +- ccw/frontend/src/pages/LiteTaskDetailPage.tsx | 67 +- ccw/frontend/src/pages/LiteTasksPage.tsx | 647 ++++++++++++++---- .../src/pages/ProjectOverviewPage.tsx | 48 +- ccw/frontend/src/pages/ReviewSessionPage.tsx | 551 ++++++++++----- ccw/frontend/src/pages/SessionDetailPage.tsx | 13 +- ccw/frontend/src/pages/SessionsPage.tsx | 26 +- .../src/pages/session-detail/TaskListTab.tsx | 10 - ccw/frontend/src/types/store.ts | 8 +- tmp.json | 0 18 files changed, 1621 insertions(+), 675 deletions(-) create mode 100644 tmp.json diff --git a/ccw/frontend/src/components/dashboard/widgets/WorkflowTaskWidget.tsx b/ccw/frontend/src/components/dashboard/widgets/WorkflowTaskWidget.tsx index 1afc8304..5fab51c1 100644 --- a/ccw/frontend/src/components/dashboard/widgets/WorkflowTaskWidget.tsx +++ b/ccw/frontend/src/components/dashboard/widgets/WorkflowTaskWidget.tsx @@ -5,13 +5,13 @@ import { memo, useMemo, useState, useEffect } from 'react'; import { useIntl } from 'react-intl'; +import { PieChart, Pie, Cell, ResponsiveContainer } from 'recharts'; import { Card } from '@/components/ui/Card'; import { Progress } from '@/components/ui/Progress'; import { Button } from '@/components/ui/Button'; import { Sparkline } from '@/components/charts/Sparkline'; import { useWorkflowStatusCounts, generateMockWorkflowStatusCounts } from '@/hooks/useWorkflowStatusCounts'; import { useDashboardStats } from '@/hooks/useDashboardStats'; -import { useCoordinatorStore } from '@/stores/coordinatorStore'; import { useProjectOverview } from '@/hooks/useProjectOverview'; import { cn } from '@/lib/utils'; import { @@ -21,11 +21,6 @@ import { CheckCircle2, XCircle, Activity, - Play, - Pause, - Square, - Loader2, - AlertCircle, ChevronLeft, ChevronRight, ChevronDown, @@ -40,7 +35,8 @@ import { FileCode, Bug, Sparkles, - BookOpen, + BarChart3, + PieChart as PieChartIcon, } from 'lucide-react'; export interface WorkflowTaskWidgetProps { @@ -175,19 +171,19 @@ function MiniStatCard({ icon: Icon, title, value, variant, sparklineData }: Mini const styles = variantStyles[variant] || variantStyles.default; return ( -
-
+
+
-

{title}

-

{value.toLocaleString()}

+

{title}

+

{value.toLocaleString()}

- +
{sparklineData && sparklineData.length > 0 && ( -
- +
+
)}
@@ -209,28 +205,12 @@ function generateSparklineData(currentValue: number, variance = 0.3): number[] { return data; } -// Orchestrator status icons and colors -const orchestratorStatusConfig: Record = { - idle: { icon: Square, color: 'text-muted-foreground', bg: 'bg-muted' }, - initializing: { icon: Loader2, color: 'text-info', bg: 'bg-info/20' }, - running: { icon: Play, color: 'text-success', bg: 'bg-success/20' }, - paused: { icon: Pause, color: 'text-warning', bg: 'bg-warning/20' }, - completed: { icon: CheckCircle2, color: 'text-success', bg: 'bg-success/20' }, - failed: { icon: XCircle, color: 'text-destructive', bg: 'bg-destructive/20' }, - cancelled: { icon: AlertCircle, color: 'text-muted-foreground', bg: 'bg-muted' }, -}; - function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) { const { formatMessage } = useIntl(); const { data, isLoading } = useWorkflowStatusCounts(); const { stats, isLoading: statsLoading } = useDashboardStats({ refetchInterval: 60000 }); const { projectOverview, isLoading: projectLoading } = useProjectOverview(); - // Get coordinator state - const coordinatorState = useCoordinatorStore(); - const orchestratorConfig = orchestratorStatusConfig[coordinatorState.status] || orchestratorStatusConfig.idle; - const OrchestratorIcon = orchestratorConfig.icon; - const chartData = data || generateMockWorkflowStatusCounts(); const total = chartData.reduce((sum, item) => sum + item.count, 0); @@ -244,11 +224,6 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) { todayActivity: generateSparklineData(stats?.todayActivity ?? 0, 0.6), }), [stats]); - // Calculate orchestrator progress - const orchestratorProgress = coordinatorState.commandChain.length > 0 - ? Math.round((coordinatorState.commandChain.filter(n => n.status === 'completed').length / coordinatorState.commandChain.length) * 100) - : 0; - // Project info expanded state const [projectExpanded, setProjectExpanded] = useState(false); @@ -284,79 +259,79 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) { ) : ( <> {/* Collapsed Header */} -
+
{/* Project Name & Icon */} -
-
- +
+
+
-

+

{projectOverview?.projectName || 'Claude Code Workflow'}

-

+

{projectOverview?.description || 'AI-powered workflow management system'}

{/* Divider */} -
+
{/* Tech Stack Badges */} -
- - +
+ + {projectOverview?.technologyStack?.languages?.[0]?.name || 'TypeScript'} - - + + {projectOverview?.technologyStack?.frameworks?.[0] || 'Node.js'} - - + + {projectOverview?.architecture?.style || 'Modular Monolith'} - {projectOverview?.technologyStack?.buildTools?.[0] && ( - - - {projectOverview.technologyStack.buildTools[0]} + {projectOverview?.technologyStack?.build_tools?.[0] && ( + + + {projectOverview.technologyStack.build_tools[0]} )}
{/* Divider */} -
+
{/* Quick Stats */} -
-
- +
+
+ {projectOverview?.developmentIndex?.feature?.length || 0} {formatMessage({ id: 'projectOverview.devIndex.category.features' })}
-
- +
+ {projectOverview?.developmentIndex?.bugfix?.length || 0} {formatMessage({ id: 'projectOverview.devIndex.category.bugfixes' })}
-
- +
+ {projectOverview?.developmentIndex?.enhancement?.length || 0} {formatMessage({ id: 'projectOverview.devIndex.category.enhancements' })}
{/* Date + Expand Button */} -
- - +
+ + {projectOverview?.initializedAt ? new Date(projectOverview.initializedAt).toLocaleDateString() : new Date().toLocaleDateString()} - + {currentSessionIndex + 1} / {MOCK_SESSIONS.length} -
{/* Session Card (Carousel Item) */} {currentSession && ( -
+
{/* Session Header */}
-
+
{formatMessage({ id: `common.status.${currentSession.status === 'in_progress' ? 'inProgress' : currentSession.status}` })}
-

{currentSession.name}

-

{currentSession.id}

+

{currentSession.name}

+

{currentSession.id}

{/* Description */} {currentSession.description && ( -

+

{currentSession.description}

)} {/* Progress bar */} -
-
+
+
{formatMessage({ id: 'common.labels.progress' })} @@ -655,20 +628,20 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) {
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" />
{/* Tags and Date */} -
+
{currentSession.tags.map((tag) => ( - - + + {tag} ))} - - + + {currentSession.updatedAt}
@@ -676,19 +649,23 @@ function WorkflowTaskWidgetComponent({ className }: WorkflowTaskWidgetProps) { {/* Task List for this Session - Two columns */}
-
- {currentSession.tasks.map((task) => { +
+ {currentSession.tasks.map((task, index) => { const config = taskStatusColors[task.status]; const StatusIcon = config.icon; + const isLastOdd = currentSession.tasks.length % 2 === 1 && index === currentSession.tasks.length - 1; return (
-
- +
+
-

+

{task.name}

diff --git a/ccw/frontend/src/components/shared/SessionCard.tsx b/ccw/frontend/src/components/shared/SessionCard.tsx index 22cb5a6a..d0339f38 100644 --- a/ccw/frontend/src/components/shared/SessionCard.tsx +++ b/ccw/frontend/src/components/shared/SessionCard.tsx @@ -76,18 +76,18 @@ const statusLabelKeys: Record = { paused: 'sessions.status.paused', }; -// Type variant configuration for session type badges +// Type variant configuration for session type badges (unique colors for each type) const typeVariantConfig: Record< SessionMetadata['type'], - { variant: 'default' | 'secondary' | 'destructive' | 'success' | 'warning' | 'info'; icon: React.ElementType } + { variant: 'default' | 'secondary' | 'destructive' | 'success' | 'warning' | 'info' | 'review'; icon: React.ElementType } > = { - review: { variant: 'info', icon: Search }, - 'tdd': { variant: 'success', icon: TestTube }, - test: { variant: 'default', icon: FileText }, - docs: { variant: 'warning', icon: File }, - workflow: { variant: 'secondary', icon: Settings }, - 'lite-plan': { variant: 'default', icon: FileText }, - 'lite-fix': { variant: 'warning', icon: Zap }, + review: { variant: 'review', icon: Search }, // Purple + 'tdd': { variant: 'success', icon: TestTube }, // Green + test: { variant: 'info', icon: FileText }, // Blue + docs: { variant: 'warning', icon: File }, // Orange/Yellow + workflow: { variant: 'default', icon: Settings }, // Primary (blue-violet) + 'lite-plan': { variant: 'secondary', icon: FileText }, // Gray/Neutral + 'lite-fix': { variant: 'destructive', icon: Zap }, // Red }; // Type label keys for i18n @@ -149,6 +149,43 @@ function calculateProgress(tasks: SessionMetadata['tasks']): TaskStatusBreakdown return { total, completed, failed, pending, inProgress, percentage }; } +/** + * Severity breakdown for review sessions + */ +interface SeverityBreakdown { + total: number; + critical: number; + high: number; + medium: number; + low: number; +} + +/** + * Calculate severity breakdown from review dimensions + */ +function calculateSeverityBreakdown(review: SessionMetadata['review']): SeverityBreakdown { + if (!review?.dimensions || review.dimensions.length === 0) { + return { total: 0, critical: 0, high: 0, medium: 0, low: 0 }; + } + + let critical = 0, high = 0, medium = 0, low = 0; + + review.dimensions.forEach(dim => { + if (dim.findings) { + dim.findings.forEach(finding => { + const severity = finding.severity?.toLowerCase(); + if (severity === 'critical') critical++; + else if (severity === 'high') high++; + else if (severity === 'medium') medium++; + else if (severity === 'low') low++; + }); + } + }); + + const total = critical + high + medium + low; + return { total, critical, high, medium, low }; +} + /** * SessionCard component for displaying session information * @@ -188,6 +225,7 @@ export function SessionCard({ : null; const progress = calculateProgress(session.tasks); + const severity = calculateSeverityBreakdown(session.review); const isPlanning = session.status === 'planning'; const isArchived = session.status === 'archived' || session.location === 'archived'; @@ -227,21 +265,24 @@ export function SessionCard({ onClick={handleCardClick} > - {/* Header - Session ID as title */} + {/* Header - Type badge + Session ID as title */}
-

- {session.session_id} -

+
+ {/* Type badge BEFORE title */} + {typeConfig && typeLabel && ( + + + {typeLabel} + + )} +

+ {session.session_id} +

+
{statusLabel} - {typeConfig && typeLabel && ( - - - {typeLabel} - - )} {showActions && ( @@ -291,21 +332,46 @@ export function SessionCard({

)} - {/* Meta info - enriched */} + {/* Meta info - different based on session type */}
{formatDate(session.created_at)} - - - {progress.total} {formatMessage({ id: 'sessions.card.tasks' })} - - {progress.total > 0 && ( - - - {progress.completed} {formatMessage({ id: 'sessions.card.completed' })} - + + {/* Review sessions: Show findings and dimensions */} + {session.type === 'review' ? ( + <> + {session.review?.dimensions && session.review.dimensions.length > 0 && ( + + + {session.review.dimensions.length} {formatMessage({ id: 'sessions.card.dimensions' })} + + )} + {session.review?.findings !== undefined && ( + + + {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' })} + + )} + + ) : ( + <> + {/* Workflow/other sessions: Show tasks */} + + + {progress.total} {formatMessage({ id: 'sessions.card.tasks' })} + + {progress.total > 0 && ( + + + {progress.completed} {formatMessage({ id: 'sessions.card.completed' })} + + )} + )} {session.updated_at && session.updated_at !== session.created_at && ( @@ -315,15 +381,9 @@ export function SessionCard({ )}
- {/* Task status badges */} - {progress.total > 0 && ( + {/* Task status badges - only for non-review sessions */} + {session.type !== 'review' && progress.total > 0 && (
- {progress.pending > 0 && ( - - - {progress.pending} {formatMessage({ id: 'sessions.taskStatus.pending' })} - - )} {progress.inProgress > 0 && ( @@ -345,8 +405,38 @@ export function SessionCard({
)} - {/* Progress bar (only show if not planning and has tasks) */} - {progress.total > 0 && !isPlanning && ( + {/* Severity badges - only for review sessions */} + {session.type === 'review' && severity.total > 0 && ( +
+ {severity.critical > 0 && ( + + + {severity.critical} Critical + + )} + {severity.high > 0 && ( + + + {severity.high} High + + )} + {severity.medium > 0 && ( + + + {severity.medium} Medium + + )} + {severity.low > 0 && ( + + + {severity.low} Low + + )} +
+ )} + + {/* Progress bar (only show for non-review sessions with tasks) */} + {session.type !== 'review' && progress.total > 0 && !isPlanning && (
{formatMessage({ id: 'sessions.card.progress' })} diff --git a/ccw/frontend/src/lib/api.ts b/ccw/frontend/src/lib/api.ts index 2351dd19..e2d0aa64 100644 --- a/ccw/frontend/src/lib/api.ts +++ b/ccw/frontend/src/lib/api.ts @@ -248,10 +248,54 @@ function transformBackendSession( } // Preserve type field from backend, or infer from session_id pattern - // Multi-level type detection: backend.type > infer from name - const sessionType = (backendSession.type as SessionMetadata['type']) || + // Multi-level type detection: backend.type > hasReview (for review sessions) > infer from name + let sessionType = (backendSession.type as SessionMetadata['type']) || 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; + 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 { session_id: backendSession.session_id, type: sessionType, @@ -265,8 +309,8 @@ function transformBackendSession( // Preserve additional fields if they exist 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, - has_review: (backendSession as unknown as { has_review?: boolean }).has_review, - review: (backendSession as unknown as { review?: SessionMetadata['review'] }).review, + has_review: backendData.hasReview, + review, summaries: (backendSession as unknown as { summaries?: SessionMetadata['summaries'] }).summaries, tasks: (backendSession as unknown as { tasks?: TaskData[] }).tasks, }; @@ -1904,6 +1948,14 @@ export interface ReviewSession { export interface ReviewSessionsResponse { reviewSessions?: ReviewSession[]; + reviewData?: { + sessions?: Array<{ + session_id: string; + dimensions: Array<{ name: string; findings?: Array }>; + findings?: Array; + progress?: unknown; + }>; + }; } /** @@ -1911,7 +1963,32 @@ export interface ReviewSessionsResponse { */ export async function fetchReviewSessions(): Promise { const data = await fetchApi('/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 []; } /** diff --git a/ccw/frontend/src/locales/en/common.json b/ccw/frontend/src/locales/en/common.json index ef30c87b..382c5ded 100644 --- a/ccw/frontend/src/locales/en/common.json +++ b/ccw/frontend/src/locales/en/common.json @@ -61,6 +61,7 @@ "inProgress": "In Progress", "running": "Running", "initializing": "Initializing", + "initialized": "Initialized", "planning": "Planning", "completed": "Completed", "failed": "Failed", @@ -264,6 +265,71 @@ "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": { "defaultTitle": "Questions", "description": "Please answer the following questions", diff --git a/ccw/frontend/src/locales/en/project-overview.json b/ccw/frontend/src/locales/en/project-overview.json index 549500cf..0694e8fd 100644 --- a/ccw/frontend/src/locales/en/project-overview.json +++ b/ccw/frontend/src/locales/en/project-overview.json @@ -17,7 +17,8 @@ "title": "Architecture", "style": "Style", "layers": "Layers", - "patterns": "Patterns" + "patterns": "Patterns", + "principles": "Principles" }, "components": { "title": "Key Components", diff --git a/ccw/frontend/src/locales/en/review-session.json b/ccw/frontend/src/locales/en/review-session.json index 1e5f4dd8..f1a08afe 100644 --- a/ccw/frontend/src/locales/en/review-session.json +++ b/ccw/frontend/src/locales/en/review-session.json @@ -5,7 +5,13 @@ "critical": "Critical", "high": "High", "medium": "Medium", - "low": "Low" + "low": "Low", + "short": { + "critical": "Crit", + "high": "High", + "medium": "Med", + "low": "Low" + } }, "stats": { "total": "Total", @@ -22,6 +28,7 @@ }, "filters": { "severity": "Severity", + "dimension": "Dimension", "sort": "Sort", "reset": "Reset" }, @@ -35,6 +42,8 @@ }, "selection": { "count": "{count} selected", + "countSelected": "{count} selected", + "total": "{count} total", "selectAll": "Select All", "clearAll": "Clear All", "clear": "Clear", @@ -46,6 +55,23 @@ "rootCause": "Root Cause", "impact": "Impact", "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": { "title": "Fix Progress", "phase": { diff --git a/ccw/frontend/src/locales/zh/common.json b/ccw/frontend/src/locales/zh/common.json index 069d803f..51641162 100644 --- a/ccw/frontend/src/locales/zh/common.json +++ b/ccw/frontend/src/locales/zh/common.json @@ -65,6 +65,7 @@ "inProgress": "进行中", "running": "运行中", "initializing": "初始化中", + "initialized": "已初始化", "planning": "规划中", "completed": "已完成", "failed": "失败", @@ -258,6 +259,71 @@ "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": { "defaultTitle": "问题", "description": "请回答以下问题", diff --git a/ccw/frontend/src/locales/zh/project-overview.json b/ccw/frontend/src/locales/zh/project-overview.json index 6777dbad..d40b6555 100644 --- a/ccw/frontend/src/locales/zh/project-overview.json +++ b/ccw/frontend/src/locales/zh/project-overview.json @@ -17,7 +17,8 @@ "title": "架构", "style": "架构风格", "layers": "分层", - "patterns": "设计模式" + "patterns": "设计模式", + "principles": "设计原则" }, "components": { "title": "核心组件", diff --git a/ccw/frontend/src/locales/zh/review-session.json b/ccw/frontend/src/locales/zh/review-session.json index 1fc85f23..10d13650 100644 --- a/ccw/frontend/src/locales/zh/review-session.json +++ b/ccw/frontend/src/locales/zh/review-session.json @@ -5,12 +5,33 @@ "critical": "严重", "high": "高", "medium": "中", - "low": "低" + "low": "低", + "short": { + "critical": "严重", + "high": "高", + "medium": "中", + "low": "低" + } }, "stats": { "total": "总发现", "dimensions": "维度" }, + "progress": { + "title": "审查进度", + "totalFindings": "总发现", + "critical": "严重", + "high": "高" + }, + "dimensionTabs": { + "all": "全部" + }, + "filters": { + "severity": "严重程度", + "dimension": "维度", + "sort": "排序", + "reset": "重置" + }, "search": { "placeholder": "搜索发现..." }, @@ -21,18 +42,59 @@ }, "selection": { "count": "已选择 {count} 项", + "countSelected": "已选 {count} 项", + "total": "共 {count} 项", "selectAll": "全选", "clearAll": "清除全部", - "clear": "清除" + "clear": "清除", + "selectVisible": "可见", + "selectCritical": "严重" }, "export": "导出修复 JSON", "codeContext": "代码上下文", "rootCause": "根本原因", "impact": "影响", "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": { "title": "未找到发现", - "message": "尝试调整筛选条件或搜索查询。" + "message": "尝试调整筛选条件或搜索查询。", + "noFixProgress": "无修复进度数据" }, "notFound": { "title": "未找到审查会话", diff --git a/ccw/frontend/src/pages/LiteTaskDetailPage.tsx b/ccw/frontend/src/pages/LiteTaskDetailPage.tsx index 1d8802ec..07dcce2a 100644 --- a/ccw/frontend/src/pages/LiteTaskDetailPage.tsx +++ b/ccw/frontend/src/pages/LiteTaskDetailPage.tsx @@ -428,49 +428,34 @@ export function LiteTaskDetailPage() {
{/* Right: Meta Information */} -
- {/* Row 1: Status Badge */} - - {task.status} - +
+ {/* Dependencies - show task IDs */} + {task.context?.depends_on && task.context.depends_on.length > 0 && ( +
+ + {task.context.depends_on.map((depId, idx) => ( + + {depId} + + ))} +
+ )} - {/* Row 2: Metadata */} -
- {/* Dependencies Count */} - {task.context?.depends_on && task.context.depends_on.length > 0 && ( - - {task.context.depends_on.length} - dep{task.context.depends_on.length > 1 ? 's' : ''} - - )} + {/* Target Files Count */} + {task.flow_control?.target_files && task.flow_control.target_files.length > 0 && ( + + {task.flow_control.target_files.length} + file{task.flow_control.target_files.length > 1 ? 's' : ''} + + )} - {/* Target Files Count */} - {task.flow_control?.target_files && task.flow_control.target_files.length > 0 && ( - - {task.flow_control.target_files.length} - file{task.flow_control.target_files.length > 1 ? 's' : ''} - - )} - - {/* Focus Paths Count */} - {task.context?.focus_paths && task.context.focus_paths.length > 0 && ( - - {task.context.focus_paths.length} - focus - - )} - - {/* Acceptance Criteria Count */} - {task.context?.acceptance && task.context.acceptance.length > 0 && ( - - {task.context.acceptance.length} - criteria - - )} -
+ {/* Implementation Steps Count */} + {task.flow_control?.implementation_approach && task.flow_control.implementation_approach.length > 0 && ( + + {task.flow_control.implementation_approach.length} + step{task.flow_control.implementation_approach.length > 1 ? 's' : ''} + + )}
diff --git a/ccw/frontend/src/pages/LiteTasksPage.tsx b/ccw/frontend/src/pages/LiteTasksPage.tsx index e9b68ceb..ef324b0d 100644 --- a/ccw/frontend/src/pages/LiteTasksPage.tsx +++ b/ccw/frontend/src/pages/LiteTasksPage.tsx @@ -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({ }} > -
- - {task.task_id || `#${index + 1}`} - -

- {task.title || formatMessage({ id: 'liteTasks.untitled' })} -

- {task.status && ( - - {task.status} +
+
+ + {task.task_id || `#${index + 1}`} - )} +

+ {task.title || formatMessage({ id: 'liteTasks.untitled' })} +

+
+ {/* Right: Meta info */} +
+ {/* Dependencies - show task IDs */} + {task.context?.depends_on && task.context.depends_on.length > 0 && ( +
+ + {task.context.depends_on.map((depId, idx) => ( + + {depId} + + ))} +
+ )} + {/* Target Files Count */} + {task.flow_control?.target_files && task.flow_control.target_files.length > 0 && ( + + {task.flow_control.target_files.length} + file{task.flow_control.target_files.length > 1 ? 's' : ''} + + )} +
{task.description && (

@@ -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('tasks'); + const [contextData, setContextData] = React.useState(null); + const [contextLoading, setContextLoading] = React.useState(false); + const [contextError, setContextError] = React.useState(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 = {}; + const taskIds = new Set(); + + 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 ( +

+ {/* Session Info Header */} +
+ {session.createdAt && ( + + + {formatMessage({ id: 'liteTasks.createdAt' })}: {new Date(session.createdAt).toLocaleDateString()} + + )} + + + {formatMessage({ id: 'liteTasks.quickCards.tasks' })}: {taskCount} {formatMessage({ id: 'liteTasks.tasksCount' })} + +
+ + {/* Tab Buttons */} +
+ + + + +
+ + {/* Tasks Tab */} + {activeTab === 'tasks' && ( +
+ {/* Goal/Solution/Implementation Header */} + {(goal || solution || implementationChain) && ( + + + {goal && ( +
+ {formatMessage({ id: 'liteTasks.multiCli.goal' })}: + {goal} +
+ )} + {solution && ( +
+ {formatMessage({ id: 'liteTasks.multiCli.solution' })}: + {solution} +
+ )} + {implementationChain && ( +
+ {formatMessage({ id: 'liteTasks.multiCli.implementation' })}: + + {implementationChain} + +
+ )} + {(feasibility > 0 || effort || risk) && ( +
+ {feasibility > 0 && ( + {feasibility}% + )} + {effort && ( + {effort} + )} + {risk && ( + + {risk} {formatMessage({ id: 'liteTasks.multiCli.risk' })} + + )} +
+ )} +
+
+ )} + + {/* 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 ( + { + e.stopPropagation(); + onTaskClick(task); + }} + > + +
+
+ + {task.task_id || `T${index + 1}`} + +
+

+ {task.title || formatMessage({ id: 'liteTasks.untitled' })} +

+ {/* Meta badges */} +
+ {task.meta?.type && ( + {task.meta.type} + )} + {filesCount > 0 && ( + + + {filesCount} files + + )} + {stepsCount > 0 && ( + + {stepsCount} steps + + )} + {criteriaCount > 0 && ( + + {criteriaCount} criteria + + )} + {depsCount > 0 && ( + + {depsCount} deps + + )} +
+
+
+
+
+
+ ); + })} +
+ )} + + {/* Discussion Tab */} + {activeTab === 'discussion' && ( +
+ + +
+ +

+ {formatMessage({ id: 'liteTasks.multiCli.discussionRounds' })} +

+ {roundCount} {formatMessage({ id: 'liteTasks.rounds' })} +
+

+ {formatMessage({ id: 'liteTasks.multiCli.discussionDescription' })} +

+ {goal && ( +
+

{goal}

+
+ )} +
+
+
+ )} + + {/* Context Tab */} + {activeTab === 'context' && ( +
+ {contextLoading && ( +
+ + {formatMessage({ id: 'liteTasks.contextPanel.loading' })} +
+ )} + {contextError && !contextLoading && ( +
+ + {formatMessage({ id: 'liteTasks.contextPanel.error' })}: {contextError} +
+ )} + {!contextLoading && !contextError && contextData && ( + + )} + {!contextLoading && !contextError && !contextData && !session.path && ( +
+ +

+ {formatMessage({ id: 'liteTasks.contextPanel.empty' })} +

+
+ )} +
+ )} + + {/* Summary Tab */} + {activeTab === 'summary' && ( +
+ + +
+ +

+ {formatMessage({ id: 'liteTasks.multiCli.planSummary' })} +

+
+ {goal && ( +
+

{formatMessage({ id: 'liteTasks.multiCli.goal' })}

+

{goal}

+
+ )} + {solution && ( +
+

{formatMessage({ id: 'liteTasks.multiCli.solution' })}

+

{solution}

+
+ )} + {implementationChain && ( +
+

{formatMessage({ id: 'liteTasks.multiCli.implementation' })}

+ + {implementationChain} + +
+ )} +
+ {formatMessage({ id: 'liteTasks.quickCards.tasks' })}: + {taskCount} + {feasibility > 0 && ( + <> + {formatMessage({ id: 'liteTasks.multiCli.feasibility' })}: + {feasibility}% + + )} +
+
+
+
+ )} +
+ ); +} + /** * 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 (
@@ -552,12 +963,6 @@ export function LiteTasksPage() { {taskStats.blocked} {formatMessage({ id: 'liteTasks.status.blocked' })} )} - {taskStats.pending > 0 && ( - - - {taskStats.pending} {formatMessage({ id: 'liteTasks.status.pending' })} - - )}
{/* 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 ( - setExpandedSessionId(expandedSessionId === session.id ? null : session.id)} - > - -
-
-
- {expandedSessionId === session.id ? ( - - ) : ( - +
+ setExpandedSessionId(isExpanded ? null : session.id)} + > + +
+
+
+ {isExpanded ? ( + + ) : ( + + )} +
+
+

{session.id}

+
+
+ + + {formatMessage({ id: 'liteTasks.type.multiCli' })} + +
+
+ + {topicTitle} +
+ + {/* Task status distribution for multi-cli */} + {taskStats.total > 0 && ( +
+ {taskStats.completed > 0 && ( + + + {taskStats.completed} {formatMessage({ id: 'liteTasks.status.completed' })} + + )} + {taskStats.inProgress > 0 && ( + + + {taskStats.inProgress} {formatMessage({ id: 'liteTasks.status.inProgress' })} + + )} + {taskStats.blocked > 0 && ( + + + {taskStats.blocked} {formatMessage({ id: 'liteTasks.status.blocked' })} + )}
-
-

{session.id}

-
-
- - - {formatMessage({ id: 'liteTasks.type.multiCli' })} - -
-
- - {topicTitle} -
- - {/* Task status distribution for multi-cli */} - {taskStats.total > 0 && ( -
- {taskStats.completed > 0 && ( - - - {taskStats.completed} {formatMessage({ id: 'liteTasks.status.completed' })} - - )} - {taskStats.inProgress > 0 && ( - - - {taskStats.inProgress} {formatMessage({ id: 'liteTasks.status.inProgress' })} - - )} - {taskStats.blocked > 0 && ( - - - {taskStats.blocked} {formatMessage({ id: 'liteTasks.status.blocked' })} - - )} - {taskStats.pending > 0 && ( - - - {taskStats.pending} {formatMessage({ id: 'liteTasks.status.pending' })} - - )} -
- )} - -
- {createdAt && ( - - - {new Date(createdAt).toLocaleDateString()} - )} - - - {roundCount} {formatMessage({ id: 'liteTasks.rounds' })} - - - - {status} - -
- - + +
+ {createdAt && ( + + + {new Date(createdAt).toLocaleDateString()} + + )} + + + {roundCount} {formatMessage({ id: 'liteTasks.rounds' })} + + + + {status} + +
+ + + + {/* Expanded multi-cli panel with tabs */} + {isExpanded && ( + + )} +
); }; diff --git a/ccw/frontend/src/pages/ProjectOverviewPage.tsx b/ccw/frontend/src/pages/ProjectOverviewPage.tsx index 9ec4051e..1f30ca8f 100644 --- a/ccw/frontend/src/pages/ProjectOverviewPage.tsx +++ b/ccw/frontend/src/pages/ProjectOverviewPage.tsx @@ -649,26 +649,26 @@ export function ProjectOverviewPage() { {/* Guidelines */} {guidelines && ( - -
-

- + +
+

+ {formatMessage({ id: 'projectOverview.guidelines.title' })}

-
+
{!isEditMode ? ( - ) : ( <> - - @@ -676,17 +676,17 @@ export function ProjectOverviewPage() {
-
+
{!isEditMode ? ( <> {/* Read-only Mode - Conventions */} {guidelines.conventions && (
-

- +

+ {formatMessage({ id: 'projectOverview.guidelines.conventions' })}

-
+
{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) => (
- + {key} - {item} + {item}
))}
@@ -713,11 +713,11 @@ export function ProjectOverviewPage() { {/* Read-only Mode - Constraints */} {guidelines.constraints && (
-

- +

+ {formatMessage({ id: 'projectOverview.guidelines.constraints' })}

-
+
{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) => (
- + {key} - {item} + {item}
))}
diff --git a/ccw/frontend/src/pages/ReviewSessionPage.tsx b/ccw/frontend/src/pages/ReviewSessionPage.tsx index 9a3fac4b..c39245bd 100644 --- a/ccw/frontend/src/pages/ReviewSessionPage.tsx +++ b/ccw/frontend/src/pages/ReviewSessionPage.tsx @@ -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 && } - {/* Filters and Controls */} + {/* Unified Filter Card with Dimension Tabs */} - {/* Checkbox-style Severity Filters */} -
-
{formatMessage({ id: 'reviewSession.filters.severity' })}
-
- {(['critical', 'high', 'medium', 'low'] as const).map(severity => { - const isEnabled = severityFilter.has(severity); - return ( - - ); - })} -
-
- - {/* Search and Sort */} -
-
+ {/* Top Bar: Search + Sort + Reset */} +
+
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" />
toggleSeverity(severity)} + className="sr-only" + /> + {formatMessage({ id: `reviewSession.severity.short.${severity}` })} + ({flattenedFindings.filter(f => f.severity === severity).length}) + + ); + })} +
+
+
+ + {/* Bottom Bar: Selection Actions + Export */} +
+
+ + {selectedFindings.size > 0 + ? formatMessage({ id: 'reviewSession.selection.countSelected' }, { count: selectedFindings.size }) + : formatMessage({ id: 'reviewSession.selection.total' }, { count: filteredFindings.length }) + } - - - - + {selectedFindings.size > 0 && ( + <> + + + + + )} + {selectedFindings.size === 0 && ( + + )}
- {/* Dimension Tabs */} -
- - {dimensions.map(dim => ( - - ))} -
- - {/* Findings List */} + {/* Split Panel: Findings List + Preview */} {filteredFindings.length === 0 ? ( @@ -746,118 +763,274 @@ export function ReviewSessionPage() { ) : ( -
- {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; +
+ {/* Left Panel: Findings List */} + + +
+ + {formatMessage({ id: 'reviewSession.findingsList.count' }, { count: filteredFindings.length })} + +
+
+ {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 ( - - -
- {/* Checkbox */} - toggleSelectFinding(findingId)} - className="mt-1" - /> + return ( +
handleFindingClick(findingId)} + > +
+ {/* Checkbox */} + { + e.stopPropagation(); + toggleSelectFinding(findingId); + }} + className="mt-0.5 flex-shrink-0" + /> -
- {/* Header */} -
toggleExpandFinding(findingId)} - > + {/* Compact Finding Content */}
-
- +
+ + + {badge.label} + + + {finding.dimension} + +
+

+ {finding.title} +

+ {finding.file && ( +

+ {finding.file}:{finding.line || '?'} +

+ )} +
+
+
+ ); + })} +
+ + + + {/* Right Panel: Enhanced Preview */} + + + {!selectedFindingId ? ( + // Enhanced Empty State +
+
+ +
+

+ {formatMessage({ id: 'reviewSession.preview.emptyTitle' })} +

+

+ {formatMessage({ id: 'reviewSession.preview.empty' })} +

+
+
+ + {formatMessage({ id: 'reviewSession.preview.emptyTipSeverity' })} +
+
+ 📁 + {formatMessage({ id: 'reviewSession.preview.emptyTipFile' })} +
+
+
+ ) : ( + // 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 ( +
+ {/* Sticky Header */} +
+ {/* Navigation + Badges Row */} +
+ {/* Navigation Buttons */} +
+ + + {findingIndex + 1} / {filteredFindings.length} + + +
+ + {/* Badges */} +
+ {badge.label} {finding.dimension} - {finding.file && ( - - {finding.file}:{finding.line || '?'} - - )}
-

{finding.title}

- {finding.description && ( -

- {finding.description} -

+ + {/* Select Button */} + +
+ + {/* Title */} +

+ {finding.title} +

+ + {/* Quick Info Bar */} +
+ {finding.file && ( +
+ 📁 + {finding.file}:{finding.line || '?'} +
)}
-
- {/* Expanded Content */} - {isExpanded && ( -
- {/* Code Context */} - {finding.code_context && ( -
-
- {formatMessage({ id: 'reviewSession.codeContext' })} -
-
-                                {finding.code_context}
-                              
+ {/* Scrollable Content */} +
+ {/* Description */} + {finding.description && ( +
+
+ 📝 + {formatMessage({ id: 'reviewSession.preview.description' })}
- )} +

+ {finding.description} +

+
+ )} - {/* Root Cause */} - {finding.root_cause && ( -
-
- {formatMessage({ id: 'reviewSession.rootCause' })} -
-

{finding.root_cause}

+ {/* Code Context */} + {finding.code_context && ( +
+
+ 💻 + {formatMessage({ id: 'reviewSession.preview.codeContext' })}
- )} +
+                              {finding.code_context}
+                            
+
+ )} - {/* Impact */} - {finding.impact && ( -
-
- {formatMessage({ id: 'reviewSession.impact' })} -
-

{finding.impact}

+ {/* Root Cause */} + {finding.root_cause && ( +
+
+ 🎯 + {formatMessage({ id: 'reviewSession.preview.rootCause' })}
- )} +

+ {finding.root_cause} +

+
+ )} - {/* Recommendations */} - {finding.recommendations && finding.recommendations.length > 0 && ( -
-
- {formatMessage({ id: 'reviewSession.recommendations' })} -
-
    - {finding.recommendations.map((rec, idx) => ( -
  • - - {rec} -
  • - ))} -
+ {/* Impact */} + {finding.impact && ( +
+
+ ⚠️ + {formatMessage({ id: 'reviewSession.preview.impact' })}
- )} -
- )} +

+ {finding.impact} +

+
+ )} + + {/* Recommendations */} + {finding.recommendations && finding.recommendations.length > 0 && ( +
+
+ + {formatMessage({ id: 'reviewSession.preview.recommendations' })} +
+
    + {finding.recommendations.map((rec, idx) => ( +
  • + + {rec} +
  • + ))} +
+
+ )} +
-
- - - ); - })} + ); + })() + )} + +
)}
diff --git a/ccw/frontend/src/pages/SessionDetailPage.tsx b/ccw/frontend/src/pages/SessionDetailPage.tsx index 66f89248..07465e38 100644 --- a/ccw/frontend/src/pages/SessionDetailPage.tsx +++ b/ccw/frontend/src/pages/SessionDetailPage.tsx @@ -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 = { + 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() {
- {formatMessage({ id: `sessions.status.${session.status}` })} + {formatMessage({ id: statusLabelKeys[session.status] })}
diff --git a/ccw/frontend/src/pages/SessionsPage.tsx b/ccw/frontend/src/pages/SessionsPage.tsx index d37de190..75de5b0d 100644 --- a/ccw/frontend/src/pages/SessionsPage.tsx +++ b/ccw/frontend/src/pages/SessionsPage.tsx @@ -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 = { + 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" > - {formatMessage({ id: `sessions.status.${status}` })} + {formatMessage({ id: statusLabelKeys[status] })} {statusFilter.includes(status) && ( )} @@ -254,7 +268,7 @@ export function SessionsPage() { className="cursor-pointer" onClick={() => toggleStatusFilter(status)} > - {formatMessage({ id: `sessions.status.${status}` })} + {formatMessage({ id: statusLabelKeys[status] })} ))} @@ -304,8 +318,8 @@ export function SessionsPage() { handleSessionClick(sessionId, session.type)} + onView={(sessionId) => handleSessionClick(sessionId, session.type)} onArchive={handleArchive} onDelete={handleDeleteClick} actionsDisabled={isMutating} diff --git a/ccw/frontend/src/pages/session-detail/TaskListTab.tsx b/ccw/frontend/src/pages/session-detail/TaskListTab.tsx index 1816e418..97099c60 100644 --- a/ccw/frontend/src/pages/session-detail/TaskListTab.tsx +++ b/ccw/frontend/src/pages/session-detail/TaskListTab.tsx @@ -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 = { - 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; diff --git a/ccw/frontend/src/types/store.ts b/ccw/frontend/src/types/store.ts index 9c28317f..567be695 100644 --- a/ccw/frontend/src/types/store.ts +++ b/ccw/frontend/src/types/store.ts @@ -182,9 +182,11 @@ export interface SessionMetadata { plan_updated_at?: string; has_review?: boolean; review?: { - dimensions: string[]; - iterations: string[]; - fixes: string[]; + dimensions: Array<{ name: string; findings?: Array<{ severity?: string }> }>; + dimensions_count?: number; + findings?: number; + iterations?: string[]; + fixes?: string[]; }; summaries?: Array<{ task_id: string; content: unknown }>; tasks?: TaskData[]; diff --git a/tmp.json b/tmp.json new file mode 100644 index 00000000..e69de29b