Skip to content

Commit e5ff095

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

File tree

4 files changed

+327
-222
lines changed

4 files changed

+327
-222
lines changed

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

Lines changed: 74 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,50 @@
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) -->
322351
<div
323-
class="step-status-container hidden md:block"
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 -->
358+
<div
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
380+
class="code-pulse-dot"
381+
style="top: {centerTopPx}px;"
382+
>
383+
<PulseDot />
384+
</div>
385+
{/each}
333386
</div>
334387
{/if}
335388
</div>
@@ -371,7 +424,7 @@
371424
}
372425
373426
/* Mobile: Smaller font, no border radius (touches edges) */
374-
@media (max-width: 768px) {
427+
@media (max-width: 767px) {
375428
.code-panel {
376429
font-size: 12px;
377430
border-radius: 0;
@@ -437,7 +490,7 @@
437490
}
438491
439492
/* Mobile: Smaller padding */
440-
@media (max-width: 768px) {
493+
@media (max-width: 767px) {
441494
.code-panel :global(pre) {
442495
padding: 16px 8px;
443496
}
@@ -481,7 +534,7 @@
481534
}
482535
483536
/* Mobile: Smaller line padding */
484-
@media (max-width: 768px) {
537+
@media (max-width: 767px) {
485538
.code-panel :global(.line) {
486539
padding: 0 8px;
487540
}
@@ -534,7 +587,7 @@
534587
}
535588
536589
/* Mobile: Smaller status icons, closer to edge */
537-
@media (max-width: 768px) {
590+
@media (max-width: 767px) {
538591
.step-status-container {
539592
right: 8px;
540593
transform: translateY(-50%) scale(0.6);
@@ -546,6 +599,15 @@
546599
opacity: 0.4;
547600
}
548601
602+
/* Pulse dot for code blocks */
603+
.code-pulse-dot {
604+
position: absolute;
605+
left: 50%;
606+
transform: translate(-50%, -50%);
607+
pointer-events: none;
608+
z-index: 10;
609+
}
610+
549611
/* Step status border (mobile - vertical bar) */
550612
.step-status-border {
551613
position: absolute;
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
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: {
29+
event_type: string;
30+
step_slug?: string;
31+
}): {
32+
icon: typeof Play | typeof CheckCircle2;
33+
color: string;
34+
text: string;
35+
} | null {
36+
if (event.event_type === 'step:started' && event.step_slug) {
37+
return {
38+
icon: Play,
39+
color: 'blue',
40+
text: getShortStepName(event.step_slug)
41+
};
42+
}
43+
if (event.event_type === 'step:completed' && event.step_slug) {
44+
return {
45+
icon: CheckCircle2,
46+
color: 'green',
47+
text: getShortStepName(event.step_slug)
48+
};
49+
}
50+
return null;
51+
}
52+
53+
// Get displayable events (started/completed steps only)
54+
const displayableEvents = $derived(
55+
flowState.timeline
56+
.map((e, idx) => ({ event: e, badge: getEventBadgeInfo(e), idx }))
57+
.filter((e) => e.badge !== null)
58+
);
59+
60+
// Truncate deep function
61+
function truncateDeep(obj: unknown, maxLength = 80): unknown {
62+
if (typeof obj === 'string') {
63+
if (obj.length > maxLength) {
64+
return `<long string: ${obj.length} chars>`;
65+
}
66+
return obj;
67+
}
68+
if (Array.isArray(obj)) {
69+
return obj.map((item) => truncateDeep(item, maxLength));
70+
}
71+
if (obj !== null && typeof obj === 'object') {
72+
const truncated: Record<string, unknown> = {};
73+
for (const [key, value] of Object.entries(obj)) {
74+
truncated[key] = truncateDeep(value, maxLength);
75+
}
76+
return truncated;
77+
}
78+
return obj;
79+
}
80+
81+
async function toggleEventExpanded(idx: number, event: unknown) {
82+
if (expandedEventIdx === idx) {
83+
expandedEventIdx = null;
84+
} else {
85+
// Generate syntax-highlighted JSON if not already cached
86+
if (!highlightedEventJson[idx]) {
87+
const truncated = truncateDeep(event);
88+
const jsonString = JSON.stringify(truncated, null, 2);
89+
const html = await codeToHtml(jsonString, {
90+
lang: 'json',
91+
theme: 'night-owl'
92+
});
93+
highlightedEventJson = { ...highlightedEventJson, [idx]: html };
94+
}
95+
// Set expanded after HTML is ready
96+
expandedEventIdx = idx;
97+
}
98+
}
99+
</script>
100+
101+
<Card class="h-full flex flex-col">
102+
<CardHeader class="pb-0 pt-2 px-3 flex-shrink-0 relative">
103+
<div class="flex items-center justify-between">
104+
<CardTitle class="text-sm">Event Stream ({displayableEvents.length})</CardTitle>
105+
</div>
106+
</CardHeader>
107+
<CardContent class="flex-1 overflow-auto pt-1 pb-2 px-3 min-h-0">
108+
<div class="space-y-1">
109+
{#if displayableEvents.length === 0}
110+
<p class="text-sm text-muted-foreground text-center py-8">
111+
No events yet. Start a flow to see events.
112+
</p>
113+
{:else}
114+
{#each displayableEvents as { badge, event, idx } (idx)}
115+
{#if badge}
116+
<button
117+
onclick={() => toggleEventExpanded(idx, event)}
118+
class="w-full text-left rounded event-badge-row event-badge-{badge.color} cursor-pointer hover:opacity-80 transition-opacity"
119+
>
120+
<div class="flex items-center gap-2 px-2 py-1.5">
121+
<svelte:component this={badge.icon} class="w-4 h-4 flex-shrink-0" />
122+
<div class="flex-1 min-w-0">
123+
<div class="font-medium text-sm">{event.step_slug || 'Unknown'}</div>
124+
<div class="text-xs opacity-70">
125+
{event.event_type.replace('step:', '')}
126+
</div>
127+
</div>
128+
<div class="flex items-center gap-2 flex-shrink-0">
129+
<div class="text-xs opacity-70 font-mono text-muted-foreground">
130+
{event.deltaDisplay}
131+
</div>
132+
<div class="text-xs opacity-50">
133+
{expandedEventIdx === idx ? '' : ''}
134+
</div>
135+
</div>
136+
</div>
137+
138+
{#if expandedEventIdx === idx && highlightedEventJson[idx]}
139+
<div class="px-2 pb-1.5" onclick={(e) => e.stopPropagation()}>
140+
<div class="event-json-display rounded overflow-x-auto">
141+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
142+
{@html highlightedEventJson[idx]}
143+
</div>
144+
</div>
145+
{/if}
146+
</button>
147+
{/if}
148+
{/each}
149+
{/if}
150+
</div>
151+
</CardContent>
152+
</Card>
153+
154+
<style>
155+
/* Event badge rows */
156+
.event-badge-row {
157+
border: 1px solid transparent;
158+
}
159+
160+
.event-badge-row.event-badge-blue {
161+
background-color: rgba(59, 91, 219, 0.2);
162+
border-color: rgba(91, 141, 239, 0.5);
163+
color: #7ba3f0;
164+
}
165+
166+
.event-badge-row.event-badge-green {
167+
background-color: rgba(23, 122, 81, 0.2);
168+
border-color: rgba(32, 165, 111, 0.5);
169+
color: #2ec184;
170+
}
171+
172+
/* Event JSON display */
173+
.event-json-display {
174+
max-height: 300px;
175+
}
176+
177+
.event-json-display :global(pre) {
178+
margin: 0 !important;
179+
padding: 8px 10px !important;
180+
background: #0d1117 !important;
181+
border-radius: 4px;
182+
font-size: 10px;
183+
line-height: 1.5;
184+
display: table;
185+
min-width: 100%;
186+
}
187+
188+
.event-json-display :global(code) {
189+
font-family: 'Fira Code', 'Monaco', 'Menlo', 'Courier New', monospace;
190+
white-space: pre;
191+
display: block;
192+
}
193+
</style>

0 commit comments

Comments
 (0)