Spaces:
Paused
Paused
File size: 5,155 Bytes
0b9dc2e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | import type { Task, TaskContext } from '@agentscope-ai/agentscope/state';
import { Ellipsis, ListX, Loader2, Square, SquareCheck } from 'lucide-react';
import { useState } from 'react';
import { PanelEmpty } from '@/components/panel/PanelEmpty';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { useTranslation } from '@/i18n/useI18n';
import { cn } from '@/lib/utils';
interface TaskPanelProps {
/**
* The task context to render. Pass ``null`` when no data is
* available yet (renders nothing).
*/
tasksContext: TaskContext | null;
className?: string;
}
/**
* State icon for a single task row.
*
* @param state - The task's current state.
* @returns An icon element sized for inline display.
*/
function StateIcon({ state }: { state: Task['state'] }) {
switch (state) {
case 'completed':
return <SquareCheck className="size-3 shrink-0" />;
case 'in_progress':
return <Loader2 className="size-3 animate-spin shrink-0" />;
default:
return <Square className="size-3 shrink-0" />;
}
}
/**
* Filters the task list so that only the last 3 of any leading run of
* completed tasks are shown; earlier ones are replaced by a single
* ellipsis sentinel.
*
* @param tasks - The full ordered task array.
* @returns An object with the visible task slice and a boolean flag
* indicating whether the ellipsis row should be rendered.
*/
function filterTasksWithEllipsis(tasks: Task[]): {
showEllipsis: boolean;
visibleTasks: Task[];
} {
// Count how many consecutive completed tasks appear at the front.
let consecutiveCompleted = 0;
for (const task of tasks) {
if (task.state === 'completed') {
consecutiveCompleted++;
} else {
break;
}
}
const MAX_VISIBLE_COMPLETED = 3;
if (consecutiveCompleted <= MAX_VISIBLE_COMPLETED) {
return { showEllipsis: false, visibleTasks: tasks };
}
// Discard all but the last MAX_VISIBLE_COMPLETED completed tasks.
return {
showEllipsis: true,
visibleTasks: tasks.slice(consecutiveCompleted - MAX_VISIBLE_COMPLETED),
};
}
/**
* Compact, read-only panel listing the agent's current tasks with
* their status and dependency information.
*
* Each row shows ``#id [icon] subject [← blocked by #x, #y]``.
* The panel header displays a progress summary like ``Tasks (3/5)``.
* When there are more than 3 leading completed tasks, the earlier ones
* are collapsed into a single ``…`` separator row.
*
* @param tasksContext - The full ``TaskContext`` from ``AgentState``.
* ``null`` hides the panel entirely.
* @param className - The className
* @returns A panel element, or ``null`` when there are no tasks.
*/
export function TaskPanel({ tasksContext, className }: TaskPanelProps) {
const { t } = useTranslation();
const [expanded, setExpanded] = useState(false);
if (!tasksContext || tasksContext.tasks.length === 0) {
return (
<PanelEmpty
icon={ListX}
title={t('panel.plan.emptyTitle')}
description={t('panel.plan.emptyDescription')}
/>
);
}
const { tasks } = tasksContext;
const completed = tasks.filter((task) => task.state === 'completed').length;
const { showEllipsis, visibleTasks } = filterTasksWithEllipsis(tasks);
const displayedTasks = expanded ? tasks : visibleTasks;
return (
<div className={cn('flex flex-col flex-1 min-h-0 text-sm', className)}>
<div className="flex flex-row items-center gap-x-2 shrink-0 pb-2">
<Badge variant={'secondary'} className="tracking-wide">
{t('panel.plan.completed', { count: completed })}
</Badge>
<Badge variant={'secondary'} className="tracking-wide">
{t('panel.plan.total', { count: tasks.length })}
</Badge>
</div>
<ul className="flex flex-col flex-1 min-h-0 gap-y-0.5 text-xs overflow-y-auto">
{showEllipsis && !expanded && (
<Button
size={'xs'}
variant={'ghost'}
className="flex items-center justify-center w-full"
onClick={() => setExpanded(true)}
>
<Ellipsis className="size-3 text-muted-foreground" />
</Button>
)}
{displayedTasks.map((task) => (
<li
key={task.id}
className={cn(
'flex gap-2 rounded px-2 py-1 items-center',
task.state === 'completed' && 'opacity-60',
)}
>
<StateIcon state={task.state} />
<div className="flex flex-col min-w-0 gap-0.5">
<span className="flex items-center gap-1.5">
<span className="text-xs font-mono text-muted-foreground">
#{task.id}
</span>
<span
className={cn(
'truncate ',
task.state === 'completed' && 'line-through',
)}
>
{task.subject}
</span>
</span>
{task.blocked_by.length > 0 && (
<span className="text-xs text-muted-foreground">
← {t('task-panel.blockedBy')}{' '}
{task.blocked_by.map((id) => `#${id}`).join(', ')}
</span>
)}
</div>
</li>
))}
</ul>
</div>
);
}
|