From c0aafee9a9923d5dc2fa3b99da4378e923933a06 Mon Sep 17 00:00:00 2001 From: Tristan TR <69242752+tristantr@users.noreply.github.com> Date: Thu, 26 Mar 2026 19:52:15 +0100 Subject: [PATCH] feat: improve-replay-ui (#8250) * Improve UI of script record * Improve UI for scripts * Remove Result & Logs loading container while flow not finised * Improve Graph view * Add click on a step mention * Fix spacing when empty * Fix step duration disappearing in recorded flows * Modernize timeline tab * Improve Script recording result UI * feat: externalize recording player controls for fake-window embedding Co-Authored-By: Claude Opus 4.6 (1M context) * refactor: reorder FlowViewer tab sync effects for clarity Co-Authored-By: Claude Opus 4.6 (1M context) * refactor: eliminate tab sync effects in FlowViewer, use selectedTab directly Co-Authored-By: Claude Opus 4.6 (1M context) * refactor: remove unnecessary untrack in FlowViewer tab init Co-Authored-By: Claude Opus 4.6 (1M context) * fix: skip tab auto-selection when selectedTab is controlled externally Co-Authored-By: Claude Opus 4.6 (1M context) * feat: export recording types from package Co-Authored-By: Claude Opus 4.6 (1M context) * fix: non-null assertion for recording.flow in FlowGraphViewer Co-Authored-By: Claude Opus 4.6 (1M context) * fix: replace banned $bindable(default_value) pattern and simplify tab sync Co-Authored-By: Claude Opus 4.6 (1M context) * fix: use svelte 5 onclick syntax on replay page Co-Authored-By: Claude Opus 4.6 (1M context) * fix: skip db clock endpoint during replay mode Co-Authored-By: Claude Opus 4.6 (1M context) * fix: remove line numbers from script recording code display Co-Authored-By: Claude Opus 4.6 (1M context) --------- Co-authored-by: hugocasa Co-authored-by: Claude Opus 4.6 (1M context) --- frontend/package.json | 7 + .../src/lib/components/FlowGraphViewer.svelte | 12 +- .../lib/components/FlowGraphViewerStep.svelte | 7 +- .../lib/components/FlowStatusViewer.svelte | 2 + .../components/FlowStatusViewerInner.svelte | 8 +- .../src/lib/components/FlowTimeline.svelte | 160 ++++++------ frontend/src/lib/components/FlowViewer.svelte | 76 ++++-- frontend/src/lib/components/JobLoader.svelte | 4 +- .../src/lib/components/ScriptEditor.svelte | 15 +- .../src/lib/components/TimelineBar.svelte | 24 +- .../lib/components/graph/FlowGraphV2.svelte | 1 + .../recording/FlowRecordingReplay.svelte | 182 +++++++++----- .../recording/ScriptRecordingReplay.svelte | 237 ++++++++++++------ frontend/src/lib/forLater.ts | 4 + .../(root)/(logged)/replay/+page.svelte | 11 +- 15 files changed, 457 insertions(+), 293 deletions(-) diff --git a/frontend/package.json b/frontend/package.json index b7eed033fb..14e7119c7a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -295,6 +295,10 @@ "svelte": "./package/components/recording/ScriptRecordingReplay.svelte", "default": "./package/components/recording/ScriptRecordingReplay.svelte" }, + "./components/recording/types": { + "types": "./package/components/recording/types.d.ts", + "default": "./package/components/recording/types.js" + }, "./components/FlowWrapper.svelte": { "types": "./package/components/FlowWrapper.svelte.d.ts", "svelte": "./package/components/FlowWrapper.svelte", @@ -500,6 +504,9 @@ "components/ScriptRecordingReplay.svelte": [ "./package/components/recording/ScriptRecordingReplay.svelte.d.ts" ], + "components/recording/types": [ + "./package/components/recording/types.d.ts" + ], "components/FlowBuilder.svelte": [ "./package/components/FlowBuilder.svelte.d.ts" ], diff --git a/frontend/src/lib/components/FlowGraphViewer.svelte b/frontend/src/lib/components/FlowGraphViewer.svelte index 9bfbca3ee4..62e5bdac8e 100644 --- a/frontend/src/lib/components/FlowGraphViewer.svelte +++ b/frontend/src/lib/components/FlowGraphViewer.svelte @@ -26,6 +26,7 @@ workspace?: string | undefined minHeight?: number noBorder?: boolean + hideDefaultInputs?: boolean } let { @@ -38,7 +39,8 @@ stepDetail = $bindable(undefined), workspace = $workspaceStore, minHeight = 400, - noBorder = false + noBorder = false, + hideDefaultInputs = false }: Props = $props() const dispatch = createEventDispatcher() @@ -47,7 +49,9 @@
{#if !noGraph}
@@ -81,14 +85,14 @@ />
{/if} - {#if !noSide} + {#if !noSide && !(hideDefaultInputs && stepDetail == undefined)} {/if}
diff --git a/frontend/src/lib/components/FlowGraphViewerStep.svelte b/frontend/src/lib/components/FlowGraphViewerStep.svelte index 8c560abef0..077edc63e6 100644 --- a/frontend/src/lib/components/FlowGraphViewerStep.svelte +++ b/frontend/src/lib/components/FlowGraphViewerStep.svelte @@ -23,9 +23,10 @@ schema?: any | undefined stepDetail?: FlowModule | string | undefined jobScriptHash?: string | undefined + hideDefaultInputs?: boolean } - let { schema = undefined, stepDetail = undefined, jobScriptHash = undefined }: Props = $props() + let { schema = undefined, stepDetail = undefined, jobScriptHash = undefined, hideDefaultInputs = false }: Props = $props() let codeViewer: Drawer | undefined = $state() @@ -92,10 +93,10 @@
{#if stepDetail == undefined}
-

+

Click on a step to see its details

- {#if schema} + {#if schema && !hideDefaultInputs}

Flow Inputs

{/if} diff --git a/frontend/src/lib/components/FlowStatusViewer.svelte b/frontend/src/lib/components/FlowStatusViewer.svelte index 1642193a46..4b4e08c1b9 100644 --- a/frontend/src/lib/components/FlowStatusViewer.svelte +++ b/frontend/src/lib/components/FlowStatusViewer.svelte @@ -44,6 +44,7 @@ workspaceId = undefined, flowState = $bindable({}), selectedJobStep = $bindable(undefined), + hideFlowResult = false, hideTimeline = false, hideDownloadInGraph = false, hideNodeDefinition = false, @@ -175,6 +176,7 @@ } }} {showLogsWithResult} + {hideFlowResult} notes={notesProp} groups={groupsProp} /> diff --git a/frontend/src/lib/components/FlowStatusViewerInner.svelte b/frontend/src/lib/components/FlowStatusViewerInner.svelte index 8aaeadb5ef..d35ae53c47 100644 --- a/frontend/src/lib/components/FlowStatusViewerInner.svelte +++ b/frontend/src/lib/components/FlowStatusViewerInner.svelte @@ -136,6 +136,7 @@ } showLogsWithResult?: boolean showJobDetailHeader?: boolean + hideFlowResult?: boolean notes?: FlowNote[] groups?: FlowValue['groups'] } @@ -178,6 +179,7 @@ toolCallStore, showLogsWithResult = false, showJobDetailHeader = false, + hideFlowResult = false, notes: notesProp = undefined, groups: groupsProp = undefined }: Props = $props() @@ -1356,7 +1358,7 @@ />
{/if} - {:else if render} + {:else if render && !hideFlowResult}
{#if showLogsWithResult && job} @@ -2141,7 +2143,7 @@ likely did not run yet

{/if} - {:else}

Select a node to see its details here

{/if}
@@ -2157,7 +2159,7 @@ {#if node?.job_id} {:else} -
Select a node with a job to see HTTP request traces
{/if} diff --git a/frontend/src/lib/components/FlowTimeline.svelte b/frontend/src/lib/components/FlowTimeline.svelte index 3fbb69503a..9bff159a03 100644 --- a/frontend/src/lib/components/FlowTimeline.svelte +++ b/frontend/src/lib/components/FlowTimeline.svelte @@ -81,36 +81,31 @@ }} /> {#if items} -
-
-
{min ? displayDate(new Date(min), true) : ''}
{#if max && min} - {/if}
{max ? displayDate(new Date(max), true) : ''}{#if !max && min}{#if now} - {msToSec(now - min, 1)}s - {/if}{/if}
-
-
-
-
-
Waiting for executor/Suspend
-
+
+
+
+ {min ? displayDate(new Date(min), true) : ''} +
+
+
+
+ Wait
- -
-
Execution
-
+
+
+ Execution
+ {#if max && min} + {msToSec(max - min, 1)}s + {/if} + {#if !max && min}{#if now} + {msToSec(now - min, 1)}s + {/if}{/if}
{#if selfWaitTime} -
- root: +
+ root: x.created_at && x.started_at)} -
-
-
+
+
{k.startsWith('subflow:') ? k.substring(8) : k} {#if localModuleStates[k]?.selectedForloop && (typ == 'forloopflow' || typ == 'whileloopflow')} @@ -141,70 +136,67 @@ {/if}
-
- {#if subItems?.length > 1} -
- {subItems?.length} jobs -
- {/if} - {#if min && total} - subItems?.[index]?.id} - > - {#snippet item({ index, style })} - {@const b = subItems?.[index]} - {#if b?.created_at} - - {@const waitingLen = b?.created_at - ? b.started_at - ? b.started_at - b?.created_at - : b.duration_ms - ? 0 - : now - b?.created_at - : 0} -
+ {#if subItems?.length > 1} + + {subItems?.length} jobs + + {/if} +
+
+ {#if min && total} + subItems?.[index]?.id} + > + {#snippet item({ index, style })} + {@const b = subItems?.[index]} + {#if b?.created_at} + {@const waitingLen = b?.created_at + ? b.started_at + ? b.started_at - b?.created_at + : b.duration_ms + ? 0 + : now - b?.created_at + : 0} +
+ + {#if b.started_at} - {#if b.started_at} - - {/if} -
- {:else} -
-
- -
-
- {/if} - {/snippet} -
- {/if}
+ {/if} +
+ {:else} +
+ {/if} + {/snippet} + + {/if} +
{/each}
+ {:else} {/if} diff --git a/frontend/src/lib/components/FlowViewer.svelte b/frontend/src/lib/components/FlowViewer.svelte index 2355ddf68d..1e321584e3 100644 --- a/frontend/src/lib/components/FlowViewer.svelte +++ b/frontend/src/lib/components/FlowViewer.svelte @@ -20,7 +20,7 @@ schema?: any } - type TabValue = 'ui' | 'raw' | 'schema' | 'diff' + export type TabValue = 'ui' | 'raw' | 'schema' | 'diff' interface Props { flow: { @@ -33,10 +33,16 @@ noSide?: boolean noGraph?: boolean initTab?: TabValue + selectedTab?: TabValue + hideTabs?: boolean noSummary?: boolean + noInput?: boolean + hideDefaultInputs?: boolean + showStepHint?: boolean noGraphDownload?: boolean availableVersions?: Array<{ id: number; deployment_msg?: string }> selectedVersionId?: number + graphContent?: import('svelte').Snippet } let { @@ -46,9 +52,15 @@ noGraph = false, availableVersions = undefined, initTab = undefined, + selectedTab = $bindable(), + hideTabs = false, noSummary = false, + noInput = false, + hideDefaultInputs = false, + showStepHint = false, noGraphDownload = false, - selectedVersionId = undefined + selectedVersionId = undefined, + graphContent = undefined }: Props = $props() let open: { [id: number]: boolean } = {} @@ -59,7 +71,10 @@ let previousVersionId: number | undefined = $state(undefined) let previousFlow: PreviousFlow | undefined = $state(undefined) - let tab: TabValue = $state(untrack(() => initTab) ?? 'diff') + const tabControlledExternally = selectedTab !== undefined + if (!tabControlledExternally) { + selectedTab = initTab ?? 'diff' + } let previousFlowCache: Record = {} @@ -90,16 +105,16 @@ }) $effect.pre(() => { - if (initTab) { + if (initTab || tabControlledExternally) { return } if (availableVersions && availableVersions.length > 0) { - tab = 'diff' + selectedTab = 'diff' } else { if (noGraph) { - tab = 'schema' + selectedTab = 'schema' } else { - tab = 'ui' + selectedTab = 'ui' } } }) @@ -127,7 +142,7 @@ - + {#if availableVersions && availableVersions.length > 0} {/if} @@ -167,23 +182,38 @@ {/if} -
- {#if !noSummary} -

{flow.summary}

-
{flow.description ?? ''}
- {/if} + {#if graphContent} + {@render graphContent()} + {:else} +
+ {#if showStepHint} +

Click on a step to see its details

+ {/if} + {#if !noSummary} +

{flow.summary}

+
{flow.description ?? ''}
+ {/if} -

- Flow Input -

- {#if flow.schema && flow.schema.properties && Object.keys(flow.schema.properties).length > 0 && flow.schema} - - {:else} -
No inputs
- {/if} + {#if !noInput} +

+ Flow Input +

+ {#if flow.schema && flow.schema.properties && Object.keys(flow.schema.properties).length > 0 && flow.schema} + + {:else} +
No inputs
+ {/if} + {/if} - -
+ +
+ {/if}
diff --git a/frontend/src/lib/components/JobLoader.svelte b/frontend/src/lib/components/JobLoader.svelte index d8a0adfd35..78429177a7 100644 --- a/frontend/src/lib/components/JobLoader.svelte +++ b/frontend/src/lib/components/JobLoader.svelte @@ -107,7 +107,7 @@ $effect(() => { if (noLogs != lastNoLogs) { lastNoLogs = noLogs - if (!noLogs) { + if (!noLogs && !getActiveReplay()) { currentEventSource?.onerror?.(new Event(noLogsChangeRestartEvent)) const lastJobId = lastCompletedJobId if (lastJobId && (job || lastCallbacks?.loadExtraLogs)) { @@ -255,7 +255,7 @@ } } export async function getLogs() { - if (job) { + if (job && !getActiveReplay()) { refreshLogOffset() const getUpdate = await JobService.getJobUpdates({ workspace: workspace!, diff --git a/frontend/src/lib/components/ScriptEditor.svelte b/frontend/src/lib/components/ScriptEditor.svelte index 2690301f28..2bc5fe8575 100644 --- a/frontend/src/lib/components/ScriptEditor.svelte +++ b/frontend/src/lib/components/ScriptEditor.svelte @@ -632,6 +632,10 @@ } export async function runTest() { + // Discard any previous recording when running a normal test + if (!scriptRecording.active) { + lastRecording = undefined + } // Not defined if JobProgressBar not loaded jobProgressBar?.reset() // Flush module edits back to modules map before running preview @@ -1530,16 +1534,7 @@ displayName: 'Test & record', icon: Disc, action: () => recordAndTest() - }, - ...(lastRecording - ? [ - { - displayName: 'Download recording', - icon: Download, - action: () => downloadRecording() - } - ] - : []) + } ]} />
diff --git a/frontend/src/lib/components/TimelineBar.svelte b/frontend/src/lib/components/TimelineBar.svelte index e4fb511450..5d1f53e961 100644 --- a/frontend/src/lib/components/TimelineBar.svelte +++ b/frontend/src/lib/components/TimelineBar.svelte @@ -14,6 +14,7 @@ running: boolean concat?: boolean gray?: boolean + spacerClass?: string } let { @@ -25,25 +26,26 @@ id, running, concat = false, - gray = false + gray = false, + spacerClass = '' }: Props = $props() {#if min && started_at != undefined} {#if !concat} -
+
{/if} {#snippet text()} 0} {@const narrow = len / total < 0.09} - {@const endPos = started_at != undefined && min != undefined ? (started_at - min + len) / total : 1} + {@const endPos = + started_at != undefined && min != undefined ? (started_at - min + len) / total : 1} {@const nearStart = endPos < 0.15} - {#if len}{msToSec(len, 1)}s{/if} {/if} diff --git a/frontend/src/lib/components/graph/FlowGraphV2.svelte b/frontend/src/lib/components/graph/FlowGraphV2.svelte index 6a4d208579..47626fb41c 100644 --- a/frontend/src/lib/components/graph/FlowGraphV2.svelte +++ b/frontend/src/lib/components/graph/FlowGraphV2.svelte @@ -921,6 +921,7 @@ document.addEventListener('keydown', globalKeyDownHandler) + return () => { document.removeEventListener('keydown', globalKeyDownHandler) } diff --git a/frontend/src/lib/components/recording/FlowRecordingReplay.svelte b/frontend/src/lib/components/recording/FlowRecordingReplay.svelte index c5d94a7b95..afd2cee94c 100644 --- a/frontend/src/lib/components/recording/FlowRecordingReplay.svelte +++ b/frontend/src/lib/components/recording/FlowRecordingReplay.svelte @@ -2,7 +2,8 @@ import type { Job } from '$lib/gen' import { workspaceStore } from '$lib/stores' import FlowStatusViewer from '$lib/components/FlowStatusViewer.svelte' - import FlowViewer from '$lib/components/FlowViewer.svelte' + import FlowViewer, { type TabValue } from '$lib/components/FlowViewer.svelte' + import FlowGraphViewer from '$lib/components/FlowGraphViewer.svelte' import FlowProgressBar from '$lib/components/flows/FlowProgressBar.svelte' import FlowExecutionStatus from '$lib/components/runs/FlowExecutionStatus.svelte' import { setActiveReplay } from './flowRecording.svelte' @@ -13,21 +14,37 @@ import { InfoIcon, LogOut, Play, Square } from 'lucide-svelte' import { onDestroy } from 'svelte' - interface Props { - recording: FlowRecording - } - - let { recording }: Props = $props() - type ReplayState = 'loaded' | 'playing' - let replayState: ReplayState = $state('loaded') + interface Props { + recording: FlowRecording + selectedTab?: TabValue + replayState?: ReplayState + hideControls?: boolean + hideTabs?: boolean + } + + let { + recording, + selectedTab = $bindable(), + replayState = $bindable(), + hideControls = false, + hideTabs = false + }: Props = $props() + + if (selectedTab === undefined) { + selectedTab = 'ui' + } + if (replayState === undefined) { + replayState = 'loaded' + } + let rootJobId: string | undefined = $state(undefined) let rootInitialJob: Job | undefined = $state(undefined) let job: Job | undefined = $state(undefined) let done = $derived((job as any)?.type === 'CompletedJob') - function stop() { + export function stop() { setActiveReplay(undefined) job = undefined initRecording() @@ -36,10 +53,7 @@ function findRootJobId(data: FlowRecording): string | undefined { for (const [id, recorded] of Object.entries(data.jobs)) { const j = recorded.initial_job - if ( - (j.job_kind === 'flow' || j.job_kind === 'flowpreview') && - !j.parent_job - ) { + if ((j.job_kind === 'flow' || j.job_kind === 'flowpreview') && !j.parent_job) { return id } } @@ -81,17 +95,19 @@ for (const mod of fs.modules) { const durations = mod.flow_jobs_duration if (durations?.started_at) { - durations.started_at = durations.started_at.map( - (d: string) => offsetDate(d) ?? d - ) + durations.started_at = durations.started_at.map((d: string) => offsetDate(d) ?? d) } } } for (const recorded of Object.values(data.jobs)) { offsetJobTimestamps(recorded.initial_job) + if (recorded.initial_job?.flow_status) offsetFlowStatus(recorded.initial_job.flow_status) for (const event of recorded.events) { - if (event.data?.job) offsetJobTimestamps(event.data.job) + if (event.data?.job) { + offsetJobTimestamps(event.data.job) + if (event.data.job.flow_status) offsetFlowStatus(event.data.job.flow_status) + } if (event.data?.flow_status) offsetFlowStatus(event.data.flow_status) } } @@ -141,22 +157,27 @@ // Push the root's completed event to fire after all sub-job events let completedIdx = -1 for (let i = rootEvents.length - 1; i >= 0; i--) { - if (rootEvents[i].data.completed) { completedIdx = i; break } + if (rootEvents[i].data.completed) { + completedIdx = i + break + } } if (completedIdx >= 0 && rootEvents[completedIdx].t < maxSubJobT) { rootEvents[completedIdx].t = maxSubJobT + 50 } } - function startReplay() { + export function startReplay() { + if (!rootJobId) return // JSON round-trip to unwrap reactive proxies and strip non-cloneable properties const snapshot = JSON.parse(JSON.stringify(recording)) as FlowRecording - fixEventOrdering(snapshot, rootJobId!) - rebaseTimestamps(snapshot, rootJobId!) + fixEventOrdering(snapshot, rootJobId) + rebaseTimestamps(snapshot, rootJobId) setActiveReplay(snapshot) - rootInitialJob = buildInitialJob(snapshot, rootJobId!) + rootInitialJob = buildInitialJob(snapshot, rootJobId) job = undefined replayState = 'playing' + selectedTab = 'ui' } onDestroy(() => { @@ -173,52 +194,81 @@

-{:else if replayState === 'loaded'} +{:else}
-
-
-

{recording.flow_path}

- - - {#snippet text()} - - Recorded {new Date(recording.recorded_at).toLocaleString()} — - {(recording.total_duration_ms / 1000).toFixed(1)}s - - {/snippet} - + {#if !hideControls} +
+
+

+ {replayState === 'playing' ? 'Replaying: ' : ''}{recording.flow_path} +

+ + + {#snippet text()} + + Recorded {new Date(recording.recorded_at).toLocaleString()} — + {(recording.total_duration_ms / 1000).toFixed(1)}s + + {/snippet} + +
+ {#if replayState === 'loaded'} + + {:else} + + {/if}
- -
- -
-{:else if replayState === 'playing' && rootJobId} -
-
-

Replaying: {recording.flow_path}

- -
- - {#if job} - {/if} - + + + {#snippet graphContent()} + {#if replayState === 'playing' && rootJobId} +
+ + {#if job} + + {/if} + +
+ {:else} +
+

Click on a step to see its details

+ +
+ {/if} + {/snippet} +
{/if} diff --git a/frontend/src/lib/components/recording/ScriptRecordingReplay.svelte b/frontend/src/lib/components/recording/ScriptRecordingReplay.svelte index 8c3d28a71a..0327044b3b 100644 --- a/frontend/src/lib/components/recording/ScriptRecordingReplay.svelte +++ b/frontend/src/lib/components/recording/ScriptRecordingReplay.svelte @@ -12,6 +12,7 @@ import { json as jsonLang } from 'svelte-highlight/languages' import HighlightTheme from '$lib/components/HighlightTheme.svelte' import JobArgs from '$lib/components/JobArgs.svelte' + import SchemaForm from '$lib/components/SchemaForm.svelte' import DisplayResult from '$lib/components/DisplayResult.svelte' import LogViewer from '$lib/components/LogViewer.svelte' import { ClipboardCopy, InfoIcon, LogOut, Play, Square } from 'lucide-svelte' @@ -19,15 +20,26 @@ import { onDestroy, tick } from 'svelte' import JobLoader from '$lib/components/JobLoader.svelte' - interface Props { - recording: ScriptRecording - } - - let { recording }: Props = $props() + export type ScriptTabValue = 'parameters' | 'code' | 'args' | 'schema' | 'result' type ReplayState = 'loaded' | 'playing' - let replayState: ReplayState = $state('loaded') + interface Props { + recording: ScriptRecording + selectedTab?: ScriptTabValue + replayState?: ReplayState + hideControls?: boolean + hideTabs?: boolean + } + + let { + recording, + selectedTab = $bindable(), + replayState = $bindable(), + hideControls = false, + hideTabs = false + }: Props = $props() + let jobId: string | undefined = $state(undefined) let job: Job | undefined = $state(undefined) let jobLoader: JobLoader | undefined = $state(undefined) @@ -35,7 +47,18 @@ let scriptRecordingStore = createScriptRecording() - function stop() { + let schema = $derived(recording.schema) + + if (selectedTab === undefined) { + if (schema && recording.args) selectedTab = 'parameters' + else if (recording.args && Object.keys(recording.args).length > 0) selectedTab = 'args' + else selectedTab = 'code' + } + if (replayState === undefined) { + replayState = 'loaded' + } + + export function stop() { setActiveReplay(undefined) job = undefined replayState = 'loaded' @@ -85,13 +108,14 @@ initRecording() - async function startReplay() { + export async function startReplay() { const snapshot = JSON.parse(JSON.stringify(recording)) as ScriptRecording rebaseTimestamps(snapshot) const replayData = scriptRecordingStore.toReplayData(snapshot) setActiveReplay(replayData) job = undefined replayState = 'playing' + selectedTab = 'result' await tick() if (jobLoader && jobId) { jobLoader.watchJob(jobId) @@ -101,8 +125,6 @@ onDestroy(() => { setActiveReplay(undefined) }) - - let schema = $derived(recording.schema) @@ -115,48 +137,141 @@

-{:else if replayState === 'loaded'} -
-
-
-

{recording.script_path || 'Untitled script'}

- {recording.language} - - - {#snippet text()} - - Recorded {new Date(recording.recorded_at).toLocaleString()} — - {(recording.total_duration_ms / 1000).toFixed(1)}s - - {/snippet} - +{:else} +
+ {#if !hideControls} +
+
+

+ {replayState === 'playing' ? 'Replaying: ' : ''}{recording.script_path || + 'Untitled script'} +

+ + {recording.language} + + + + {#snippet text()} + + Recorded {new Date(recording.recorded_at).toLocaleString()} — + {(recording.total_duration_ms / 1000).toFixed(1)}s + + {/snippet} + +
+ {#if replayState === 'loaded'} + + {:else} + + {/if}
- -
- - {#if recording.args && Object.keys(recording.args).length > 0} - {/if} - - + {#if replayState === 'playing'} + + {/if} + + + {#if replayState === 'playing'} + + {/if} + {#if schema && recording.args} + + {/if} + {#if recording.args && Object.keys(recording.args).length > 0} + + {/if} + {#if !schema || !recording.args} + + {/if} {#if schema} {/if} {#snippet content()} + + {#if replayState === 'playing' && jobId} +
+
+

Result

+
+ {#if job !== undefined && job.type === 'CompletedJob' && job.result !== undefined} + + {:else if done} +
+ No output available +
+ {:else} +
+ Waiting for result... +
+ {/if} +
+
+
+

Logs

+
+ +
+
+
+ {/if} +
+ + {#if schema && recording.args} +
+
+ +
+
+ +
+
+ {/if} +
+ + {#if recording.args && Object.keys(recording.args).length > 0} +
+ +
+ {/if} +
-
+
@@ -180,46 +295,4 @@ {/snippet}
-{:else if replayState === 'playing' && jobId} -
-
-

Replaying: {recording.script_path || 'Untitled script'}

- -
- - - {#if done && job} -
-

Result

-
- {#if job.type === 'CompletedJob' && job.result !== undefined} - - {:else} -
No result available
- {/if} -
-
- {/if} - -
- -
-
{/if} diff --git a/frontend/src/lib/forLater.ts b/frontend/src/lib/forLater.ts index ea290e2a53..b3ce8001e2 100644 --- a/frontend/src/lib/forLater.ts +++ b/frontend/src/lib/forLater.ts @@ -1,6 +1,7 @@ import { get } from 'svelte/store' import { dbClockDrift } from './stores' import { JobService } from './gen' +import { getActiveReplay } from './components/recording/flowRecording.svelte' import pLimit from 'p-limit' function subtractSeconds(date: Date, seconds: number): Date { @@ -26,6 +27,9 @@ export function forLater(scheduledString: string): boolean { const limit = pLimit(1) export function getDbClockNow() { + if (getActiveReplay()) { + return new Date() + } let drift = get(dbClockDrift) if (drift == undefined) { limit(() => computeDrift()) diff --git a/frontend/src/routes/(root)/(logged)/replay/+page.svelte b/frontend/src/routes/(root)/(logged)/replay/+page.svelte index 559961aff0..d89bcbb9b0 100644 --- a/frontend/src/routes/(root)/(logged)/replay/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/replay/+page.svelte @@ -51,14 +51,14 @@
{#if flowRecording}
-
{:else if scriptRecording}
-
@@ -70,12 +70,7 @@

Upload a recording JSON file to replay a flow or script execution offline.

- + Drag and drop a recording file