Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 71 additions & 12 deletions apps/demo/src/lib/components/CodePanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -144,20 +157,32 @@
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();
}
});

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);
Expand All @@ -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 });

Expand Down Expand Up @@ -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}

<!-- Desktop: Icon badge -->
<!-- Tablet: Left border (like mobile) -->
<div
class="step-status-border hidden md:block lg:hidden status-{stepStatus}"
class:status-dimmed={isDimmed}
style="top: {blockTopPx}px; height: {blockHeightPx}px;"
></div>

<!-- Desktop (large screens): Icon badge -->
<div
class="step-status-container hidden md:block"
class="step-status-container hidden lg:block"
class:status-dimmed={isDimmed}
data-step={block.stepSlug}
data-start-line={block.startLine}
style="top: calc({iconTop}em + 12px);"
style="top: {iconTopPx}px;"
>
<StatusBadge status={stepStatus} variant="icon-only" size="xl" />
</div>
{/if}
{/each}

<!-- Pulse dots for all code blocks (including flow_config) -->
{#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}

<div class="code-pulse-dot" style="top: {centerTopPx}px;">
<PulseDot />
</div>
{/each}
</div>
{/if}
</div>
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -437,7 +487,7 @@
}

/* Mobile: Smaller padding */
@media (max-width: 768px) {
@media (max-width: 767px) {
.code-panel :global(pre) {
padding: 16px 8px;
}
Expand Down Expand Up @@ -481,7 +531,7 @@
}

/* Mobile: Smaller line padding */
@media (max-width: 768px) {
@media (max-width: 767px) {
.code-panel :global(.line) {
padding: 0 8px;
}
Expand Down Expand Up @@ -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);
Expand All @@ -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;
Expand Down
5 changes: 1 addition & 4 deletions apps/demo/src/lib/components/DebugPanel.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@
return 'text-muted-foreground';
}
}

</script>

