The assistant can pick connectors and show subagent steps, and the databases page is replaced by connections, sources, and a dashboard. Co-authored-by: Cursor <cursoragent@cursor.com>
88 lines
3.5 KiB
TypeScript
88 lines
3.5 KiB
TypeScript
import { ChevronDown, ChevronUp } from 'lucide-react'
|
|
import { useEffect, useRef, useState } from 'react'
|
|
import type { ExecutionStep } from '../types/chat'
|
|
import { ManusStepCard } from '../execution/ManusStepCard'
|
|
import { groupTimeline, SubagentLane, subagentGroupId } from '../execution/SubagentLane'
|
|
import { t } from '../../../i18n'
|
|
|
|
export function StepTimeline({
|
|
steps,
|
|
activeStepId,
|
|
onStepClick,
|
|
defaultCollapsed = false,
|
|
}: {
|
|
steps: ExecutionStep[]
|
|
activeStepId?: string | null
|
|
onStepClick?: (stepId: string) => void
|
|
defaultCollapsed?: boolean
|
|
}) {
|
|
const [collapsed, setCollapsed] = useState(defaultCollapsed)
|
|
const listRef = useRef<HTMLUListElement>(null)
|
|
const runningId = [...steps].reverse().find((step) => step.status === 'running')?.id
|
|
useEffect(() => {
|
|
const nodes = listRef.current?.querySelectorAll('[data-running="true"]')
|
|
nodes?.[nodes.length - 1]?.scrollIntoView({ block: 'nearest' })
|
|
}, [runningId, steps.length])
|
|
if (!steps.length) return null
|
|
|
|
const leadSteps = steps.filter((step) => !subagentGroupId(step.id))
|
|
const counted = leadSteps.length ? leadSteps : steps
|
|
const runningCount = steps.filter((step) => step.status === 'running').length
|
|
const doneCount = counted.filter((step) => step.status === 'done' || step.status === 'completed').length
|
|
const rows = groupTimeline(steps)
|
|
|
|
return (
|
|
<div className="rounded-xl border border-line bg-[#f8f9fc] dark:bg-surface-subtle/30">
|
|
<button
|
|
type="button"
|
|
onClick={() => setCollapsed((v) => !v)}
|
|
className="flex w-full items-center justify-between px-3 py-2.5 text-sm font-medium text-ink"
|
|
>
|
|
<span className="flex items-center gap-2">
|
|
<span>{t('assistant.panel.executionSteps')}</span>
|
|
<span className="rounded-full bg-surface-subtle px-2 py-0.5 text-[10px] font-normal text-ink-muted">
|
|
{doneCount}/{counted.length}
|
|
{runningCount > 0 ? ` · ${t('assistant.execution.processing')}` : ''}
|
|
</span>
|
|
</span>
|
|
{collapsed ? <ChevronDown className="h-4 w-4 text-ink-muted" /> : <ChevronUp className="h-4 w-4 text-ink-muted" />}
|
|
</button>
|
|
{!collapsed ? (
|
|
<ul ref={listRef} className="relative space-y-0.5 border-t border-line px-2 py-2">
|
|
{rows.map((row, index) => {
|
|
const isLast = index === rows.length - 1
|
|
if (row.type === 'lane') {
|
|
return (
|
|
<li key={`lane-${row.agents.map((agent) => agent.id).join('-')}`} className="relative py-1 ps-1">
|
|
{!isLast ? (
|
|
<span className="absolute start-[1.15rem] top-4 bottom-0 w-px bg-violet-200 dark:bg-violet-400/30" aria-hidden />
|
|
) : null}
|
|
<SubagentLane agents={row.agents} activeStepId={activeStepId} onStepClick={onStepClick} />
|
|
</li>
|
|
)
|
|
}
|
|
const step = row.step
|
|
return (
|
|
<li
|
|
key={step.id}
|
|
className="relative ps-1"
|
|
data-running={step.status === 'running' ? 'true' : undefined}
|
|
>
|
|
{!isLast ? (
|
|
<span className="absolute start-[1.15rem] top-9 bottom-0 w-px bg-line" aria-hidden />
|
|
) : null}
|
|
<ManusStepCard
|
|
step={step}
|
|
active={step.id === activeStepId}
|
|
compact
|
|
onClick={() => onStepClick?.(step.id)}
|
|
/>
|
|
</li>
|
|
)
|
|
})}
|
|
</ul>
|
|
) : null}
|
|
</div>
|
|
)
|
|
}
|