diff --git a/apps/demo/src/lib/components/CodePanel.svelte b/apps/demo/src/lib/components/CodePanel.svelte index 8cb1647a4..793754432 100644 --- a/apps/demo/src/lib/components/CodePanel.svelte +++ b/apps/demo/src/lib/components/CodePanel.svelte @@ -44,6 +44,19 @@ return blocks; }); + // All code blocks (including flow_config) for pulse dot positioning + const allCodeBlocks = $derived.by(() => { + const blocks: Array<{ stepSlug: string; startLine: number; endLine: number }> = []; + + for (const [stepSlug, section] of Object.entries(FLOW_SECTIONS)) { + if (section.startLine !== undefined && section.endLine !== undefined) { + blocks.push({ stepSlug, startLine: section.startLine, endLine: section.endLine }); + } + } + + return blocks; + }); + // Helper to trim common leading whitespace from code function trimLeadingWhitespace(code: string): string { const lines = code.split('\n'); @@ -144,6 +157,12 @@ const mobile = isMobile; const selected = selectedStep; + // Cleanup old handlers first + if (cleanupHandlers) { + cleanupHandlers(); + cleanupHandlers = undefined; + } + // Setup handlers for full code view (desktop or mobile with no selection) if (codeContainer && (!mobile || !selected || selected === 'flow_config')) { setupClickHandlersDelayed(); @@ -151,13 +170,19 @@ }); function setupClickHandlers() { - if (!codeContainer) return; + if (!codeContainer) { + console.log('[CodePanel] setupClickHandlers: codeContainer not found'); + return; + } + + console.log('[CodePanel] Setting up click handlers'); // Store handlers for cleanup const handlers: Array<{ element: Element; type: string; handler: EventListener }> = []; // Find all line elements const lines = codeContainer.querySelectorAll('.line'); + console.log('[CodePanel] Found', lines.length, 'lines'); lines.forEach((line, index) => { const lineNumber = index + 1; const stepSlug = getStepFromLine(lineNumber); @@ -170,7 +195,8 @@ // Click handler const clickHandler = () => { - console.log('CodePanel: Line clicked, stepSlug:', stepSlug); + console.log('[CodePanel] Line clicked, stepSlug:', stepSlug); + console.log('[CodePanel] isMobile:', isMobile, 'selectedStep:', selectedStep); // Clear hover state before navigating dispatch('step-hovered', { stepSlug: null }); @@ -313,23 +339,47 @@ {#each stepBlocks as block (block.stepSlug)} {@const stepStatus = getStepStatus(block.stepSlug)} {#if stepStatus} - {@const blockHeight = (block.endLine - block.startLine + 1) * 1.5} - {@const blockTop = (block.startLine - 1) * 1.5} - {@const iconTop = blockTop + blockHeight / 2} + {@const lineHeightPx = 19.5} + {@const paddingTopPx = 12} + {@const numLines = block.endLine - block.startLine + 1} + {@const blockHeightPx = numLines * lineHeightPx} + {@const blockTopPx = (block.startLine - 1) * lineHeightPx + paddingTopPx} + {@const iconTopPx = blockTopPx + blockHeightPx / 2} {@const isDimmed = selectedStep && block.stepSlug !== selectedStep} - + + + + {/if} {/each} + + + {#each allCodeBlocks as block (block.stepSlug)} + {@const lineHeightPx = 19.5} + {@const paddingTopPx = 12} + {@const numLines = block.endLine - block.startLine + 1} + {@const blockHeightPx = numLines * lineHeightPx} + {@const blockTopPx = (block.startLine - 1) * lineHeightPx + paddingTopPx} + {@const centerTopPx = blockTopPx + blockHeightPx / 2} + +
+ +
+ {/each} {/if} @@ -371,7 +421,7 @@ } /* Mobile: Smaller font, no border radius (touches edges) */ - @media (max-width: 768px) { + @media (max-width: 767px) { .code-panel { font-size: 12px; border-radius: 0; @@ -437,7 +487,7 @@ } /* Mobile: Smaller padding */ - @media (max-width: 768px) { + @media (max-width: 767px) { .code-panel :global(pre) { padding: 16px 8px; } @@ -481,7 +531,7 @@ } /* Mobile: Smaller line padding */ - @media (max-width: 768px) { + @media (max-width: 767px) { .code-panel :global(.line) { padding: 0 8px; } @@ -534,7 +584,7 @@ } /* Mobile: Smaller status icons, closer to edge */ - @media (max-width: 768px) { + @media (max-width: 767px) { .step-status-container { right: 8px; transform: translateY(-50%) scale(0.6); @@ -546,6 +596,15 @@ opacity: 0.4; } + /* Pulse dot for code blocks */ + .code-pulse-dot { + position: absolute; + left: 50%; + transform: translate(-50%, -50%); + pointer-events: none; + z-index: 10; + } + /* Step status border (mobile - vertical bar) */ .step-status-border { position: absolute; diff --git a/apps/demo/src/lib/components/DebugPanel.svelte b/apps/demo/src/lib/components/DebugPanel.svelte index 5122ae05d..c26a07c99 100644 --- a/apps/demo/src/lib/components/DebugPanel.svelte +++ b/apps/demo/src/lib/components/DebugPanel.svelte @@ -75,7 +75,6 @@ return 'text-muted-foreground'; } } -
@@ -119,9 +118,7 @@ >{event.cumulativeDisplay} {#if event.deltaMs > 0} - {event.deltaDisplay} + {event.deltaDisplay} {/if}
diff --git a/apps/demo/src/lib/components/EventsPanel.svelte b/apps/demo/src/lib/components/EventsPanel.svelte new file mode 100644 index 000000000..0fdb69f8b --- /dev/null +++ b/apps/demo/src/lib/components/EventsPanel.svelte @@ -0,0 +1,190 @@ + + + + +
+ Event Stream ({displayableEvents.length}) +
+
+ +
+ {#if displayableEvents.length === 0} +

+ No events yet. Start a flow to see events. +

+ {:else} + {#each displayableEvents as { badge, event, idx } (idx)} + {#if badge} + + {/if} + {/each} + {/if} +
+
+
+ + diff --git a/apps/demo/src/lib/components/ExplanationPanel.svelte b/apps/demo/src/lib/components/ExplanationPanel.svelte index ae5b44224..da759b9fb 100644 --- a/apps/demo/src/lib/components/ExplanationPanel.svelte +++ b/apps/demo/src/lib/components/ExplanationPanel.svelte @@ -376,9 +376,9 @@ - -
- + +
+
{#if currentStepInfo.dependsOn.length > 0} @@ -443,42 +443,32 @@ {/if}
- -
- -
-
Input
- {#if highlightedInput} -
- - {@html highlightedInput} -
- {:else if flowState.status === 'idle'} + +
+
Input
+ {#if highlightedInput} +
+ + {@html highlightedInput} +
+ {:else if flowState.status === 'idle'} +
+ + Run the workflow to see input +
+ {:else if currentStepInfo && currentStepInfo.dependsOn.length > 0} + {@const incompleteDeps = currentStepInfo.dependsOn.filter( + (dep) => getStepStatus(dep) !== 'completed' + )} + {#if incompleteDeps.length > 0}
- - Run the workflow to see input + + Waiting for {incompleteDeps.join(', ')} to complete
- {:else if currentStepInfo && currentStepInfo.dependsOn.length > 0} - {@const incompleteDeps = currentStepInfo.dependsOn.filter( - (dep) => getStepStatus(dep) !== 'completed' - )} - {#if incompleteDeps.length > 0} -
- - Waiting for {incompleteDeps.join(', ')} to complete -
- {:else} -
- - Waiting for step to start -
- {/if} {:else}
Waiting for step to start
{/if} -
+ {:else} +
+ + Waiting for step to start +
+ {/if} +
- -
-
Output
- {#if highlightedOutput} -
- - {@html highlightedOutput} -
- {:else if flowState.status === 'idle'} -
- - Run the workflow to see output -
- {:else if stepStatus === 'started'} -
- - Step is running... -
- {:else} -
- - Waiting for step to complete -
- {/if} -
+ +
+
Output
+ {#if highlightedOutput} +
+ + {@html highlightedOutput} +
+ {:else if flowState.status === 'idle'} +
+ + Run the workflow to see output +
+ {:else if stepStatus === 'started'} +
+ + Step is running... +
+ {:else} +
+ + Waiting for step to complete +
+ {/if}
diff --git a/apps/demo/src/lib/stores/pgflow-state.svelte.ts b/apps/demo/src/lib/stores/pgflow-state.svelte.ts index cd617cb55..489b9219a 100644 --- a/apps/demo/src/lib/stores/pgflow-state.svelte.ts +++ b/apps/demo/src/lib/stores/pgflow-state.svelte.ts @@ -132,11 +132,11 @@ export function createFlowState( const now = new SvelteDate(); const occurredAt = event.status === 'started' && 'started_at' in event - ? new Date(event.started_at) + ? new SvelteDate(event.started_at) : event.status === 'completed' && 'completed_at' in event - ? new Date(event.completed_at) + ? new SvelteDate(event.completed_at) : event.status === 'failed' && 'failed_at' in event - ? new Date(event.failed_at) + ? new SvelteDate(event.failed_at) : undefined; // DEBUG: Log broadcast event @@ -180,11 +180,11 @@ export function createFlowState( const now = new SvelteDate(); const occurredAt = event.status === 'started' && 'started_at' in event - ? new Date(event.started_at) + ? new SvelteDate(event.started_at) : event.status === 'completed' && 'completed_at' in event - ? new Date(event.completed_at) + ? new SvelteDate(event.completed_at) : event.status === 'failed' && 'failed_at' in event - ? new Date(event.failed_at) + ? new SvelteDate(event.failed_at) : undefined; // DEBUG: Log broadcast event @@ -245,32 +245,32 @@ export function createFlowState( return { get status() { // Track stateVersion for reactivity - stateVersion; + void stateVersion; return run?.step(stepSlug).status || 'created'; }, get output() { // Track stateVersion for reactivity - stateVersion; + void stateVersion; return run?.step(stepSlug).output; }, get error() { // Track stateVersion for reactivity - stateVersion; + void stateVersion; return run?.step(stepSlug).error_message; }, get started_at() { // Track stateVersion for reactivity - stateVersion; + void stateVersion; return run?.step(stepSlug).started_at; }, get completed_at() { // Track stateVersion for reactivity - stateVersion; + void stateVersion; return run?.step(stepSlug).completed_at; }, get failed_at() { // Track stateVersion for reactivity - stateVersion; + void stateVersion; return run?.step(stepSlug).failed_at; } }; diff --git a/apps/demo/src/routes/+page.svelte b/apps/demo/src/routes/+page.svelte index 6fa2eacee..54193977b 100644 --- a/apps/demo/src/routes/+page.svelte +++ b/apps/demo/src/routes/+page.svelte @@ -4,15 +4,15 @@ import { createFlowState } from '$lib/stores/pgflow-state.svelte'; import { pulseDots } from '$lib/stores/pulse-dots.svelte'; import DAGVisualization from '$lib/components/DAGVisualization.svelte'; - import DebugPanel from '$lib/components/DebugPanel.svelte'; + import EventsPanel from '$lib/components/EventsPanel.svelte'; import CodePanel from '$lib/components/CodePanel.svelte'; import ExplanationPanel from '$lib/components/ExplanationPanel.svelte'; import WelcomeModal from '$lib/components/WelcomeModal.svelte'; import PulseDot from '$lib/components/PulseDot.svelte'; import { Button } from '$lib/components/ui/button'; import { Input } from '$lib/components/ui/input'; - import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card'; - import { Play, CheckCircle2 } from '@lucide/svelte'; + import { Card } from '$lib/components/ui/card'; + import { Play, CheckCircle2, Code, GitBranch, Radio } from '@lucide/svelte'; import { codeToHtml } from 'shiki'; import type ArticleFlow from '../../supabase/functions/article_flow_worker/article_flow'; @@ -168,13 +168,6 @@ const isRunning = $derived(flowState.status === 'starting' || flowState.status === 'started'); - // Event stream collapsed state - let eventStreamCollapsed = $state(false); - - function toggleEventStream() { - eventStreamCollapsed = !eventStreamCollapsed; - } - // Mobile events panel state let mobileEventsVisible = $state(false); let mobileEventsContentVisible = $state(false); // Delayed for animation @@ -263,10 +256,7 @@ } // Helper to get event badge info - function getEventBadgeInfo(event: { - event_type: string; - step_slug?: string; - }): { + function getEventBadgeInfo(event: { event_type: string; step_slug?: string }): { icon: typeof Play | typeof CheckCircle2; color: string; text: string; @@ -295,7 +285,6 @@ .filter((e) => e.badge !== null) ); - // Fix for mobile viewport height (address bar issue) // Set actual viewport height as CSS custom property for browsers without dvh support function setViewportHeight() { @@ -340,13 +329,13 @@
- pgflow + pgflow pgflow | @@ -360,10 +349,24 @@ >
-
+ + {#if explanationVisible} +
+ +
+ {:else} +
+ {/if} - - - + +
+ +
- - {#if flowState.events.length > 0} - - {/if} + +
- {#each displayableEvents as { badge, event, idx }, i (idx)} + {#each displayableEvents as { badge, event, idx } (idx)} {#if badge}