import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { AnimatePresence, motion, type Variants } from "motion/react"; import { cn } from "@/shared/ui/cn"; import type { ScheduledConditionRecord } from "../types"; import { createVisibleTickerCards, type TickerStackCard } from "./agent-task-ticker-utils"; type AgentTaskTickerProps = { conditions: ScheduledConditionRecord[]; className?: string; }; type TickerTransitionDirection = -1 | 1; const tickerEase = [0.22, 1, 0.36, 1] as const; const tickerSwitchAnimationMs = 380; const tickerCardVariants: Variants = { initial: (direction: TickerTransitionDirection) => ({ opacity: 0.62, x: 0, y: direction > 0 ? 36 : -28, rotate: 0 }), exit: (direction: TickerTransitionDirection) => ({ opacity: [1, 1, 0], x: 0, y: direction > 0 ? 36 : -28, rotate: 0, transition: { duration: 0.3, times: [0, 0.65, 1], ease: [0.5, 0, 0.2, 1] } }) }; const tickerContentVariants: Variants = { initial: { clipPath: "inset(0 100% 0 0)" }, animate: { clipPath: "inset(0 0% 0 0)", transition: { duration: 0.18, ease: tickerEase } }, exit: { clipPath: "inset(0 0 0 100%)", transition: { duration: 0.14, ease: [0.5, 0, 0.2, 1] } } }; const tickerIndicatorVariants: Variants = { initial: { opacity: 0, x: 4, scale: 0.96 }, animate: { opacity: 1, x: 0, scale: 1, transition: { duration: 0.16, ease: tickerEase } }, exit: { opacity: 0, x: 4, scale: 0.96, transition: { duration: 0.12, ease: [0.5, 0, 0.2, 1] } } }; export function AgentTaskTicker({ conditions, className }: AgentTaskTickerProps) { const sectionRef = useRef(null); const isTaskSwitchLockedRef = useRef(false); const taskSwitchUnlockTimeoutRef = useRef(null); const [nowMs, setNowMs] = useState(() => Date.now()); const [rotationIndex, setRotationIndex] = useState(0); const [transitionDirection, setTransitionDirection] = useState(1); const [taskSwitchAnimation, setTaskSwitchAnimation] = useState(() => ({ id: 0, active: false })); const [isHovered, setIsHovered] = useState(false); const [isDocumentVisible, setIsDocumentVisible] = useState(() => ( typeof document === "undefined" || document.visibilityState === "visible" )); const [visibilityRevision, setVisibilityRevision] = useState(0); const runningConditions = conditions; const visibleTickerCards = useMemo( () => createVisibleTickerCards(runningConditions, rotationIndex, nowMs), [runningConditions, rotationIndex, nowMs] ); const hasMultipleRunningTasks = runningConditions.length > 1; const activeIndicatorIndex = runningConditions.length > 0 ? rotationIndex % runningConditions.length : 0; const tickerLabel = hasMultipleRunningTasks ? `工况任务步骤,${runningConditions.length} 个执行中,可滚动切换` : "工况任务步骤"; const releaseTaskSwitchLock = useCallback(() => { if (taskSwitchUnlockTimeoutRef.current !== null) { window.clearTimeout(taskSwitchUnlockTimeoutRef.current); taskSwitchUnlockTimeoutRef.current = null; } isTaskSwitchLockedRef.current = false; }, []); const finishTaskSwitchAnimation = useCallback(() => { setTaskSwitchAnimation((current) => ( current.active ? { ...current, active: false } : current )); }, []); const lockTaskSwitch = useCallback(() => { isTaskSwitchLockedRef.current = true; if (taskSwitchUnlockTimeoutRef.current !== null) { window.clearTimeout(taskSwitchUnlockTimeoutRef.current); } taskSwitchUnlockTimeoutRef.current = window.setTimeout(() => { isTaskSwitchLockedRef.current = false; taskSwitchUnlockTimeoutRef.current = null; finishTaskSwitchAnimation(); }, tickerSwitchAnimationMs); }, [finishTaskSwitchAnimation]); const shiftTask = useCallback((direction: TickerTransitionDirection) => { if (runningConditions.length <= 1 || isTaskSwitchLockedRef.current) { return; } lockTaskSwitch(); setTransitionDirection(direction); setTaskSwitchAnimation((current) => ({ id: current.id + 1, active: true })); setRotationIndex((current) => ( (current + direction + runningConditions.length) % runningConditions.length )); }, [lockTaskSwitch, runningConditions.length]); const showPreviousTask = useCallback(() => { shiftTask(-1); }, [shiftTask]); const showNextTask = useCallback(() => { shiftTask(1); }, [shiftTask]); const handleWheel = useCallback((event: WheelEvent) => { if (!hasMultipleRunningTasks || event.deltaY === 0) { return; } event.preventDefault(); if (event.deltaY > 0) { showNextTask(); return; } showPreviousTask(); }, [hasMultipleRunningTasks, showNextTask, showPreviousTask]); useEffect(() => { if (!isDocumentVisible) { return; } const refreshNow = () => setNowMs(Date.now()); refreshNow(); const intervalId = window.setInterval(() => { refreshNow(); }, 1_000); return () => window.clearInterval(intervalId); }, [isDocumentVisible]); useEffect(() => { setRotationIndex(0); releaseTaskSwitchLock(); finishTaskSwitchAnimation(); }, [finishTaskSwitchAnimation, releaseTaskSwitchLock, runningConditions.length]); useEffect(() => releaseTaskSwitchLock, [releaseTaskSwitchLock]); useEffect(() => { const handleVisibilityChange = () => { const isVisible = document.visibilityState === "visible"; releaseTaskSwitchLock(); finishTaskSwitchAnimation(); setIsHovered(false); setIsDocumentVisible(isVisible); setVisibilityRevision((current) => current + 1); if (isVisible) { setNowMs(Date.now()); } }; document.addEventListener("visibilitychange", handleVisibilityChange); return () => document.removeEventListener("visibilitychange", handleVisibilityChange); }, [finishTaskSwitchAnimation, releaseTaskSwitchLock]); useEffect(() => { const section = sectionRef.current; if (!section) { return; } section.addEventListener("wheel", handleWheel, { passive: false }); return () => section.removeEventListener("wheel", handleWheel); }, [handleWheel]); useEffect(() => { if (!hasMultipleRunningTasks || isHovered || !isDocumentVisible) { return; } const intervalId = window.setInterval(() => { if (document.visibilityState !== "visible") { return; } shiftTask(1); }, 6_000); return () => window.clearInterval(intervalId); }, [hasMultipleRunningTasks, isDocumentVisible, isHovered, runningConditions.length, shiftTask]); if (visibleTickerCards.length === 0) { return null; } return (
setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} >
{visibleTickerCards .slice() .reverse() .map((card) => ( ))}
); } function TickerTaskCard({ card, isActive, animateContentChanges, transitionDirection }: { card: TickerStackCard; isActive: boolean; animateContentChanges: boolean; transitionDirection: TickerTransitionDirection; }) { const { condition, view, stackIndex } = card; const stackStyle = getTickerStackStyle(stackIndex); const contentKey = view.currentStep ? `${condition.id}-${view.currentStep.id}` : `${condition.id}-pending`; return (
); } function TickerTaskCardContent({ view }: { view: TickerStackCard["view"] }) { if (!view.currentStep) { return (
{view.title}
); } return (
{view.title}
{view.currentStep.title}
); } function TickerProgressBar({ progressLabel, muted = false }: { progressLabel: string; muted?: boolean; }) { return ( ); } function TickerStatusDot({ progressValue }: { progressValue?: number }) { const progress = typeof progressValue === "number" ? Math.min(100, Math.max(0, progressValue)) : 0; const dashOffset = 44 - (44 * progress) / 100; return ( ); } function getTickerStackStyle(stackIndex: number) { if (stackIndex === 1) { return { opacity: 1, x: 0, y: 7, rotate: 0 }; } if (stackIndex === 2) { return { opacity: 1, x: 0, y: 14, rotate: 0 }; } return { opacity: 1, x: 0, y: 0, rotate: 0 }; }