"use client"; import React from "react"; import { Box, Chip, CircularProgress, Collapse, IconButton, Stack, Typography, alpha, useTheme, } from "@mui/material"; import AssignmentTurnedInRounded from "@mui/icons-material/AssignmentTurnedInRounded"; import CheckCircleRounded from "@mui/icons-material/CheckCircleRounded"; import BlockRounded from "@mui/icons-material/BlockRounded"; import KeyboardArrowDownRounded from "@mui/icons-material/KeyboardArrowDownRounded"; import KeyboardArrowUpRounded from "@mui/icons-material/KeyboardArrowUpRounded"; import RadioButtonUncheckedRounded from "@mui/icons-material/RadioButtonUncheckedRounded"; import type { Message } from "./GlobalChatbox.types"; export const TodoPlanCard = ({ todoUpdate, }: { todoUpdate: NonNullable; }) => { const theme = useTheme(); const total = todoUpdate.todos.length; const completed = todoUpdate.todos.filter((todo) => todo.status === "completed").length; const running = todoUpdate.todos.find((todo) => todo.status === "in_progress"); const cancelled = todoUpdate.todos.filter((todo) => todo.status === "cancelled").length; const pending = todoUpdate.todos.filter((todo) => todo.status === "pending").length; const progress = total > 0 ? Math.round((completed / total) * 100) : 0; const isAborted = cancelled > 0 && completed + cancelled === total; const canCollapse = total > 4; const [expanded, setExpanded] = React.useState(!canCollapse && !isAborted); const pinnedTodos = canCollapse ? todoUpdate.todos.slice(0, 4) : todoUpdate.todos; const collapsibleTodos = canCollapse ? todoUpdate.todos.slice(4) : []; const hiddenCount = expanded ? 0 : collapsibleTodos.length; const latestUpdatedAt = Math.max( todoUpdate.createdAt, ...todoUpdate.todos .map((todo) => todo.updatedAt ?? todo.createdAt ?? 0) .filter((value) => value > 0), ); const updatedAtLabel = latestUpdatedAt > 0 ? new Intl.DateTimeFormat("zh-CN", { hour: "2-digit", minute: "2-digit", }).format(new Date(latestUpdatedAt)) : undefined; const getTodoVisual = (status: NonNullable["todos"][number]["status"]) => { if (status === "completed") { return { icon: , color: theme.palette.success.main, label: "完成" }; } if (status === "in_progress") { return { icon: , color: "#0288d1", label: "进行中" }; } if (status === "cancelled") { return { icon: , color: theme.palette.text.disabled, label: "中止" }; } return { icon: , color: theme.palette.text.secondary, label: "待办" }; }; const getPriorityLabel = (priority: NonNullable["todos"][number]["priority"]) => { if (priority === "high") return { label: "高优先级", color: "#8a5a00" }; if (priority === "medium") return { label: "中优先级", color: "#9a6a16" }; if (priority === "low") return { label: "低优先级", color: "#8d7960" }; return undefined; }; const statusSummary = isAborted ? `${completed} 完成 / ${cancelled} 中止` : [ completed ? `${completed} 完成` : null, running ? "1 进行中" : null, pending ? `${pending} 待办` : null, cancelled ? `${cancelled} 中止` : null, ].filter(Boolean).join(" / ") || "等待任务"; const renderTodoRow = ( todo: NonNullable["todos"][number], index: number, ) => { const visual = getTodoVisual(todo.status); const priority = getPriorityLabel(todo.priority); return ( {visual.icon} {todo.content} {priority ? ( ) : null} ); }; if (total === 0) { return null; } return ( { if (canCollapse) { setExpanded((value) => !value); } }} onKeyDown={(event) => { if (canCollapse && (event.key === "Enter" || event.key === " ")) { event.preventDefault(); setExpanded((value) => !value); } }} sx={{ px: 1.4, py: 1.15, cursor: canCollapse ? "pointer" : "default", transition: "background-color 0.2s ease", "&:hover": canCollapse ? { bgcolor: alpha("#00838f", 0.035) } : undefined, }} > 会话任务 {statusSummary}{updatedAtLabel ? ` · ${updatedAtLabel} 更新` : ""} {canCollapse ? ( {expanded ? ( ) : ( )} ) : null} {pinnedTodos.map((todo, index) => renderTodoRow(todo, index))} {canCollapse ? ( {collapsibleTodos.map((todo, index) => renderTodoRow(todo, index + pinnedTodos.length), )} ) : null} {hiddenCount > 0 ? ( 还有 {hiddenCount} 项,展开查看全部 ) : null} ); };