Add native Workflow export to daydream.live#636
Conversation
📝 WalkthroughWalkthroughThe changes introduce Daydream export functionality across the frontend by adding authentication and export state props to multiple components, creating a new Daydream API utility module, and implementing the core export logic with state management in StreamPage. Changes
Sequence DiagramsequenceDiagram
participant User as User
participant StreamPage
participant AuthService as Auth Service
participant DaydreamAPI as Daydream API
participant ExternalBrowser as External Browser
User->>StreamPage: Click "Export to daydream.live"
alt User Not Authenticated
StreamPage->>AuthService: Check authentication
AuthService-->>StreamPage: Not authenticated
StreamPage->>AuthService: Redirect to sign-in
AuthService->>ExternalBrowser: Open sign-in page
else User Authenticated
StreamPage->>AuthService: Check authentication
AuthService-->>StreamPage: Authenticated
StreamPage->>StreamPage: Build ScopeWorkflow with settings<br/>prompts, plugins, LoRAs
StreamPage->>StreamPage: Retrieve Daydream API key
StreamPage->>DaydreamAPI: createDaydreamImportSession(apiKey, workflowData)
DaydreamAPI->>DaydreamAPI: POST /v1/workflows/import-sessions
DaydreamAPI-->>StreamPage: ImportSessionResponse {token, createUrl}
StreamPage->>ExternalBrowser: Open createUrl
StreamPage->>User: Show success toast
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
Comment |
Signed-off-by: Thom Shutt <[email protected]>
a2cfa7d to
b071f41
Compare
🚀 fal.ai Preview Deployment
TestingConnect to this preview deployment by running this on your branch: 🧪 E2E tests will run automatically against this deployment. |
✅ E2E Tests passed
Test ArtifactsCheck the workflow run for screenshots. |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
frontend/src/components/PromptTimeline.tsx (1)
155-157: Don’t mask a missing Daydream export binding with a no-op.These props are optional upstream, but this wrapper still gives
ExportDialoga callable handler. A caller can omit the feature wiring and still render a Daydream CTA that silently does nothing. Make the export props required end-to-end or omit the CTA when no real handler is present.Also applies to: 615-617
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@frontend/src/components/PromptTimeline.tsx` around lines 155 - 157, The optional Daydream export props (onExportToDaydream, isAuthenticated, isExportingToDaydream) on the PromptTimeline wrapper are masking a missing export handler and can render an ExportDialog CTA that silently no-ops; update PromptTimeline (and the other instance at the referenced lines) to either make these props required end-to-end (remove the ? from onExportToDaydream/isAuthenticated/isExportingToDaydream) or, preferably, gate rendering of the ExportDialog/CTA behind a truthy onExportToDaydream check so the CTA is omitted when no real handler is supplied; adjust PropTypes/TS types and any parent callers to pass the handler or rely on the conditional render to avoid a silent no-op.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@frontend/src/pages/StreamPage.tsx`:
- Around line 302-340: The browser popup is blocked because
openExternalUrl(result.createUrl) runs after an await; to fix, open a blank
window synchronously before the async call and then navigate it when the URL is
available: call window.open("", "_blank") (store the returned Window reference)
immediately before awaiting createDaydreamImportSession in the export flow in
StreamPage (where createDaydreamImportSession and openExternalUrl are used),
then after the await set the window.location.href (or call openExternalUrl with
the stored ref) to result.createUrl; keep existing toasts and
setIsExportingToDaydream logic intact.
---
Nitpick comments:
In `@frontend/src/components/PromptTimeline.tsx`:
- Around line 155-157: The optional Daydream export props (onExportToDaydream,
isAuthenticated, isExportingToDaydream) on the PromptTimeline wrapper are
masking a missing export handler and can render an ExportDialog CTA that
silently no-ops; update PromptTimeline (and the other instance at the referenced
lines) to either make these props required end-to-end (remove the ? from
onExportToDaydream/isAuthenticated/isExportingToDaydream) or, preferably, gate
rendering of the ExportDialog/CTA behind a truthy onExportToDaydream check so
the CTA is omitted when no real handler is supplied; adjust PropTypes/TS types
and any parent callers to pass the handler or rely on the conditional render to
avoid a silent no-op.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 399168f1-b9f7-42ea-9e6b-96d201ad2b14
📒 Files selected for processing (5)
frontend/src/components/ExportDialog.tsxfrontend/src/components/PromptInputWithTimeline.tsxfrontend/src/components/PromptTimeline.tsxfrontend/src/lib/daydreamExport.tsfrontend/src/pages/StreamPage.tsx
| const apiKey = getDaydreamAPIKey(); | ||
| if (!apiKey) { | ||
| toast.error("Not authenticated with Daydream"); | ||
| return; | ||
| } | ||
|
|
||
| setIsExportingToDaydream(true); | ||
| try { | ||
| const pluginInfoMap = new Map<string, PluginInfo>( | ||
| plugins.map(p => [p.name, p]) | ||
| ); | ||
|
|
||
| const workflow = buildScopeWorkflow({ | ||
| name: "Untitled Workflow", | ||
| settings, | ||
| timelinePrompts, | ||
| promptState: { | ||
| promptItems, | ||
| interpolationMethod, | ||
| transitionSteps, | ||
| temporalInterpolationMethod, | ||
| }, | ||
| pipelineInfoMap: pipelines ?? {}, | ||
| loraFiles, | ||
| pluginInfoMap, | ||
| scopeVersion: scopeVersion ?? "unknown", | ||
| }); | ||
|
|
||
| const result = await createDaydreamImportSession( | ||
| apiKey, | ||
| workflow, | ||
| workflow.metadata.name | ||
| ); | ||
|
|
||
| openExternalUrl(result.createUrl); | ||
| toast.success("Opening daydream.live...", { | ||
| description: | ||
| "Your workflow has been sent to daydream.live for publishing.", | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Do modern browsers typically block window.open() when it is invoked after an awaited fetch/promise inside a click handler, rather than synchronously in the original user gesture?
💡 Result:
Yes—in most modern browsers, window.open() is likely to be blocked if it runs after an await/promise boundary, even if the await occurs inside a click handler, because popup opening is generally gated on transient user activation and browsers often stop treating the call as “directly user-initiated” once execution is deferred. Popup blockers commonly return null in this case. [1]
Browsers expose this as the User Activation model: transient activation is short-lived and can be lost/consumed, and APIs such as window.open() require it. [2] WebKit also documents cases where user activation gets consumed such that a later window.open() fails, illustrating why “do something async, then open a popup” is fragile. [3]
Typical workaround: open a blank window synchronously during the click (while activation is definitely present), then navigate it after the async work completes. [1]
Sources:
[1] MDN — Window.open() popup/user-activation requirements and blocked return behavior (developer.mozilla.org)
[2] MDN — User activation (transient activation can expire/be consumed) (developer.mozilla.org)
[3] WebKit blog — User Activation API and activation being consumed affecting window.open() (webkit.org)
Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/API/Window/open?utm_source=openai
- 2: https://developer.mozilla.org/en-US/docs/Web/Security/User_activation?utm_source=openai
- 3: https://webkit.org/blog/13862/the-user-activation-api/?utm_source=openai
🏁 Script executed:
# Locate and examine the file mentioned in the review
fd "StreamPage.tsx" -type fRepository: daydreamlive/scope
Length of output: 232
🏁 Script executed:
# Look for the openExternal implementation
fd "openExternal.ts" -type fRepository: daydreamlive/scope
Length of output: 232
🏁 Script executed:
fd "StreamPage.tsx"Repository: daydreamlive/scope
Length of output: 95
🏁 Script executed:
fd "openExternal.ts"Repository: daydreamlive/scope
Length of output: 94
🏁 Script executed:
# Check the length of StreamPage.tsx to see if it's safe to read
wc -l frontend/src/pages/StreamPage.tsxRepository: daydreamlive/scope
Length of output: 100
🏁 Script executed:
# Read lines around the specified range (302-340)
sed -n '295,350p' frontend/src/pages/StreamPage.tsxRepository: daydreamlive/scope
Length of output: 1532
🏁 Script executed:
# Read the openExternal.ts file to understand the implementation
cat frontend/src/lib/openExternal.tsRepository: daydreamlive/scope
Length of output: 639
🏁 Script executed:
# Search for other uses of openExternalUrl to see if this is a widespread pattern
rg "openExternalUrl" frontend/src/Repository: daydreamlive/scope
Length of output: 519
🏁 Script executed:
# Search for other uses of window.open in the codebase
rg "window\.open" frontend/src/Repository: daydreamlive/scope
Length of output: 559
Preserve the user gesture before the async export call.
openExternalUrl() is reached only after awaiting createDaydreamImportSession(). In browser builds, window.open(url, "_blank") called after an await loses transient user activation and is blocked by popup blockers. The import session still gets created, but nothing opens and the success toast becomes a false positive.
💡 Minimal fix sketch
const apiKey = getDaydreamAPIKey();
if (!apiKey) {
toast.error("Not authenticated with Daydream");
return;
}
+ const pendingWindow =
+ window.scope?.openExternal == null
+ ? window.open("about:blank", "_blank")
+ : null;
+ if (pendingWindow) {
+ pendingWindow.opener = null;
+ }
+
setIsExportingToDaydream(true);- openExternalUrl(result.createUrl);
+ if (pendingWindow && !pendingWindow.closed) {
+ pendingWindow.location.replace(result.createUrl);
+ } else {
+ openExternalUrl(result.createUrl);
+ } } catch (err) {
+ pendingWindow?.close();
console.error("Export to daydream.live failed:", err);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const apiKey = getDaydreamAPIKey(); | |
| if (!apiKey) { | |
| toast.error("Not authenticated with Daydream"); | |
| return; | |
| } | |
| setIsExportingToDaydream(true); | |
| try { | |
| const pluginInfoMap = new Map<string, PluginInfo>( | |
| plugins.map(p => [p.name, p]) | |
| ); | |
| const workflow = buildScopeWorkflow({ | |
| name: "Untitled Workflow", | |
| settings, | |
| timelinePrompts, | |
| promptState: { | |
| promptItems, | |
| interpolationMethod, | |
| transitionSteps, | |
| temporalInterpolationMethod, | |
| }, | |
| pipelineInfoMap: pipelines ?? {}, | |
| loraFiles, | |
| pluginInfoMap, | |
| scopeVersion: scopeVersion ?? "unknown", | |
| }); | |
| const result = await createDaydreamImportSession( | |
| apiKey, | |
| workflow, | |
| workflow.metadata.name | |
| ); | |
| openExternalUrl(result.createUrl); | |
| toast.success("Opening daydream.live...", { | |
| description: | |
| "Your workflow has been sent to daydream.live for publishing.", | |
| }); | |
| const apiKey = getDaydreamAPIKey(); | |
| if (!apiKey) { | |
| toast.error("Not authenticated with Daydream"); | |
| return; | |
| } | |
| const pendingWindow = | |
| window.scope?.openExternal == null | |
| ? window.open("about:blank", "_blank") | |
| : null; | |
| if (pendingWindow) { | |
| pendingWindow.opener = null; | |
| } | |
| setIsExportingToDaydream(true); | |
| try { | |
| const pluginInfoMap = new Map<string, PluginInfo>( | |
| plugins.map(p => [p.name, p]) | |
| ); | |
| const workflow = buildScopeWorkflow({ | |
| name: "Untitled Workflow", | |
| settings, | |
| timelinePrompts, | |
| promptState: { | |
| promptItems, | |
| interpolationMethod, | |
| transitionSteps, | |
| temporalInterpolationMethod, | |
| }, | |
| pipelineInfoMap: pipelines ?? {}, | |
| loraFiles, | |
| pluginInfoMap, | |
| scopeVersion: scopeVersion ?? "unknown", | |
| }); | |
| const result = await createDaydreamImportSession( | |
| apiKey, | |
| workflow, | |
| workflow.metadata.name | |
| ); | |
| if (pendingWindow && !pendingWindow.closed) { | |
| pendingWindow.location.replace(result.createUrl); | |
| } else { | |
| openExternalUrl(result.createUrl); | |
| } | |
| toast.success("Opening daydream.live...", { | |
| description: | |
| "Your workflow has been sent to daydream.live for publishing.", | |
| }); | |
| } catch (err) { | |
| pendingWindow?.close(); | |
| console.error("Export to daydream.live failed:", err); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@frontend/src/pages/StreamPage.tsx` around lines 302 - 340, The browser popup
is blocked because openExternalUrl(result.createUrl) runs after an await; to
fix, open a blank window synchronously before the async call and then navigate
it when the URL is available: call window.open("", "_blank") (store the returned
Window reference) immediately before awaiting createDaydreamImportSession in the
export flow in StreamPage (where createDaydreamImportSession and openExternalUrl
are used), then after the await set the window.location.href (or call
openExternalUrl with the stored ref) to result.createUrl; keep existing toasts
and setIsExportingToDaydream logic intact.
Signed-off-by: Thom Shutt <[email protected]>
Summary by CodeRabbit