Skip to content

Commit 65962c8

Browse files
samejrericallam
authored andcommitted
Moves toggle button functionality into the header with new icons
1 parent 13cbac7 commit 65962c8

File tree

4 files changed

+193
-77
lines changed

4 files changed

+193
-77
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export function ListBulletIcon({ className }: { className?: string }) {
2+
return (
3+
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
4+
<path
5+
d="M9 5H20"
6+
stroke="currentColor"
7+
strokeWidth="2"
8+
strokeLinecap="round"
9+
strokeLinejoin="round"
10+
/>
11+
<path
12+
d="M9 12H20"
13+
stroke="currentColor"
14+
strokeWidth="2"
15+
strokeLinecap="round"
16+
strokeLinejoin="round"
17+
/>
18+
<path
19+
d="M9 19H20"
20+
stroke="currentColor"
21+
strokeWidth="2"
22+
strokeLinecap="round"
23+
strokeLinejoin="round"
24+
/>
25+
<circle cx="4" cy="5" r="1" fill="currentColor" />
26+
<circle cx="4" cy="12" r="1" fill="currentColor" />
27+
<circle cx="4" cy="19" r="1" fill="currentColor" />
28+
</svg>
29+
);
30+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
export function MoveToBottomIcon({ className }: { className?: string }) {
2+
return (
3+
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
4+
<path
5+
d="M12 15L12 3"
6+
stroke="currentColor"
7+
strokeWidth="2"
8+
strokeLinecap="round"
9+
strokeLinejoin="round"
10+
/>
11+
<path
12+
d="M3 21L21 21"
13+
stroke="currentColor"
14+
strokeWidth="2"
15+
strokeLinecap="round"
16+
strokeLinejoin="round"
17+
/>
18+
<path
19+
d="M7.5 12.5L12 17L16.5 12.5"
20+
stroke="currentColor"
21+
strokeWidth="2"
22+
strokeLinecap="round"
23+
strokeLinejoin="round"
24+
/>
25+
</svg>
26+
);
27+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
export function SnakedArrowIcon({ className }: { className?: string }) {
2+
return (
3+
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
4+
<path
5+
d="M5 5H16C17.6569 5 19 6.34315 19 8L19 8.5C19 10.1569 17.6569 11.5 16 11.5H8C6.34314 11.5 5 12.8431 5 14.5L5 15C4.99999 16.6569 6.34314 18 8 18H18.634"
6+
stroke="currentColor"
7+
strokeWidth="2"
8+
strokeLinecap="round"
9+
strokeLinejoin="round"
10+
/>
11+
<path
12+
d="M16 21L19 18L16 15"
13+
stroke="currentColor"
14+
strokeWidth="2"
15+
strokeLinecap="round"
16+
strokeLinejoin="round"
17+
/>
18+
</svg>
19+
);
20+
}

apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.streams.$streamKey/route.tsx

Lines changed: 116 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1+
import { BoltIcon, BoltSlashIcon } from "@heroicons/react/20/solid";
2+
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
13
import { type SSEStreamPart, SSEStreamSubscription } from "@trigger.dev/core/v3";
2-
import {
3-
BoltIcon,
4-
BoltSlashIcon,
5-
ListBulletIcon,
6-
Bars3BottomLeftIcon,
7-
} from "@heroicons/react/20/solid";
84
import { Clipboard, ClipboardCheck } from "lucide-react";
9-
import { type LoaderFunctionArgs } from "@remix-run/server-runtime";
105
import { useCallback, useEffect, useRef, useState } from "react";
116
import simplur from "simplur";
7+
import { ListBulletIcon } from "~/assets/icons/ListBulletIcon";
8+
import { MoveToBottomIcon } from "~/assets/icons/MoveToBottomIcon";
9+
import { MoveToTopIcon } from "~/assets/icons/MoveToTopIcon";
10+
import { SnakedArrowIcon } from "~/assets/icons/SnakedArrowIcon";
1211
import { Paragraph } from "~/components/primitives/Paragraph";
1312
import {
1413
Tooltip,
@@ -144,7 +143,8 @@ export function RealtimeStreamViewer({
144143
// Use IntersectionObserver to detect when the bottom element is visible
145144
useEffect(() => {
146145
const bottomElement = bottomRef.current;
147-
if (!bottomElement) return;
146+
const scrollElement = scrollRef.current;
147+
if (!bottomElement || !scrollElement) return;
148148

149149
const observer = new IntersectionObserver(
150150
(entries) => {
@@ -154,17 +154,46 @@ export function RealtimeStreamViewer({
154154
}
155155
},
156156
{
157-
root: scrollRef.current,
157+
root: scrollElement,
158158
threshold: 0.1,
159+
rootMargin: "0px",
159160
}
160161
);
161162

162163
observer.observe(bottomElement);
163164

165+
// Also add a scroll listener as a backup to ensure state updates
166+
let scrollTimeout: ReturnType<typeof setTimeout> | null = null;
167+
const handleScroll = () => {
168+
if (!scrollElement || !bottomElement) return;
169+
170+
// Clear any existing timeout
171+
if (scrollTimeout) {
172+
clearTimeout(scrollTimeout);
173+
}
174+
175+
// Debounce the state update to avoid interrupting smooth scroll
176+
scrollTimeout = setTimeout(() => {
177+
const scrollBottom = scrollElement.scrollTop + scrollElement.clientHeight;
178+
const isNearBottom = scrollElement.scrollHeight - scrollBottom < 50;
179+
setIsAtBottom(isNearBottom);
180+
}, 100);
181+
};
182+
183+
scrollElement.addEventListener("scroll", handleScroll);
184+
// Check initial state
185+
const scrollBottom = scrollElement.scrollTop + scrollElement.clientHeight;
186+
const isNearBottom = scrollElement.scrollHeight - scrollBottom < 50;
187+
setIsAtBottom(isNearBottom);
188+
164189
return () => {
165190
observer.disconnect();
191+
scrollElement.removeEventListener("scroll", handleScroll);
192+
if (scrollTimeout) {
193+
clearTimeout(scrollTimeout);
194+
}
166195
};
167-
}, []);
196+
}, [chunks.length]);
168197

169198
// Auto-scroll to bottom when new chunks arrive, if we're at the bottom
170199
useEffect(() => {
@@ -182,79 +211,103 @@ export function RealtimeStreamViewer({
182211
<div className="flex h-full flex-col overflow-hidden border-t border-grid-bright">
183212
{/* Header */}
184213
<div className="flex flex-wrap items-center justify-between gap-2 border-b border-grid-bright bg-background-bright px-3 py-3">
185-
<Paragraph variant="small/bright" className="mb-0">
186-
Stream: <span className="font-mono text-text-dimmed">{streamKey}</span>
187-
</Paragraph>
188-
<div className="flex flex-wrap items-center gap-3">
189-
<div className="flex flex-wrap items-center gap-2.5">
190-
<div className="flex items-center gap-1">
191-
{isConnected ? (
192-
<BoltIcon className={cn("size-3.5 animate-pulse text-success")} />
193-
) : (
194-
<BoltSlashIcon className={cn("size-3.5 text-text-dimmed")} />
195-
)}
196-
<Paragraph variant="small" className="mb-0">
214+
<div className="flex items-center gap-1.5">
215+
<TooltipProvider>
216+
<Tooltip>
217+
<TooltipTrigger>
218+
{isConnected ? (
219+
<BoltIcon className={cn("size-3.5 animate-pulse text-success")} />
220+
) : (
221+
<BoltSlashIcon className={cn("size-3.5 text-text-dimmed")} />
222+
)}
223+
</TooltipTrigger>
224+
<TooltipContent side="top" className="text-xs">
197225
{isConnected ? "Connected" : "Disconnected"}
198-
</Paragraph>
199-
</div>
200-
<Paragraph variant="small" className="mb-0">
201-
{simplur`${chunks.length} chunk[|s]`}
202-
</Paragraph>
203-
</div>
226+
</TooltipContent>
227+
</Tooltip>
228+
</TooltipProvider>
229+
<Paragraph variant="small/bright" className="mb-0">
230+
Stream: <span className="font-mono text-text-dimmed">{streamKey}</span>
231+
</Paragraph>
232+
</div>
233+
<div className="flex flex-wrap items-center gap-3">
234+
<Paragraph variant="small" className="mb-0">
235+
{simplur`${chunks.length} chunk[|s]`}
236+
</Paragraph>
204237
<TooltipProvider>
205238
<Tooltip disableHoverableContent>
206239
<TooltipTrigger
207240
onClick={() => setViewMode(viewMode === "list" ? "compact" : "list")}
208241
className="text-text-dimmed transition-colors focus-custom hover:cursor-pointer hover:text-text-bright"
209242
>
210243
{viewMode === "list" ? (
211-
<Bars3BottomLeftIcon className="size-4" />
244+
<SnakedArrowIcon className="size-4" />
212245
) : (
213246
<ListBulletIcon className="size-4" />
214247
)}
215248
</TooltipTrigger>
216249
<TooltipContent side="left" className="text-xs">
217-
{viewMode === "list" ? "Compact view" : "List view"}
250+
{viewMode === "list" ? "Flow as text" : "View as list"}
218251
</TooltipContent>
219252
</Tooltip>
220253
</TooltipProvider>
254+
{chunks.length > 0 && (
255+
<TooltipProvider>
256+
<Tooltip open={copied || mouseOver} disableHoverableContent>
257+
<TooltipTrigger
258+
onClick={onCopied}
259+
onMouseEnter={() => setMouseOver(true)}
260+
onMouseLeave={() => setMouseOver(false)}
261+
className={cn(
262+
"transition-colors duration-100 focus-custom hover:cursor-pointer",
263+
copied ? "text-success" : "text-text-dimmed hover:text-text-bright"
264+
)}
265+
>
266+
{copied ? (
267+
<ClipboardCheck className="size-4" />
268+
) : (
269+
<Clipboard className="size-4" />
270+
)}
271+
</TooltipTrigger>
272+
<TooltipContent side="left" className="text-xs">
273+
{copied ? "Copied" : "Copy"}
274+
</TooltipContent>
275+
</Tooltip>
276+
</TooltipProvider>
277+
)}
278+
{chunks.length > 0 && (
279+
<TooltipProvider>
280+
<Tooltip disableHoverableContent>
281+
<TooltipTrigger
282+
onClick={() => {
283+
if (isAtBottom) {
284+
scrollRef.current?.scrollTo({ top: 0, behavior: "smooth" });
285+
} else {
286+
bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
287+
}
288+
}}
289+
className="text-text-dimmed transition-colors focus-custom hover:cursor-pointer hover:text-text-bright"
290+
>
291+
{isAtBottom ? (
292+
<MoveToTopIcon className="size-4" />
293+
) : (
294+
<MoveToBottomIcon className="size-4" />
295+
)}
296+
</TooltipTrigger>
297+
<TooltipContent side="left" className="text-xs">
298+
{isAtBottom ? "Scroll to top" : "Scroll to bottom"}
299+
</TooltipContent>
300+
</Tooltip>
301+
</TooltipProvider>
302+
)}
221303
</div>
222304
</div>
223305

224306
{/* Content */}
225307
<div
226308
ref={scrollRef}
227-
className="relative flex-1 overflow-y-auto bg-charcoal-900 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
309+
className="flex-1 overflow-y-auto bg-charcoal-900 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
228310
>
229-
{chunks.length > 0 && (
230-
<div className="pointer-events-none sticky top-2.5 z-50 h-0">
231-
<div className="pointer-events-auto absolute right-3 top-0">
232-
<TooltipProvider>
233-
<Tooltip open={copied || mouseOver} disableHoverableContent>
234-
<TooltipTrigger
235-
onClick={onCopied}
236-
onMouseEnter={() => setMouseOver(true)}
237-
onMouseLeave={() => setMouseOver(false)}
238-
className={cn(
239-
"transition-colors duration-100 focus-custom hover:cursor-pointer",
240-
copied ? "text-success" : "text-text-dimmed hover:text-text-bright"
241-
)}
242-
>
243-
{copied ? (
244-
<ClipboardCheck className="size-4" />
245-
) : (
246-
<Clipboard className="size-4" />
247-
)}
248-
</TooltipTrigger>
249-
<TooltipContent side="left" className="text-xs">
250-
{copied ? "Copied" : "Copy"}
251-
</TooltipContent>
252-
</Tooltip>
253-
</TooltipProvider>
254-
</div>
255-
</div>
256-
)}
257-
258311
{error && (
259312
<div className="border-b border-error/20 bg-error/10 p-3">
260313
<Paragraph variant="small" className="mb-0 text-error">
@@ -272,7 +325,7 @@ export function RealtimeStreamViewer({
272325
)}
273326

274327
{chunks.length > 0 && viewMode === "list" && (
275-
<div className="p-3 font-mono text-xs leading-relaxed">
328+
<div className="font-mono text-xs leading-tight">
276329
{chunks.map((chunk, index) => (
277330
<StreamChunkLine
278331
key={index}
@@ -287,27 +340,13 @@ export function RealtimeStreamViewer({
287340
)}
288341

289342
{chunks.length > 0 && viewMode === "compact" && (
290-
<div className="p-3 font-mono text-xs leading-relaxed">
343+
<div className="p-3 pt-0 font-mono text-xs leading-relaxed">
291344
<CompactStreamView chunks={chunks} />
292345
{/* Sentinel element for IntersectionObserver */}
293346
<div ref={bottomRef} className="h-px" />
294347
</div>
295348
)}
296349
</div>
297-
298-
{/* Footer with auto-scroll indicator */}
299-
{!isAtBottom && chunks.length > 0 && (
300-
<div className="border-t border-grid-bright bg-charcoal-850 px-3 py-2">
301-
<button
302-
onClick={() => {
303-
bottomRef.current?.scrollIntoView({ behavior: "smooth", block: "end" });
304-
}}
305-
className="text-xs text-blue-500 hover:text-blue-400"
306-
>
307-
↓ Scroll to bottom
308-
</button>
309-
</div>
310-
)}
311350
</div>
312351
);
313352
}
@@ -351,7 +390,7 @@ function StreamChunkLine({
351390
<div className="group flex w-full gap-3 py-1 hover:bg-charcoal-800">
352391
{/* Line number */}
353392
<div
354-
className="flex-none select-none text-right text-charcoal-500"
393+
className="flex-none select-none pl-2 text-right text-charcoal-500"
355394
style={{ width: `${Math.max(maxLineNumberWidth, 3)}ch` }}
356395
>
357396
{lineNumber}

0 commit comments

Comments
 (0)