Files
Claude-Code-Workflow/ccw/frontend/src/pages/LiteTasksPage.tsx
catlog22 37ba849e75 feat: add CLI Viewer Page with multi-pane layout and state management
- Implemented the CliViewerPage component for displaying CLI outputs in a configurable multi-pane layout.
- Integrated Zustand for state management, allowing for dynamic layout changes and tab management.
- Added layout options: single, split horizontal, split vertical, and 2x2 grid.
- Created viewerStore for managing layout, panes, and tabs, including actions for adding/removing panes and tabs.
- Added CoordinatorPage barrel export for easier imports.
2026-02-03 17:28:26 +08:00

926 lines
35 KiB
TypeScript

// ========================================
// LiteTasksPage Component
// ========================================
// Lite-plan and lite-fix task list page with TaskDrawer
import * as React from 'react';
import { useIntl } from 'react-intl';
import {
ArrowLeft,
Zap,
Wrench,
FileEdit,
MessagesSquare,
Calendar,
XCircle,
Activity,
Repeat,
MessageCircle,
ChevronDown,
ChevronRight,
Search,
SortAsc,
SortDesc,
ListFilter,
Hash,
ListChecks,
Package,
Loader2,
Compass,
Stethoscope,
FolderOpen,
FileText,
CheckCircle2,
Clock,
AlertCircle,
} from 'lucide-react';
import { useLiteTasks } from '@/hooks/useLiteTasks';
import { Button } from '@/components/ui/Button';
import { Badge } from '@/components/ui/Badge';
import { Card, CardContent } from '@/components/ui/Card';
import { Tabs, TabsContent } from '@/components/ui/Tabs';
import { TabsNavigation, type TabItem } from '@/components/ui/TabsNavigation';
import { TaskDrawer } from '@/components/shared/TaskDrawer';
import { fetchLiteSessionContext, type LiteTask, type LiteTaskSession, type LiteSessionContext } from '@/lib/api';
import { useNavigate } from 'react-router-dom';
type LiteTaskTab = 'lite-plan' | 'lite-fix' | 'multi-cli-plan';
type SortField = 'date' | 'name' | 'tasks';
type SortOrder = 'asc' | 'desc';
/**
* Get i18n text from label object (supports {en, zh} format)
* Note: fallback should be provided dynamically from component context
*/
function getI18nText(label: string | { en?: string; zh?: string } | undefined): string | undefined {
if (!label) return undefined;
if (typeof label === 'string') return label;
return label.en || label.zh;
}
type ExpandedTab = 'tasks' | 'context';
/**
* ExpandedSessionPanel - Multi-tab panel shown when a lite session is expanded
*/
function ExpandedSessionPanel({
session,
onTaskClick,
}: {
session: LiteTaskSession;
onTaskClick: (task: LiteTask) => void;
}) {
const { formatMessage } = useIntl();
const [activeTab, setActiveTab] = React.useState<ExpandedTab>('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;
// Load context data lazily when context tab is selected
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]);
return (
<div className="mt-2 ml-6 pb-2">
{/* Quick Info Cards */}
<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('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>
</div>
{/* Tasks Tab */}
{activeTab === 'tasks' && (
<div className="space-y-2">
{tasks.map((task, index) => (
<Card
key={task.id || index}
className="cursor-pointer hover:shadow-sm hover:border-primary/50 transition-all border-border"
onClick={(e) => {
e.stopPropagation();
onTaskClick(task);
}}
>
<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}
</Badge>
)}
</div>
{task.description && (
<p className="text-xs text-muted-foreground mt-1.5 pl-[calc(1.5rem+0.75rem)] line-clamp-2">
{task.description}
</p>
)}
</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>
)}
</div>
);
}
/**
* ContextContent - Renders the context data sections
*/
function ContextContent({
contextData,
session,
}: {
contextData: LiteSessionContext;
session: LiteTaskSession;
}) {
const { formatMessage } = useIntl();
const plan = session.plan || {};
const hasExplorations = !!(contextData.explorations?.manifest);
const hasDiagnoses = !!(contextData.diagnoses?.manifest || contextData.diagnoses?.items?.length);
const hasContext = !!contextData.context;
const hasFocusPaths = !!(plan.focus_paths as string[] | undefined)?.length;
const hasSummary = !!(plan.summary as string | undefined);
const hasAnyContent = hasExplorations || hasDiagnoses || hasContext || hasFocusPaths || hasSummary;
if (!hasAnyContent) {
return (
<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>
);
}
return (
<div className="space-y-3">
{/* Explorations Section */}
{hasExplorations && (
<ContextSection
icon={<Compass className="h-4 w-4" />}
title={formatMessage({ id: 'liteTasks.contextPanel.explorations' })}
badge={
contextData.explorations?.manifest?.exploration_count
? formatMessage(
{ id: 'liteTasks.contextPanel.explorationsCount' },
{ count: contextData.explorations.manifest.exploration_count as number }
)
: undefined
}
>
<div className="space-y-2">
{!!contextData.explorations?.manifest?.task_description && (
<div className="text-xs text-muted-foreground">
<span className="font-medium text-foreground">
{formatMessage({ id: 'liteTasks.contextPanel.taskDescription' })}:
</span>{' '}
{String(contextData.explorations.manifest.task_description)}
</div>
)}
{!!contextData.explorations?.manifest?.complexity && (
<div className="text-xs text-muted-foreground">
<span className="font-medium text-foreground">
{formatMessage({ id: 'liteTasks.contextPanel.complexity' })}:
</span>{' '}
<Badge variant="info" className="text-[10px]">
{String(contextData.explorations.manifest.complexity)}
</Badge>
</div>
)}
{contextData.explorations?.data && (
<div className="flex flex-wrap gap-1.5 mt-1">
{Object.keys(contextData.explorations.data).map((angle) => (
<Badge key={angle} variant="secondary" className="text-[10px] capitalize">
{angle.replace(/-/g, ' ')}
</Badge>
))}
</div>
)}
</div>
</ContextSection>
)}
{/* Diagnoses Section */}
{hasDiagnoses && (
<ContextSection
icon={<Stethoscope className="h-4 w-4" />}
title={formatMessage({ id: 'liteTasks.contextPanel.diagnoses' })}
badge={
contextData.diagnoses?.items?.length
? formatMessage(
{ id: 'liteTasks.contextPanel.diagnosesCount' },
{ count: contextData.diagnoses.items.length }
)
: undefined
}
>
{contextData.diagnoses?.items?.map((item, i) => (
<div key={i} className="text-xs text-muted-foreground py-1 border-b border-border/50 last:border-0">
{(item.title as string) || (item.description as string) || `Diagnosis ${i + 1}`}
</div>
))}
</ContextSection>
)}
{/* Context Package Section */}
{hasContext && (
<ContextSection
icon={<Package className="h-4 w-4" />}
title={formatMessage({ id: 'liteTasks.contextPanel.contextPackage' })}
>
<pre className="text-xs text-muted-foreground overflow-auto max-h-48 bg-muted/50 rounded p-2 whitespace-pre-wrap">
{JSON.stringify(contextData.context, null, 2)}
</pre>
</ContextSection>
)}
{/* Focus Paths from Plan */}
{hasFocusPaths && (
<ContextSection
icon={<FolderOpen className="h-4 w-4" />}
title={formatMessage({ id: 'liteTasks.contextPanel.focusPaths' })}
>
<div className="flex flex-wrap gap-1.5">
{(plan.focus_paths as string[]).map((p, i) => (
<Badge key={i} variant="secondary" className="text-[10px] font-mono">
{p}
</Badge>
))}
</div>
</ContextSection>
)}
{/* Plan Summary */}
{hasSummary && (
<ContextSection
icon={<FileText className="h-4 w-4" />}
title={formatMessage({ id: 'liteTasks.contextPanel.summary' })}
>
<p className="text-xs text-muted-foreground">{plan.summary as string}</p>
</ContextSection>
)}
</div>
);
}
/**
* ContextSection - Collapsible section wrapper for context items
*/
function ContextSection({
icon,
title,
badge,
children,
}: {
icon: React.ReactNode;
title: string;
badge?: string;
children: React.ReactNode;
}) {
const [isOpen, setIsOpen] = React.useState(true);
return (
<Card className="border-border" onClick={(e) => e.stopPropagation()}>
<button
className="w-full flex items-center gap-2 p-3 text-left hover:bg-muted/50 transition-colors"
onClick={() => setIsOpen(!isOpen)}
>
<span className="text-muted-foreground">{icon}</span>
<span className="text-sm font-medium text-foreground flex-1">{title}</span>
{badge && (
<Badge variant="secondary" className="text-[10px]">{badge}</Badge>
)}
{isOpen ? (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
) : (
<ChevronRight className="h-4 w-4 text-muted-foreground" />
)}
</button>
{isOpen && (
<CardContent className="px-3 pb-3 pt-0">
{children}
</CardContent>
)}
</Card>
);
}
/**
* LiteTasksPage component - Display lite-plan and lite-fix sessions with expandable tasks
*/
export function LiteTasksPage() {
const navigate = useNavigate();
const { formatMessage } = useIntl();
const { litePlan, liteFix, multiCliPlan, isLoading, error, refetch } = useLiteTasks();
const [activeTab, setActiveTab] = React.useState<LiteTaskTab>('lite-plan');
const [expandedSessionId, setExpandedSessionId] = React.useState<string | null>(null);
const [selectedTask, setSelectedTask] = React.useState<LiteTask | null>(null);
const [searchQuery, setSearchQuery] = React.useState('');
const [sortField, setSortField] = React.useState<SortField>('date');
const [sortOrder, setSortOrder] = React.useState<SortOrder>('desc');
// Filter and sort sessions
const filterAndSort = React.useCallback((sessions: LiteTaskSession[]) => {
let filtered = sessions;
// Apply search filter
if (searchQuery.trim()) {
const query = searchQuery.toLowerCase();
filtered = sessions.filter(session =>
session.id.toLowerCase().includes(query) ||
session.tasks?.some(task =>
task.title?.toLowerCase().includes(query) ||
task.task_id?.toLowerCase().includes(query)
)
);
}
// Apply sort
const sorted = [...filtered].sort((a, b) => {
let comparison = 0;
switch (sortField) {
case 'date':
comparison = new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime();
break;
case 'name':
comparison = a.id.localeCompare(b.id);
break;
case 'tasks':
comparison = (a.tasks?.length || 0) - (b.tasks?.length || 0);
break;
}
return sortOrder === 'asc' ? comparison : -comparison;
});
return sorted;
}, [searchQuery, sortField, sortOrder]);
// Filtered data
const filteredLitePlan = React.useMemo(() => filterAndSort(litePlan), [litePlan, filterAndSort]);
const filteredLiteFix = React.useMemo(() => filterAndSort(liteFix), [liteFix, filterAndSort]);
const filteredMultiCliPlan = React.useMemo(() => filterAndSort(multiCliPlan), [multiCliPlan, filterAndSort]);
// Toggle sort
const toggleSort = (field: SortField) => {
if (sortField === field) {
setSortOrder(prev => prev === 'asc' ? 'desc' : 'asc');
} else {
setSortField(field);
setSortOrder('desc');
}
};
const handleBack = () => {
navigate('/sessions');
};
// Get status badge color
const getStatusColor = (status?: string) => {
const statusColors: Record<string, string> = {
decided: 'success',
converged: 'success',
plan_generated: 'success',
completed: 'success',
exploring: 'info',
initialized: 'info',
analyzing: 'warning',
debating: 'warning',
blocked: 'destructive',
conflict: 'destructive',
};
return statusColors[status || ''] || 'secondary';
};
// Render lite task card with expandable tasks
const renderLiteTaskCard = (session: LiteTaskSession) => {
const isLitePlan = session.type === 'lite-plan';
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]);
const firstTask = session.tasks?.[0];
return (
<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-3">
<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={isLitePlan ? 'secondary' : 'warning'} className="gap-1 flex-shrink-0">
{isLitePlan ? <FileEdit className="h-3 w-3" /> : <Wrench className="h-3 w-3" />}
{formatMessage({ id: isLitePlan ? 'liteTasks.type.plan' : 'liteTasks.type.fix' })}
</Badge>
</div>
{/* Task preview - first task title */}
{firstTask?.title && (
<div className="mb-3 pb-3 border-b border-border/50">
<p className="text-sm text-foreground line-clamp-1">{firstTask.title}</p>
</div>
)}
{/* Task status distribution */}
<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>
{/* Date and task count */}
<div className="flex items-center gap-4 text-xs text-muted-foreground">
{session.createdAt && (
<span className="flex items-center gap-1">
<Calendar className="h-3.5 w-3.5" />
{new Date(session.createdAt).toLocaleDateString()}
</span>
)}
{taskCount > 0 && (
<span className="flex items-center gap-1">
<Hash className="h-3.5 w-3.5" />
{taskCount} {formatMessage({ id: 'liteTasks.tasksCount' })}
</span>
)}
</div>
</CardContent>
</Card>
{/* Expanded tasks panel with tabs */}
{isExpanded && session.tasks && session.tasks.length > 0 && (
<ExpandedSessionPanel
session={session}
onTaskClick={setSelectedTask}
/>
)}
</div>
);
};
// Render multi-cli plan card
const renderMultiCliCard = (session: LiteTaskSession) => {
const metadata = session.metadata || {};
const latestSynthesis = session.latestSynthesis || {};
const roundCount = (metadata.roundId as number) || session.roundCount || 1;
const topicTitle = getI18nText(
latestSynthesis.title as string | { en?: string; zh?: string } | undefined
) || formatMessage({ id: 'liteTasks.discussionTopic' });
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]);
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>
<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>
);
};
// Loading state
if (isLoading) {
return (
<div className="space-y-6">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" disabled>
<ArrowLeft className="h-4 w-4 mr-2" />
{formatMessage({ id: 'common.actions.back' })}
</Button>
<div className="h-8 w-64 rounded bg-muted animate-pulse" />
</div>
<div className="h-64 rounded-lg bg-muted animate-pulse" />
</div>
);
}
// Error state
if (error) {
return (
<div className="flex items-center gap-2 p-4 rounded-lg bg-destructive/10 border border-destructive/30 text-destructive">
<XCircle className="h-5 w-5 flex-shrink-0" />
<div className="flex-1">
<p className="text-sm font-medium">{formatMessage({ id: 'common.errors.loadFailed' })}</p>
<p className="text-xs mt-0.5">{error.message}</p>
</div>
<Button variant="outline" size="sm" onClick={() => refetch()}>
{formatMessage({ id: 'common.actions.retry' })}
</Button>
</div>
);
}
const totalSessions = litePlan.length + liteFix.length + multiCliPlan.length;
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Button variant="ghost" size="sm" onClick={handleBack}>
<ArrowLeft className="h-4 w-4 mr-2" />
{formatMessage({ id: 'common.actions.back' })}
</Button>
<div>
<h1 className="text-2xl font-semibold text-foreground">
{formatMessage({ id: 'liteTasks.title' })}
</h1>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasks.subtitle' }, { count: totalSessions })}
</p>
</div>
</div>
</div>
{/* Tabs */}
<TabsNavigation
value={activeTab}
onValueChange={(v) => setActiveTab(v as LiteTaskTab)}
tabs={[
{
value: 'lite-plan',
label: formatMessage({ id: 'liteTasks.type.plan' }),
icon: <FileEdit className="h-4 w-4" />,
badge: <Badge variant="secondary" className="ml-2">{litePlan.length}</Badge>,
},
{
value: 'lite-fix',
label: formatMessage({ id: 'liteTasks.type.fix' }),
icon: <Wrench className="h-4 w-4" />,
badge: <Badge variant="secondary" className="ml-2">{liteFix.length}</Badge>,
},
{
value: 'multi-cli-plan',
label: formatMessage({ id: 'liteTasks.type.multiCli' }),
icon: <MessagesSquare className="h-4 w-4" />,
badge: <Badge variant="secondary" className="ml-2">{multiCliPlan.length}</Badge>,
},
]}
/>
{/* Search and Sort Toolbar */}
<div className="mt-4 flex flex-col sm:flex-row gap-3 items-start sm:items-center justify-between">
{/* Search */}
<div className="relative flex-1 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: 'liteTasks.searchPlaceholder' })}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 text-sm rounded-lg border border-border bg-background focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"
/>
</div>
{/* Sort Buttons */}
<div className="flex items-center gap-2">
<span className="text-xs text-muted-foreground flex items-center gap-1">
<ListFilter className="h-3.5 w-3.5" />
{formatMessage({ id: 'liteTasks.sortBy' })}:
</span>
<Button
variant={sortField === 'date' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => toggleSort('date')}
className="h-8 px-3 text-xs gap-1"
>
<Calendar className="h-3.5 w-3.5" />
{formatMessage({ id: 'liteTasks.sort.date' })}
{sortField === 'date' && (sortOrder === 'desc' ? <SortDesc className="h-3 w-3" /> : <SortAsc className="h-3 w-3" />)}
</Button>
<Button
variant={sortField === 'name' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => toggleSort('name')}
className="h-8 px-3 text-xs gap-1"
>
{formatMessage({ id: 'liteTasks.sort.name' })}
{sortField === 'name' && (sortOrder === 'desc' ? <SortDesc className="h-3 w-3" /> : <SortAsc className="h-3 w-3" />)}
</Button>
<Button
variant={sortField === 'tasks' ? 'secondary' : 'ghost'}
size="sm"
onClick={() => toggleSort('tasks')}
className="h-8 px-3 text-xs gap-1"
>
<Hash className="h-3.5 w-3.5" />
{formatMessage({ id: 'liteTasks.sort.tasks' })}
{sortField === 'tasks' && (sortOrder === 'desc' ? <SortDesc className="h-3 w-3" /> : <SortAsc className="h-3 w-3" />)}
</Button>
</div>
</div>
{/* Lite Plan Tab */}
{activeTab === 'lite-plan' && (
<div className="mt-4">
{litePlan.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Zap className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'liteTasks.empty.title' }, { type: 'lite-plan' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasks.empty.message' })}
</p>
</div>
) : filteredLitePlan.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Search className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'liteTasks.noResults.title' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasks.noResults.message' })}
</p>
</div>
) : (
<div className="grid gap-3">{filteredLitePlan.map(renderLiteTaskCard)}</div>
)}
</div>
)}
{/* Lite Fix Tab */}
{activeTab === 'lite-fix' && (
<div className="mt-4">
{liteFix.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Zap className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'liteTasks.empty.title' }, { type: 'lite-fix' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasks.empty.message' })}
</p>
</div>
) : filteredLiteFix.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Search className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'liteTasks.noResults.title' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasks.noResults.message' })}
</p>
</div>
) : (
<div className="grid gap-3">{filteredLiteFix.map(renderLiteTaskCard)}</div>
)}
</div>
)}
{/* Multi-CLI Plan Tab */}
{activeTab === 'multi-cli-plan' && (
<div className="mt-4">
{multiCliPlan.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Zap className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'liteTasks.empty.title' }, { type: 'multi-cli-plan' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasks.empty.message' })}
</p>
</div>
) : filteredMultiCliPlan.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Search className="h-12 w-12 text-muted-foreground mb-4" />
<h3 className="text-lg font-medium text-foreground mb-2">
{formatMessage({ id: 'liteTasks.noResults.title' })}
</h3>
<p className="text-sm text-muted-foreground">
{formatMessage({ id: 'liteTasks.noResults.message' })}
</p>
</div>
) : (
<div className="grid gap-3">{filteredMultiCliPlan.map(renderMultiCliCard)}</div>
)}
</div>
)}
{/* TaskDrawer */}
<TaskDrawer
task={selectedTask}
isOpen={!!selectedTask}
onClose={() => setSelectedTask(null)}
/>
</div>
);
}
export default LiteTasksPage;