import { useCallback, useEffect, useMemo, useRef, useState, type ReactElement, } from "react"; import { Button, Input, Label, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, cn, } from "@arronqzy/ui"; import { type ExecutionTraceEntry, type PageLifecyclePhase, } from "@arronqzy/blueprint-dsl"; import { useI18n } from "@arronqzy/i18n/react"; import { getLifecyclePhaseLabel } from "../graph/document"; import type { ExecutionLogSettings } from "../library/execution-log-settings"; function IconClear({ className }: { className?: string }) { return ( ); } function IconClearDb({ className }: { className?: string }) { return ( ); } function ArrowDownIcon({ className }: { className?: string }) { return ( ); } function formatJson(value: unknown) { try { return JSON.stringify(value, null, 2); } catch { return String(value); } } function LogTooltip({ content, children, }: { content: string; children: ReactElement; }) { return ( {children} {content} ); } function LogTooltipButton({ content, disabled, className, ...props }: React.ComponentProps & { content: string }) { const button = ( ); return ( {disabled ? {button} : button} ); } const SCROLL_BOTTOM_THRESHOLD = 48; function isScrollNearBottom(element: HTMLElement) { return ( element.scrollHeight - element.scrollTop - element.clientHeight <= SCROLL_BOTTOM_THRESHOLD ); } export type BlueprintExecutionLogPanelProps = { entries: ExecutionTraceEntry[]; settings: ExecutionLogSettings; onUpdateSettings: (patch: Partial) => void; onSave: () => void; onExport: () => void; onClear: () => void; onClearAllSaved?: () => void | Promise; onApplyRetention: () => void; hasSavedRuns?: boolean; lifecyclePhase?: string; }; export function BlueprintExecutionLogPanel({ entries, settings, onUpdateSettings, onSave, onExport, onClear, onClearAllSaved, onApplyRetention, hasSavedRuns = false, lifecyclePhase, }: BlueprintExecutionLogPanelProps) { const { t } = useI18n(); const scrollRef = useRef(null); const bottomRef = useRef(null); const prevEntryCountRef = useRef(entries.length); const stickToBottomRef = useRef(true); const [hasNewBelow, setHasNewBelow] = useState(false); const phaseLabel = useMemo(() => { if (!lifecyclePhase) return null; return getLifecyclePhaseLabel(t, lifecyclePhase as PageLifecyclePhase); }, [lifecyclePhase, t]); const latestEntryPreview = useMemo(() => { const latest = entries[entries.length - 1]; if (!latest) return ""; return latest.nodeLabel ?? latest.nodeId; }, [entries]); const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => { bottomRef.current?.scrollIntoView({ behavior, block: "end" }); stickToBottomRef.current = true; setHasNewBelow(false); }, []); const handleScroll = useCallback(() => { const el = scrollRef.current; if (!el) return; const nearBottom = isScrollNearBottom(el); stickToBottomRef.current = nearBottom; if (nearBottom) { setHasNewBelow(false); } }, []); useEffect(() => { const prevCount = prevEntryCountRef.current; prevEntryCountRef.current = entries.length; if (entries.length === 0) { setHasNewBelow(false); stickToBottomRef.current = true; return; } if (entries.length <= prevCount) return; if (stickToBottomRef.current) { scrollToBottom(prevCount === 0 ? "auto" : "smooth"); return; } setHasNewBelow(true); }, [entries, scrollToBottom]); return ( {t("blueprint.log.title")} {phaseLabel ? t("blueprint.log.simulateSceneWithPhase", { phaseLabel }) : t("blueprint.log.selectLifecycleToDebug")} {onClearAllSaved ? ( void onClearAllSaved()} > ) : null} {entries.length === 0 ? ( {t("blueprint.log.emptyHint")} ) : ( {entries.map((entry, index) => ( {entry.nodeLabel ?? entry.nodeId} {entry.nodeType} · {entry.nodeId} {entry.isoTime} {t("blueprint.log.input")} {formatJson(entry.inputs)} {t("blueprint.log.output")} {entry.error ?? formatJson(entry.outputs)} {index < entries.length - 1 ? ( ) : null} ))} )} {hasNewBelow ? ( scrollToBottom("smooth")} > {t("blueprint.log.hasNewLogs")} {latestEntryPreview ? ( · {latestEntryPreview} ) : null} ) : null} {t("blueprint.log.saveLog")} {t("blueprint.log.exportJson")} onUpdateSettings({ autoSave: e.target.checked })} className="h-3.5 w-3.5 rounded border border-input" /> {t("blueprint.log.autoSaveAfterRun")} {t("blueprint.log.maxSavedCount")} onUpdateSettings({ maxSavedRuns: Math.max(1, Number(e.target.value) || 1), }) } className="h-8" aria-label={t("blueprint.log.maxSavedCountAria")} /> {t("blueprint.log.retentionDays")} onUpdateSettings({ retentionDays: Math.max(1, Number(e.target.value) || 1), }) } className="h-8" aria-label={t("blueprint.log.retentionDaysAria")} /> {t("blueprint.log.cleanupExpired")} ); }
{t("blueprint.log.emptyHint")}
{formatJson(entry.inputs)}
{entry.error ?? formatJson(entry.outputs)}