初始化模版工程
This commit is contained in:
156
components/nova-sdk/message-list/message-item/TodoList.tsx
Normal file
156
components/nova-sdk/message-list/message-item/TodoList.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
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
|
||||
Reference in New Issue
Block a user