|
1 | 1 | <script lang="ts"> |
2 | 2 | import type { createFlowState } from '$lib/stores/pgflow-state.svelte'; |
3 | 3 | import { Card, CardContent, CardHeader, CardTitle } from '$lib/components/ui/card'; |
4 | | - import { Play, CheckCircle2 } from '@lucide/svelte'; |
| 4 | + import { Play, CheckCircle2, XCircle } from '@lucide/svelte'; |
5 | 5 | import { codeToHtml } from 'shiki'; |
6 | 6 |
|
7 | 7 | interface Props { |
|
12 | 12 |
|
13 | 13 | let expandedEventIdx = $state<number | null>(null); |
14 | 14 | let highlightedEventJson = $state<Record<number, string>>({}); |
| 15 | + let isMobile = $state(false); |
| 16 | +
|
| 17 | + // Detect mobile viewport |
| 18 | + if (typeof window !== 'undefined') { |
| 19 | + const mediaQuery = window.matchMedia('(max-width: 767px)'); |
| 20 | + isMobile = mediaQuery.matches; |
| 21 | +
|
| 22 | + const updateMobile = (e: MediaQueryListEvent) => { |
| 23 | + isMobile = e.matches; |
| 24 | + // Clear cache when switching to force regeneration with new truncation |
| 25 | + highlightedEventJson = {}; |
| 26 | + }; |
| 27 | +
|
| 28 | + mediaQuery.addEventListener('change', updateMobile); |
| 29 | + } |
15 | 30 |
|
16 | 31 | // Helper to get short step name |
17 | 32 | function getShortStepName(stepSlug: string): string { |
|
26 | 41 |
|
27 | 42 | // Helper to get event badge info |
28 | 43 | function getEventBadgeInfo(event: { event_type: string; step_slug?: string }): { |
29 | | - icon: typeof Play | typeof CheckCircle2; |
| 44 | + icon: typeof Play | typeof CheckCircle2 | typeof XCircle; |
30 | 45 | color: string; |
31 | 46 | text: string; |
32 | 47 | } | null { |
|
44 | 59 | text: getShortStepName(event.step_slug) |
45 | 60 | }; |
46 | 61 | } |
| 62 | + if (event.event_type === 'step:failed' && event.step_slug) { |
| 63 | + return { |
| 64 | + icon: XCircle, |
| 65 | + color: 'red', |
| 66 | + text: getShortStepName(event.step_slug) |
| 67 | + }; |
| 68 | + } |
47 | 69 | return null; |
48 | 70 | } |
49 | 71 |
|
50 | | - // Get displayable events (started/completed steps only) |
| 72 | + // Get displayable events (started/completed/failed steps only) |
51 | 73 | const displayableEvents = $derived( |
52 | 74 | flowState.timeline |
53 | 75 | .map((e, idx) => ({ event: e, badge: getEventBadgeInfo(e), idx })) |
|
81 | 103 | } else { |
82 | 104 | // Generate syntax-highlighted JSON if not already cached |
83 | 105 | if (!highlightedEventJson[idx]) { |
84 | | - const truncated = truncateDeep(event); |
| 106 | + // Mobile: 50 chars, Desktop: 500 chars |
| 107 | + const maxLength = isMobile ? 50 : 500; |
| 108 | + const truncated = truncateDeep(event, maxLength); |
85 | 109 | const jsonString = JSON.stringify(truncated, null, 2); |
86 | 110 | const html = await codeToHtml(jsonString, { |
87 | 111 | lang: 'json', |
|
93 | 117 | expandedEventIdx = idx; |
94 | 118 | } |
95 | 119 | } |
| 120 | +
|
| 121 | + // Auto-expand failed events |
| 122 | + $effect(() => { |
| 123 | + // Find the most recent failed event |
| 124 | + const failedEvents = displayableEvents.filter((e) => e.event.event_type === 'step:failed'); |
| 125 | + if (failedEvents.length > 0) { |
| 126 | + const mostRecentFailed = failedEvents[failedEvents.length - 1]; |
| 127 | + // Auto-expand it |
| 128 | + if (expandedEventIdx !== mostRecentFailed.idx) { |
| 129 | + toggleEventExpanded(mostRecentFailed.idx, mostRecentFailed.event); |
| 130 | + } |
| 131 | + } |
| 132 | + }); |
96 | 133 | </script> |
97 | 134 |
|
98 | 135 | <Card class="h-full flex flex-col"> |
|
166 | 203 | color: #2ec184; |
167 | 204 | } |
168 | 205 |
|
| 206 | + .event-badge-row.event-badge-red { |
| 207 | + background-color: rgba(220, 38, 38, 0.2); |
| 208 | + border-color: rgba(239, 68, 68, 0.5); |
| 209 | + color: #f87171; |
| 210 | + } |
| 211 | +
|
169 | 212 | /* Event JSON display */ |
170 | 213 | .event-json-display { |
171 | 214 | max-height: 300px; |
|
0 commit comments