Skip to content

Commit ad4358e

Browse files
committed
feat: demo - fix the wider view
1 parent 7d9e3a0 commit ad4358e

File tree

8 files changed

+476
-352
lines changed

8 files changed

+476
-352
lines changed

apps/demo/src/lib/components/CodePanel.svelte

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@
4444
return blocks;
4545
});
4646
47+
// All code blocks (including flow_config) for pulse dot positioning
48+
const allCodeBlocks = $derived.by(() => {
49+
const blocks: Array<{ stepSlug: string; startLine: number; endLine: number }> = [];
50+
51+
for (const [stepSlug, section] of Object.entries(FLOW_SECTIONS)) {
52+
if (section.startLine !== undefined && section.endLine !== undefined) {
53+
blocks.push({ stepSlug, startLine: section.startLine, endLine: section.endLine });
54+
}
55+
}
56+
57+
return blocks;
58+
});
59+
4760
// Helper to trim common leading whitespace from code
4861
function trimLeadingWhitespace(code: string): string {
4962
const lines = code.split('\n');
@@ -144,20 +157,32 @@
144157
const mobile = isMobile;
145158
const selected = selectedStep;
146159
160+
// Cleanup old handlers first
161+
if (cleanupHandlers) {
162+
cleanupHandlers();
163+
cleanupHandlers = undefined;
164+
}
165+
147166
// Setup handlers for full code view (desktop or mobile with no selection)
148167
if (codeContainer && (!mobile || !selected || selected === 'flow_config')) {
149168
setupClickHandlersDelayed();
150169
}
151170
});
152171
153172
function setupClickHandlers() {
154-
if (!codeContainer) return;
173+
if (!codeContainer) {
174+
console.log('[CodePanel] setupClickHandlers: codeContainer not found');
175+
return;
176+
}
177+
178+
console.log('[CodePanel] Setting up click handlers');
155179
156180
// Store handlers for cleanup
157181
const handlers: Array<{ element: Element; type: string; handler: EventListener }> = [];
158182
159183
// Find all line elements
160184
const lines = codeContainer.querySelectorAll('.line');
185+
console.log('[CodePanel] Found', lines.length, 'lines');
161186
lines.forEach((line, index) => {
162187
const lineNumber = index + 1;
163188
const stepSlug = getStepFromLine(lineNumber);
@@ -170,7 +195,8 @@
170195
171196
// Click handler
172197
const clickHandler = () => {
173-
console.log('CodePanel: Line clicked, stepSlug:', stepSlug);
198+
console.log('[CodePanel] Line clicked, stepSlug:', stepSlug);
199+
console.log('[CodePanel] isMobile:', isMobile, 'selectedStep:', selectedStep);
174200
// Clear hover state before navigating
175201
dispatch('step-hovered', { stepSlug: null });
176202
@@ -313,23 +339,47 @@
313339
{#each stepBlocks as block (block.stepSlug)}
314340
{@const stepStatus = getStepStatus(block.stepSlug)}
315341
{#if stepStatus}
316-
{@const blockHeight = (block.endLine - block.startLine + 1) * 1.5}
317-
{@const blockTop = (block.startLine - 1) * 1.5}
318-
{@const iconTop = blockTop + blockHeight / 2}
342+
{@const lineHeightPx = 19.5}
343+
{@const paddingTopPx = 12}
344+
{@const numLines = block.endLine - block.startLine + 1}
345+
{@const blockHeightPx = numLines * lineHeightPx}
346+
{@const blockTopPx = (block.startLine - 1) * lineHeightPx + paddingTopPx}
347+
{@const iconTopPx = blockTopPx + blockHeightPx / 2}
319348
{@const isDimmed = selectedStep && block.stepSlug !== selectedStep}
320349

321-
<!-- Desktop: Icon badge -->
350+
<!-- Tablet: Left border (like mobile) -->
351+
<div
352+
class="step-status-border hidden md:block lg:hidden status-{stepStatus}"
353+
class:status-dimmed={isDimmed}
354+
style="top: {blockTopPx}px; height: {blockHeightPx}px;"
355+
></div>
356+
357+
<!-- Desktop (large screens): Icon badge -->
322358
<div
323-
class="step-status-container hidden md:block"
359+
class="step-status-container hidden lg:block"
324360
class:status-dimmed={isDimmed}
325361
data-step={block.stepSlug}
326362
data-start-line={block.startLine}
327-
style="top: calc({iconTop}em + 12px);"
363+
style="top: {iconTopPx}px;"
328364
>
329365
<StatusBadge status={stepStatus} variant="icon-only" size="xl" />
330366
</div>
331367
{/if}
332368
{/each}
369+
370+
<!-- Pulse dots for all code blocks (including flow_config) -->
371+
{#each allCodeBlocks as block (block.stepSlug)}
372+
{@const lineHeightPx = 19.5}
373+
{@const paddingTopPx = 12}
374+
{@const numLines = block.endLine - block.startLine + 1}
375+
{@const blockHeightPx = numLines * lineHeightPx}
376+
{@const blockTopPx = (block.startLine - 1) * lineHeightPx + paddingTopPx}
377+
{@const centerTopPx = blockTopPx + blockHeightPx / 2}
378+
379+
<div class="code-pulse-dot" style="top: {centerTopPx}px;">
380+
<PulseDot />
381+
</div>
382+
{/each}
333383
</div>
334384
{/if}
335385
</div>
@@ -371,7 +421,7 @@
371421
}
372422
373423
/* Mobile: Smaller font, no border radius (touches edges) */
374-
@media (max-width: 768px) {
424+
@media (max-width: 767px) {
375425
.code-panel {
376426
font-size: 12px;
377427
border-radius: 0;
@@ -437,7 +487,7 @@
437487
}
438488
439489
/* Mobile: Smaller padding */
440-
@media (max-width: 768px) {
490+
@media (max-width: 767px) {
441491
.code-panel :global(pre) {
442492
padding: 16px 8px;
443493
}
@@ -481,7 +531,7 @@
481531
}
482532
483533
/* Mobile: Smaller line padding */
484-
@media (max-width: 768px) {
534+
@media (max-width: 767px) {
485535
.code-panel :global(.line) {
486536
padding: 0 8px;
487537
}
@@ -534,7 +584,7 @@
534584
}
535585
536586
/* Mobile: Smaller status icons, closer to edge */
537-
@media (max-width: 768px) {
587+
@media (max-width: 767px) {
538588
.step-status-container {
539589
right: 8px;
540590
transform: translateY(-50%) scale(0.6);
@@ -546,6 +596,15 @@
546596
opacity: 0.4;
547597
}
548598
599+
/* Pulse dot for code blocks */
600+
.code-pulse-dot {
601+
position: absolute;
602+
left: 50%;
603+
transform: translate(-50%, -50%);
604+
pointer-events: none;
605+
z-index: 10;
606+
}
607+
549608
/* Step status border (mobile - vertical bar) */
550609
.step-status-border {
551610
position: absolute;

apps/demo/src/lib/components/DebugPanel.svelte

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@
7575
return 'text-muted-foreground';
7676
}
7777
}
78-
7978
</script>
8079

8180
<div class="flex flex-col h-full min-w-0">
@@ -119,9 +118,7 @@
119118
>{event.cumulativeDisplay}</code
120119
>
121120
{#if event.deltaMs > 0}
122-
<code class="text-xs text-muted-foreground/70 font-mono"
123-
>{event.deltaDisplay}</code
124-
>
121+
<code class="text-xs text-muted-foreground/70 font-mono">{event.deltaDisplay}</code>
125122
{/if}
126123
</div>
127124
<code class="w-[140px] text-base font-semibold font-mono {eventColor}">
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
<script lang="ts">
2+
import type { createFlowState } from '$lib/stores/pgflow-state.svelte';
3+
import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card';
4+
import { Play, CheckCircle2 } from '@lucide/svelte';
5+
import { codeToHtml } from 'shiki';
6+
7+
interface Props {
8+
flowState: ReturnType<typeof createFlowState>;
9+
}
10+
11+
let { flowState }: Props = $props();
12+
13+
let expandedEventIdx = $state<number | null>(null);
14+
let highlightedEventJson = $state<Record<number, string>>({});
15+
16+
// Helper to get short step name
17+
function getShortStepName(stepSlug: string): string {
18+
const shortNames: Record<string, string> = {
19+
fetchArticle: 'fetch',
20+
summarize: 'summ',
21+
extractKeywords: 'kwrds',
22+
publish: 'pub'
23+
};
24+
return shortNames[stepSlug] || stepSlug.slice(0, 5);
25+
}
26+
27+
// Helper to get event badge info
28+
function getEventBadgeInfo(event: { event_type: string; step_slug?: string }): {
29+
icon: typeof Play | typeof CheckCircle2;
30+
color: string;
31+
text: string;
32+
} | null {
33+
if (event.event_type === 'step:started' && event.step_slug) {
34+
return {
35+
icon: Play,
36+
color: 'blue',
37+
text: getShortStepName(event.step_slug)
38+
};
39+
}
40+
if (event.event_type === 'step:completed' && event.step_slug) {
41+
return {
42+
icon: CheckCircle2,
43+
color: 'green',
44+
text: getShortStepName(event.step_slug)
45+
};
46+
}
47+
return null;
48+
}
49+
50+
// Get displayable events (started/completed steps only)
51+
const displayableEvents = $derived(
52+
flowState.timeline
53+
.map((e, idx) => ({ event: e, badge: getEventBadgeInfo(e), idx }))
54+
.filter((e) => e.badge !== null)
55+
);
56+
57+
// Truncate deep function
58+
function truncateDeep(obj: unknown, maxLength = 80): unknown {
59+
if (typeof obj === 'string') {
60+
if (obj.length > maxLength) {
61+
return `<long string: ${obj.length} chars>`;
62+
}
63+
return obj;
64+
}
65+
if (Array.isArray(obj)) {
66+
return obj.map((item) => truncateDeep(item, maxLength));
67+
}
68+
if (obj !== null && typeof obj === 'object') {
69+
const truncated: Record<string, unknown> = {};
70+
for (const [key, value] of Object.entries(obj)) {
71+
truncated[key] = truncateDeep(value, maxLength);
72+
}
73+
return truncated;
74+
}
75+
return obj;
76+
}
77+
78+
async function toggleEventExpanded(idx: number, event: unknown) {
79+
if (expandedEventIdx === idx) {
80+
expandedEventIdx = null;
81+
} else {
82+
// Generate syntax-highlighted JSON if not already cached
83+
if (!highlightedEventJson[idx]) {
84+
const truncated = truncateDeep(event);
85+
const jsonString = JSON.stringify(truncated, null, 2);
86+
const html = await codeToHtml(jsonString, {
87+
lang: 'json',
88+
theme: 'night-owl'
89+
});
90+
highlightedEventJson = { ...highlightedEventJson, [idx]: html };
91+
}
92+
// Set expanded after HTML is ready
93+
expandedEventIdx = idx;
94+
}
95+
}
96+
</script>
97+
98+
<Card class="h-full flex flex-col">
99+
<CardHeader class="pb-0 pt-2 px-3 flex-shrink-0 relative">
100+
<div class="flex items-center justify-between">
101+
<CardTitle class="text-sm">Event Stream ({displayableEvents.length})</CardTitle>
102+
</div>
103+
</CardHeader>
104+
<CardContent class="flex-1 overflow-auto pt-1 pb-2 px-3 min-h-0">
105+
<div class="space-y-1">
106+
{#if displayableEvents.length === 0}
107+
<p class="text-sm text-muted-foreground text-center py-8">
108+
No events yet. Start a flow to see events.
109+
</p>
110+
{:else}
111+
{#each displayableEvents as { badge, event, idx } (idx)}
112+
{#if badge}
113+
<button
114+
onclick={() => toggleEventExpanded(idx, event)}
115+
class="w-full text-left rounded event-badge-row event-badge-{badge.color} cursor-pointer hover:opacity-80 transition-opacity"
116+
>
117+
<div class="flex items-center gap-2 px-2 py-1.5">
118+
<svelte:component this={badge.icon} class="w-4 h-4 flex-shrink-0" />
119+
<div class="flex-1 min-w-0">
120+
<div class="font-medium text-sm">{event.step_slug || 'Unknown'}</div>
121+
<div class="text-xs opacity-70">
122+
{event.event_type.replace('step:', '')}
123+
</div>
124+
</div>
125+
<div class="flex items-center gap-2 flex-shrink-0">
126+
<div class="text-xs opacity-70 font-mono text-muted-foreground">
127+
{event.deltaDisplay}
128+
</div>
129+
<div class="text-xs opacity-50">
130+
{expandedEventIdx === idx ? '' : ''}
131+
</div>
132+
</div>
133+
</div>
134+
135+
{#if expandedEventIdx === idx && highlightedEventJson[idx]}
136+
<div class="px-2 pb-1.5" onclick={(e) => e.stopPropagation()}>
137+
<div class="event-json-display rounded overflow-x-auto">
138+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
139+
{@html highlightedEventJson[idx]}
140+
</div>
141+
</div>
142+
{/if}
143+
</button>
144+
{/if}
145+
{/each}
146+
{/if}
147+
</div>
148+
</CardContent>
149+
</Card>
150+
151+
<style>
152+
/* Event badge rows */
153+
.event-badge-row {
154+
border: 1px solid transparent;
155+
}
156+
157+
.event-badge-row.event-badge-blue {
158+
background-color: rgba(59, 91, 219, 0.2);
159+
border-color: rgba(91, 141, 239, 0.5);
160+
color: #7ba3f0;
161+
}
162+
163+
.event-badge-row.event-badge-green {
164+
background-color: rgba(23, 122, 81, 0.2);
165+
border-color: rgba(32, 165, 111, 0.5);
166+
color: #2ec184;
167+
}
168+
169+
/* Event JSON display */
170+
.event-json-display {
171+
max-height: 300px;
172+
}
173+
174+
.event-json-display :global(pre) {
175+
margin: 0 !important;
176+
padding: 8px 10px !important;
177+
background: #0d1117 !important;
178+
border-radius: 4px;
179+
font-size: 10px;
180+
line-height: 1.5;
181+
display: table;
182+
min-width: 100%;
183+
}
184+
185+
.event-json-display :global(code) {
186+
font-family: 'Fira Code', 'Monaco', 'Menlo', 'Courier New', monospace;
187+
white-space: pre;
188+
display: block;
189+
}
190+
</style>

0 commit comments

Comments
 (0)