Files
2026-03-20 07:33:46 +00:00

157 lines
6.1 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { memo } from 'react'
import { ArrowRight, Check, ClipboardList, Circle } from 'lucide-react'
import { cn } from '@/utils/cn'
import { useNovaKit } from '../../context/useNovaKit'
export interface TodoItem {
id?: string | number
title?: string
status?: 'pending' | 'completed' | 'in_progress' | string
[key: string]: unknown
}
export interface TodoListProps {
items: TodoItem[]
className?: string
}
function TodoListComponent({ items = [], className }: TodoListProps) {
const { agentName } = useNovaKit()
const todoItems = items
const totalCount = todoItems.length
const completedCount = todoItems.filter(item => item.status === 'completed').length
const isAllCompleted = totalCount > 0 && completedCount === totalCount
const displayCompletedCount =
totalCount === 0
? 0
: Math.min(totalCount, isAllCompleted ? completedCount : completedCount + 1)
const progressRatio =
totalCount === 0
? 0
: displayCompletedCount / totalCount
const progressPercent = Math.max(0, Math.min(100, progressRatio * 100))
return (
<div
className={cn(
'relative my-1 mb-4 max-w-3xl overflow-hidden rounded-xl border border-gray-200 bg-white',
className,
)}
>
{/* Header任务执行流水线 */}
<div className="flex items-center justify-between bg-gray-50 border-b border-gray-100 px-3 py-[5px]">
<div className="flex min-w-0 flex-1 items-center gap-2">
<div className="flex h-4 w-4 flex-none items-center justify-center text-muted-foreground">
<ClipboardList className="h-4 w-4" />
</div>
<div className="min-w-0">
<h2 className="truncate text-[14px] leading-[22px] text-foreground">
线
</h2>
<p className="truncate text-[12px] text-muted-foreground">{agentName}</p>
</div>
</div>
<div className="flex items-center gap-1 pl-3">
<span className="text-[12px] font-medium text-muted-foreground">Progress</span>
<span className="text-[12px] font-medium text-muted-foreground">
{displayCompletedCount} / {totalCount || 0}
</span>
</div>
</div>
{/* 列表内容:卡片列表布局 */}
<div className="relative overflow-hidden transition-all duration-200 ease-in-out">
<div className="relative overflow-visible bg-white px-3 py-2.5">
<div className="absolute left-0 top-0 h-[2px] w-full bg-border/70">
<div
className="h-full bg-primary transition-all duration-700"
style={{ width: `${progressPercent}%` }}
/>
</div>
<div className="flex flex-col pt-1">
{todoItems.map((item, index) => {
const status = item.status as string | undefined
const isCompleted = status === 'completed'
const isActive = status === 'in_progress'
const isPending = !isCompleted && !isActive
const stepLabel = `STEP ${index + 1} OF ${totalCount || 0}`
return (
<div
key={(item.id as string) ?? index}
className={cn(
'flex items-center justify-between gap-3 py-1',
isCompleted && 'opacity-70',
isPending && 'opacity-60',
)}
>
{/* 左侧:状态圆点 + 文案 */}
<div className="flex min-w-0 flex-1 items-center gap-2">
{/* 状态圆点 */}
<div className="relative flex flex-none items-center justify-center">
{isCompleted && (
<div className="flex h-4 w-4 items-center justify-center rounded-full bg-primary">
<Check className="h-2.5 w-2.5 text-white" />
</div>
)}
{isActive && (
<div className="flex h-4 w-4 items-center justify-center rounded-full bg-primary/12 text-primary">
<ArrowRight className="h-2.5 w-2.5" />
</div>
)}
{isPending && !isCompleted && !isActive && (
<div className="flex h-4 w-4 items-center justify-center rounded-full border border-gray-200 bg-white">
<Circle className="h-2.5 w-2.5 text-muted-foreground/55" />
</div>
)}
</div>
{/* 文案 */}
<div className="min-w-0 space-y-0.5">
<p
className={cn(
'truncate text-[14px] leading-[22px] font-normal',
isActive
? 'text-foreground'
: 'text-muted-foreground',
isCompleted && 'line-through decoration-border',
)}
>
{item.title as string}
</p>
{typeof (item as any).description === 'string' && (
<p className="truncate text-[12px] text-muted-foreground">
{(item as any).description as string}
</p>
)}
</div>
{isActive && (
<span className="h-1 w-1 flex-none rounded-full bg-primary animate-pulse" />
)}
</div>
{/* 右侧:步骤标签 */}
<span
className={cn(
'flex-none text-[10px] font-mono font-medium tracking-tight',
isActive
? 'text-primary'
: 'text-muted-foreground',
)}
>
{stepLabel}
</span>
</div>
)
})}
</div>
</div>
</div>
</div>
)
}
export const TodoList = memo(TodoListComponent)
export default TodoList