/** * Expert Mode log panel. * * Displays a CrewAI-style chain-of-thought trace of the planner / * workflow runner / LLM calls happening on the backend during the * wizard's generate-all SSE run. Reads ``expertLog`` from * ``wizardProgressStore`` — the store appends every SSE frame as * a structured entry, regardless of whether this panel is mounted. * * Visibility is gated by ``useExpertMode()`` (a ``localStorage``- * backed boolean). When off, this component renders nothing — the * log is still being collected, so flipping the toggle mid-run shows * the full history. * * The panel is meant to live inside ``WizardProgressOverlay`` as a * collapsible side rail. Authoring contexts that surface a smaller * inline trace (e.g. a debug drawer) can mount the same component * elsewhere — it's pure. */ import React, { useEffect, useMemo, useState } from "react"; import { useWizardProgress, type ExpertLogEntry } from "./wizardProgressStore"; const STORAGE_KEY = "homepilot_expert_mode"; /** * localStorage-backed boolean for "show the expert log". * * Module-scoped subscriber set so multiple components stay in sync * within the same tab without a context provider — toggling the * setting from anywhere updates every consumer. */ const expertModeListeners = new Set<() => void>(); function readExpertMode(): boolean { try { return globalThis.localStorage?.getItem(STORAGE_KEY) === "1"; } catch { return false; } } export function setExpertMode(next: boolean): void { try { if (next) { globalThis.localStorage?.setItem(STORAGE_KEY, "1"); } else { globalThis.localStorage?.removeItem(STORAGE_KEY); } } catch { /* private browsing / quota — non-fatal */ } expertModeListeners.forEach((fn) => { try { fn(); } catch { /* swallow */ } }); } export function useExpertMode(): [boolean, (next: boolean) => void] { const [enabled, setEnabled] = useState(() => readExpertMode()); useEffect(() => { const sync = () => setEnabled(readExpertMode()); expertModeListeners.add(sync); // Cross-tab sync — pure bonus, costs nothing. const storage = (e: StorageEvent) => { if (e.key === STORAGE_KEY) sync(); }; globalThis.addEventListener?.("storage", storage); return () => { expertModeListeners.delete(sync); globalThis.removeEventListener?.("storage", storage); }; }, []); return [enabled, setExpertMode]; } const KIND_ICON: Record = { thought: "💭", step: "▸", llm: "✨", render: "▦", phase: "•", }; const KIND_COLOR: Record = { thought: "text-[#c4b5fd]", step: "text-[#3ea6ff]", llm: "text-[#fbbf24]", render: "text-[#9f7fd1]", phase: "text-[#aaa]", }; function formatTime(ts: number, baseline: number): string { const seconds = Math.max(0, (ts - baseline) / 1000); return `+${seconds.toFixed(1)}s`; } function ExpertLogRow({ entry, baseline, }: { entry: ExpertLogEntry; baseline: number; }) { const [expanded, setExpanded] = useState(false); const hasPayload = Object.keys(entry.payload || {}).length > 0; return (
{KIND_ICON[entry.kind]} {formatTime(entry.ts, baseline)} {entry.label} {hasPayload && ( )}
{entry.summary && (
{entry.summary}
)} {expanded && hasPayload && (
          {JSON.stringify(entry.payload, null, 2)}
        
)}
); } export function ExpertLogPanel({ className, emptyHint, }: { className?: string; emptyHint?: string; }): React.ReactElement { const state = useWizardProgress(); const entries = state.expertLog; const baseline = useMemo( () => (entries.length > 0 ? entries[0].ts : Date.now()), [entries.length > 0 ? entries[0].id : null], // re-baseline only on reset ); const containerRef = React.useRef(null); useEffect(() => { // Auto-scroll to bottom as new entries arrive — same affordance // as a terminal log; users can still scroll up to read history. const el = containerRef.current; if (!el) return; const isNearBottom = el.scrollHeight - el.scrollTop - el.clientHeight < 80; if (isNearBottom) { el.scrollTop = el.scrollHeight; } }, [entries.length]); return (
Expert log
{entries.length} event{entries.length === 1 ? "" : "s"}
{entries.length === 0 ? (
{emptyHint || "Waiting for the planner. Steps, LLM calls and reasoning will appear here as the run progresses."}
) : ( entries.map((entry) => ( )) )}
); } export function ExpertModeToggle({ className, }: { className?: string; }): React.ReactElement { const [enabled, setEnabled] = useExpertMode(); return ( ); }