<div class="flex flex-col h-full min-w-0">
Expand Down Expand Up @@ -119,9 +118,7 @@
>{event.cumulativeDisplay}</code
>
{#if event.deltaMs > 0}
<code class="text-xs text-muted-foreground/70 font-mono"
>{event.deltaDisplay}</code
>
<code class="text-xs text-muted-foreground/70 font-mono">{event.deltaDisplay}</code>
{/if}
</div>
<code class="w-[140px] text-base font-semibold font-mono {eventColor}">
Expand Down
190 changes: 190 additions & 0 deletions apps/demo/src/lib/components/EventsPanel.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
<script lang="ts">
import type { createFlowState } from '$lib/stores/pgflow-state.svelte';
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
import { Play, CheckCircle2 } from '@lucide/svelte';
import { codeToHtml } from 'shiki';

interface Props {
flowState: ReturnType<typeof createFlowState>;
}

let { flowState }: Props = $props();

let expandedEventIdx = $state<number | null>(null);
let highlightedEventJson = $state<Record<number, string>>({});

// Helper to get short step name
function getShortStepName(stepSlug: string): string {
const shortNames: Record<string, string> = {
fetchArticle: 'fetch',
summarize: 'summ',
extractKeywords: 'kwrds',
publish: 'pub'
};
return shortNames[stepSlug] || stepSlug.slice(0, 5);
}

// Helper to get event badge info
function getEventBadgeInfo(event: { event_type: string; step_slug?: string }): {
icon: typeof Play | typeof CheckCircle2;
color: string;
text: string;
} | null {
if (event.event_type === 'step:started' && event.step_slug) {
return {
icon: Play,
color: 'blue',
text: getShortStepName(event.step_slug)
};
}
if (event.event_type === 'step:completed' && event.step_slug) {
return {
icon: CheckCircle2,
color: 'green',
text: getShortStepName(event.step_slug)
};
}
return null;
}

// Get displayable events (started/completed steps only)
const displayableEvents = $derived(
flowState.timeline
.map((e, idx) => ({ event: e, badge: getEventBadgeInfo(e), idx }))
.filter((e) => e.badge !== null)
);

// Truncate deep function
function truncateDeep(obj: unknown, maxLength = 80): unknown {
if (typeof obj === 'string') {
if (obj.length > maxLength) {
return `<long string: ${obj.length} chars>`;
}
return obj;
}
if (Array.isArray(obj)) {
return obj.map((item) => truncateDeep(item, maxLength));
}
if (obj !== null && typeof obj === 'object') {
const truncated: Record<string, unknown> = {};
for (const [key, value] of Object.entries(obj)) {
truncated[key] = truncateDeep(value, maxLength);
}
return truncated;
}
return obj;
}

async function toggleEventExpanded(idx: number, event: unknown) {
if (expandedEventIdx === idx) {
expandedEventIdx = null;
} else {
// Generate syntax-highlighted JSON if not already cached
if (!highlightedEventJson[idx]) {
const truncated = truncateDeep(event);
const jsonString = JSON.stringify(truncated, null, 2);
const html = await codeToHtml(jsonString, {
lang: 'json',
theme: 'night-owl'
});
highlightedEventJson = { ...highlightedEventJson, [idx]: html };
}
// Set expanded after HTML is ready
expandedEventIdx = idx;
}
}
</script>

<Card class="h-full flex flex-col">
<CardHeader class="pb-0 pt-2 px-3 flex-shrink-0 relative">
<div class="flex items-center justify-between">
<CardTitle class="text-sm">Event Stream ({displayableEvents.length})</CardTitle>
</div>
</CardHeader>
<CardContent class="flex-1 overflow-auto pt-1 pb-2 px-3 min-h-0">
<div class="space-y-1">
{#if displayableEvents.length === 0}
<p class="text-sm text-muted-foreground text-center py-8">
No events yet. Start a flow to see events.
</p>
{:else}
{#each displayableEvents as { badge, event, idx } (idx)}
{#if badge}
<button
onclick={() => toggleEventExpanded(idx, event)}
class="w-full text-left rounded event-badge-row event-badge-{badge.color} cursor-pointer hover:opacity-80 transition-opacity"
>
<div class="flex items-center gap-2 px-2 py-1.5">
<svelte:component this={badge.icon} class="w-4 h-4 flex-shrink-0" />
<div class="flex-1 min-w-0">
<div class="font-medium text-sm">{event.step_slug || 'Unknown'}</div>
<div class="text-xs opacity-70">
{event.event_type.replace('step:', '')}
</div>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<div class="text-xs opacity-70 font-mono text-muted-foreground">
{event.deltaDisplay}
</div>
<div class="text-xs opacity-50">
{expandedEventIdx === idx ? '▼' : '▶'}
</div>
</div>
</div>

{#if expandedEventIdx === idx && highlightedEventJson[idx]}
<div class="px-2 pb-1.5" onclick={(e) => e.stopPropagation()}>
<div class="event-json-display rounded overflow-x-auto">
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html highlightedEventJson[idx]}
</div>
</div>
{/if}
</button>
{/if}
{/each}
{/if}
</div>
</CardContent>
</Card>

<style>
/* Event badge rows */
.event-badge-row {
border: 1px solid transparent;
}

.event-badge-row.event-badge-blue {
background-color: rgba(59, 91, 219, 0.2);
border-color: rgba(91, 141, 239, 0.5);
color: #7ba3f0;
}

.event-badge-row.event-badge-green {
background-color: rgba(23, 122, 81, 0.2);
border-color: rgba(32, 165, 111, 0.5);
color: #2ec184;
}

/* Event JSON display */
.event-json-display {
max-height: 300px;
}

.event-json-display :global(pre) {
margin: 0 !important;
padding: 8px 10px !important;
background: #0d1117 !important;
border-radius: 4px;
font-size: 10px;
line-height: 1.5;
display: table;
min-width: 100%;
}

.event-json-display :global(code) {
font-family: 'Fira Code', 'Monaco', 'Menlo', 'Courier New', monospace;
white-space: pre;
display: block;
}
</style>
Loading
Loading