import { useCallback, useRef, type JSX, type ReactNode } from "react"; import { Button } from "@tutti-os/ui-system/components"; import { ChevronDownIcon } from "@tutti-os/ui-system/icons"; import { useTranslation } from "../../../i18n/index"; import { CollapsibleReveal } from "./CollapsibleReveal"; import type { AgentTurnDisclosureStore } from "./AgentTurnDisclosureContext"; import { useElapsedSeconds } from "./useElapsedSeconds"; import { formatAgentTurnDuration, type AgentTurnDuration, type AgentTurnTiming, type AgentTurnWorkSectionModel, type AgentTurnWorkSectionRow } from "./agentTurnWorkSectionModel"; interface AgentTurnWorkSectionProps { model: AgentTurnWorkSectionModel; sessionId: string; turnKey: string; showDivider?: boolean; disclosureStore: AgentTurnDisclosureStore; onDisclosureMotionChange?: ( turnKey: string, active: boolean, anchorElement?: HTMLElement | null ) => void; footer?: ReactNode; renderRow: ( row: AgentTurnWorkSectionRow["row"], rowIndex: number, renderKey?: string ) => JSX.Element; } export function AgentTurnWorkSection({ model, sessionId, turnKey, showDivider = false, disclosureStore, onDisclosureMotionChange, renderRow, footer }: AgentTurnWorkSectionProps): JSX.Element { const { t } = useTranslation(); const disclosureKey = `${sessionId}:${turnKey}`; const expanded = model.collapseEligible ? (disclosureStore.expandedOverrides[disclosureKey] ?? false) : true; const toggleLabel = expanded ? t("agentHost.agentGui.collapseTurnWork") : t("agentHost.agentGui.expandTurnWork"); const pendingRevealTransitionsRef = useRef(0); const disclosureMotionActiveRef = useRef(false); const collapsibleSectionCount = model.sections.reduce( (count, section) => count + (section.kind === "work" && model.collapseEligible ? 1 : 0), 0 ); const finishDisclosureMotion = useCallback(() => { if (!disclosureMotionActiveRef.current) { return; } disclosureMotionActiveRef.current = false; pendingRevealTransitionsRef.current = 0; onDisclosureMotionChange?.(turnKey, false); }, [onDisclosureMotionChange, turnKey]); const startDisclosureMotion = useCallback( (anchorElement: HTMLElement | null) => { pendingRevealTransitionsRef.current = collapsibleSectionCount; if (collapsibleSectionCount === 0) { return; } disclosureMotionActiveRef.current = true; onDisclosureMotionChange?.(turnKey, true, anchorElement); }, [collapsibleSectionCount, onDisclosureMotionChange, turnKey] ); const handleRevealTransitionEnd = useCallback(() => { if (!disclosureMotionActiveRef.current) { return; } pendingRevealTransitionsRef.current -= 1; if (pendingRevealTransitionsRef.current <= 0) { finishDisclosureMotion(); } }, [finishDisclosureMotion]); return (
{showDivider ? ( ); } function renderRows( rows: readonly AgentTurnWorkSectionRow[], renderRow: AgentTurnWorkSectionProps["renderRow"] ): ReactNode { return rows.map(({ row, rowIndex, renderKey }) => renderRow(row, rowIndex, renderKey) ); } function AgentTurnDurationLabel({ timing }: { timing: AgentTurnTiming; }): JSX.Element { const { t } = useTranslation(); const liveElapsedSeconds = useElapsedSeconds( timing.kind === "live" && timing.observationGapPresentationState === undefined && !Number.isFinite(timing.frozenAtUnixMs) ? timing.startedAtUnixMs : null ); if ( timing.kind === "live" && timing.observationGapPresentationState !== undefined ) { return ( {timing.observationGapPresentationState === "peer-offline" ? t("agentHost.agentGui.turnPeerDeviceOfflinePendingSync") : t("agentHost.agentGui.turnPeerDeviceProgressSynchronizing")} ); } const elapsedSeconds = timing.kind === "live" ? Number.isFinite(timing.frozenAtUnixMs) ? Math.max( 0, Math.floor( ((timing.frozenAtUnixMs as number) - timing.startedAtUnixMs) / 1_000 ) ) : (liveElapsedSeconds ?? 0) : timing.elapsedSeconds; return {translateDuration(t, timing.kind, elapsedSeconds)}; } function translateDuration( t: ReturnType["t"], kind: AgentTurnTiming["kind"], elapsedSeconds: number ): string { const duration = formatAgentTurnDuration(elapsedSeconds); return kind === "live" ? translateLiveDuration(t, duration) : translateSettledDuration(t, duration); } function translateLiveDuration( t: ReturnType["t"], duration: AgentTurnDuration ): string { if (duration.kind === "seconds") { return t("agentHost.agentGui.turnProcessedSeconds", { seconds: duration.seconds }); } if (duration.kind === "minutes") { return t("agentHost.agentGui.turnProcessedMinutes", { minutes: duration.minutes }); } return t("agentHost.agentGui.turnProcessedMinutesSeconds", { minutes: duration.minutes, seconds: duration.seconds }); } function translateSettledDuration( t: ReturnType["t"], duration: AgentTurnDuration ): string { if (duration.kind === "seconds") { return t("agentHost.agentGui.turnTotalSeconds", { seconds: duration.seconds }); } if (duration.kind === "minutes") { return t("agentHost.agentGui.turnTotalMinutes", { minutes: duration.minutes }); } return t("agentHost.agentGui.turnTotalMinutesSeconds", { minutes: duration.minutes, seconds: duration.seconds }); }