组件
AI 任务清单 AI Todo
用于清晰展示 AI Agent 的执行计划分解、当前执行阶段与子任务实时进度。
基础用法
在 Agent 执行复杂多步骤任务时,展示结构化的待办清单与进度指示:
Loading…
安装与引入
通过 CLI 自动添加组件,或手动复制源码至项目中:
pnpm dlx @wui-design/cli@latest add @wui/ai-todo安装基础依赖与图标库
pnpm add lucide-react class-variance-authority clsx tailwind-merge复制组件源码到
components/ui/ai-todo.tsx"use client"
import * as React from "react"
import { cva } from "class-variance-authority"
import { AnimatePresence, motion, useReducedMotion } from "motion/react"
import {
BanIcon,
ListTodoIcon,
LoaderCircleIcon,
} from "lucide-react"
import { cn } from "@/lib/utils"
export type AiTodoStatus =
| "pending"
| "in-progress"
| "completed"
| "cancelled"
const easeOut = [0.22, 1, 0.36, 1] as const
const popSpring = {
type: "spring",
stiffness: 520,
damping: 30,
mass: 0.6,
} as const
const aiTodoIndicatorVariants = cva(
"relative flex size-5 shrink-0 items-center justify-center rounded-full border transition-colors duration-300",
{
variants: {
status: {
pending: "border-border text-muted-foreground",
"in-progress": "border-info-border bg-info-subtle text-info",
completed: "border-success bg-success text-success-foreground",
cancelled: "border-border bg-muted text-muted-foreground",
},
},
defaultVariants: { status: "pending" },
}
)
const statusLabels: Record<AiTodoStatus, string> = {
pending: "待处理",
"in-progress": "进行中",
completed: "已完成",
cancelled: "已取消",
}
function AiTodo({ className, ...props }: React.ComponentProps<"section">) {
return (
<section
data-slot="ai-todo"
className={cn("overflow-hidden rounded-md border bg-background", className)}
{...props}
/>
)
}
export interface AiTodoHeaderProps extends React.ComponentProps<"header"> {
/** Leading icon. Pass `null` to hide it. Defaults to a checklist icon. */
icon?: React.ReactNode
}
function AiTodoHeader({
className,
icon,
children,
...props
}: AiTodoHeaderProps) {
return (
<header
data-slot="ai-todo-header"
className={cn(
"flex items-center gap-2 border-b px-3 py-2.5 text-sm font-medium",
className
)}
{...props}
>
{icon === undefined ? (
<ListTodoIcon className="size-4 shrink-0 text-muted-foreground" />
) : (
icon
)}
{children}
</header>
)
}
export interface AiTodoProgressProps
extends Omit<React.ComponentProps<"div">, "children"> {
/** Completed amount. */
value: number
/** Total amount. @default 100 */
max?: number
}
/** A hairline progress bar that springs to the latest completion ratio. */
function AiTodoProgress({
className,
value,
max = 100,
...props
}: AiTodoProgressProps) {
const reduceMotion = useReducedMotion()
const ratio = max > 0 ? Math.min(Math.max(value / max, 0), 1) : 0
return (
<div
role="progressbar"
aria-valuemin={0}
aria-valuemax={max}
aria-valuenow={value}
data-slot="ai-todo-progress"
data-complete={ratio === 1 ? "true" : "false"}
className={cn("h-0.5 w-full overflow-hidden bg-muted", className)}
{...props}
>
<motion.div
className={cn(
"h-full origin-left transition-colors duration-300",
ratio === 1 ? "bg-success" : "bg-primary"
)}
initial={false}
animate={{ scaleX: ratio }}
transition={
reduceMotion
? { duration: 0 }
: { type: "spring", stiffness: 260, damping: 32 }
}
/>
</div>
)
}
function AiTodoList({ className, ...props }: React.ComponentProps<"ol">) {
return (
<ol
data-slot="ai-todo-list"
className={cn("divide-y", className)}
{...props}
/>
)
}
function TodoStatusIcon({
status,
reduceMotion,
}: {
status: AiTodoStatus
reduceMotion: boolean
}) {
if (status === "completed") {
return (
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={3}
strokeLinecap="round"
strokeLinejoin="round"
className="size-3"
aria-hidden
>
<motion.path
d="M20 6 9 17l-5-5"
initial={reduceMotion ? false : { pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={
reduceMotion
? { duration: 0 }
: { duration: 0.32, ease: easeOut, delay: 0.08 }
}
/>
</svg>
)
}
if (status === "pending") return null
const Icon = status === "in-progress" ? LoaderCircleIcon : BanIcon
return (
<Icon
className={cn(
"size-3",
status === "in-progress" && "motion-safe:animate-spin"
)}
/>
)
}
export interface AiTodoItemProps
extends Omit<React.ComponentProps<"li">, "title"> {
/** Main task label. */
title: React.ReactNode
/** Optional supporting detail. */
description?: React.ReactNode
/** Task progression state. @default "pending" */
status?: AiTodoStatus
/** Called when the status control is pressed. */
onStatusChange?: (status: AiTodoStatus) => void
}
/** A readable task row with optional status interaction. */
function AiTodoItem({
className,
title,
description,
status = "pending",
onStatusChange,
...props
}: AiTodoItemProps) {
const reduceMotion = !!useReducedMotion()
const nextStatus = status === "completed" ? "pending" : "completed"
const done = status === "completed" || status === "cancelled"
const indicatorContent = (
<AnimatePresence initial={false} mode="popLayout">
<motion.span
key={status}
className="flex items-center justify-center"
initial={reduceMotion ? false : { opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
exit={reduceMotion ? undefined : { opacity: 0, scale: 0.5 }}
transition={reduceMotion ? { duration: 0 } : popSpring}
>
<TodoStatusIcon status={status} reduceMotion={reduceMotion} />
</motion.span>
</AnimatePresence>
)
return (
<li
data-slot="ai-todo-item"
data-status={status}
className={cn(
"flex items-start gap-3 px-3 py-3 transition-colors duration-300 data-[status=in-progress]:bg-muted/40",
className
)}
{...props}
>
{onStatusChange ? (
<button
type="button"
data-slot="ai-todo-indicator"
aria-label={status === "completed" ? "标记为未完成" : "标记为已完成"}
className={cn(
aiTodoIndicatorVariants({ status }),
"cursor-pointer outline-none hover:border-foreground/40 focus-visible:ring-[3px] focus-visible:ring-ring/35 active:scale-90 motion-safe:transition-[color,background-color,border-color,scale]"
)}
onClick={() => onStatusChange(nextStatus)}
>
{indicatorContent}
</button>
) : (
<span
role="img"
data-slot="ai-todo-indicator"
aria-label={statusLabels[status]}
className={aiTodoIndicatorVariants({ status })}
>
{indicatorContent}
</span>
)}
<div className="min-w-0 flex-1">
<div
data-slot="ai-todo-title"
className={cn(
"text-sm font-medium leading-5 transition-colors duration-300",
done && "text-muted-foreground"
)}
>
<span
className="box-decoration-clone transition-[background-size] duration-400 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none"
style={{
backgroundImage: "linear-gradient(currentColor, currentColor)",
backgroundRepeat: "no-repeat",
backgroundPosition: "0 55%",
backgroundSize: done ? "100% 1px" : "0% 1px",
}}
>
{title}
</span>
</div>
{description ? (
<div
data-slot="ai-todo-description"
className="mt-0.5 text-xs leading-5 text-muted-foreground"
>
{description}
</div>
) : null}
</div>
</li>
)
}
export {
AiTodo,
AiTodoHeader,
AiTodoItem,
AiTodoList,
AiTodoProgress,
aiTodoIndicatorVariants,
}
属性 Props
AiTodo Props
继承原生 HTML <section> 元素的全部属性,支持通过 className 扩展样式。
AiTodoHeader Props
继承原生 HTML <header> 元素的全部属性,通常包含任务标题与右侧完成度统计。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| icon | React.ReactNode | — | 标题前的图标,默认为清单图标;传入 `null` 可隐藏。 |
AiTodoProgress Props
放在 AiTodoHeader 下方的细进度条,完成度变化时以弹簧动画推进,全部完成后切换为成功色。
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| value | number | — | 已完成数量。 |
| max | number | 100 | 总数量。 |
AiTodoList Props
继承原生 HTML <ol> 元素的全部属性,内含具体的 AiTodoItem 任务条目。
AiTodoItem Props
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| title | React.ReactNode | — | 任务条目的主要标题文案。 |
| description | React.ReactNode | — | 任务条目的补充说明、执行细节或错误信息。 |
| status | "pending" | "in-progress" | "completed" | "cancelled" | "pending" | 任务当前的生命周期状态。pending 为待执行,in-progress 为执行中(带旋转动效与行高亮),completed 为已完成(勾选描边绘制、删除线从左向右划过),cancelled 为已取消。 |
| onStatusChange | (status: AiTodoStatus) => void | — | 当传入该回调时,左侧状态图标会变成可交互按钮,点击可在 completed 与 pending 之间切换;未传入时渲染为带状态文案的只读图标。 |
| className | string | — | 应用于任务条目外层 <li> 元素的额外 CSS 类名。 |
事件 Events
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| onStatusChange | (status: AiTodoStatus) => void | — | 用户点击任务条目前置状态图标按钮时触发,返回即将切换的目标状态字符串。 |
| onClick | (event: React.MouseEvent<HTMLLIElement>) => void | — | 整个任务行点击事件透传。 |
使用场景与设计规范
AiTodo 专为大模型复杂规划与工具调度过程可视化而生:
- 减少用户黑盒等待焦虑:当 Agent 接收到复杂指令(如“重构组件并跑通测试”)时,先输出任务规划列表,并在执行过程中将对应子任务逐一置为
in-progress与completed,用户能明确获知当前程序卡点或执行进度。 - 只读 vs 可交互:
- 只读模式:省略
onStatusChange属性,状态图标为纯静态展示,适用于模型自主执行流水线。 - 可交互模式:传入
onStatusChange属性,允许用户手动标记完成、勾选确认或干预任务流。
- 只读模式:省略
- 状态语义区分:遇到被跳过的分支步骤使用
cancelled(灰色删除线),不要与执行失败混淆。
场景示例
实时自动化执行演示
模拟 Agent 逐步分析并推进任务,支持暂停、重置与用户手动切换状态:
Loading…
只读式发布部署工作流
在自动化 CI/CD 或部署流程中,通过只读任务流呈现清晰的执行轨迹与状态标识:
Loading…
无障碍与交互 Accessibility
- 有序列表语义:底层使用
<ol>与<li>构建,屏幕阅读器会自动播报“第 X 项,共 Y 项”。 - 可交互状态指示器:当提供
onStatusChange时,状态图标渲染为原生<button>并带有动态aria-label(如“标记为已完成”)。 - 旋转动效适配:
in-progress状态的旋转动画使用motion-safe:animate-spin,在开启prefers-reduced-motion时自动静止。