pagescene: finish anonymous-use polish

- Gate File → Load my scene for anon: route through promptSignup
  instead of opening a dialog that would just error-toast on the
  silent listUserMediaFiles failure.
- Remove dead SettingsModal mount in Controls3D (no UI ever triggered
  it). Drops the @storyteller/ui-settings-modal import from this file.
- New AnonHintChip pill in the top-right corner of the playground
  ("Sign up to save"). Renders only when currentUserToken is undefined
  AND not in view-only mode; click routes to promptSignup. Tauri users
  never see it.
- Scaffolding for view-only PreviewBox: new component renders a small
  card with the author's generation result; returns null when no URL
  is supplied. Mounted in Stage3DBody under the isViewOnly guard,
  reading sceneMeta.previewImageUrl (new optional field). Data-path
  wiring (populating previewImageUrl from adapter.loadScene's
  response) is the next step — leaving the UI plumbing in place so
  that follow-up is purely data, not layout surgery.
This commit is contained in:
Tanish Manku committed 2026-05-13 09:08:43 -07:00
1 parent a9fa8d710a
commit 008fcfeaa7
9 files changed
+112 -25

No files matched your search

@@ -187,6 +187,7 @@ export { getSceneGenerationMetaData } from "./lib/sceneMetadata";
// signalScene, addToast, loadingBarData/IsShowing signals, etc.)
// are either surfaced via adapter slots or replaced with
// lib-internal state.
export { AnonHintChip } from "./lib/comps/AnonHintChip";
export { AssetMenu, AssetModal } from "./lib/comps/AssetMenu";
export { Controls3D } from "./lib/comps/Controls3D";
export { ControlsTopButtons } from "./lib/comps/ControlsTopButtons";
@@ -199,6 +200,8 @@ export { EditorCanvas, CameraViewCanvas } from "./lib/comps/EngineCanvases";
export { Outliner } from "./lib/comps/Outliner";
export { ControlPanelSceneObject } from "./lib/comps/ControlPanelSceneObject";
export { SceneContainer } from "./lib/comps/SceneContainer";
export { PreviewBox } from "./lib/comps/PreviewBox";
export type { PreviewBoxProps } from "./lib/comps/PreviewBox";
export { PreviewEngineCamera } from "./lib/comps/PreviewEngineCamera";
export { PreviewImages } from "./lib/comps/PreviewImages";
export { OnboardingHelper } from "./lib/comps/OnboardingHelper";
@@ -22,6 +22,10 @@ export interface SceneMeta {
ownerToken: string | undefined;
isModified: boolean | undefined;
isInitializing: boolean;
// URL to the author's generation result for this scene, shown in
// view-only mode's PreviewBox. Host populates this from the
// adapter.loadScene response. Optional; absent ⇒ no preview rendered.
previewImageUrl?: string;
}
export type SceneObjectKind = "object" | "character" | "shape";
@@ -33,6 +33,7 @@ import type { PopoverItem } from "@storyteller/ui-popover";
import { v4 as uuidv4 } from "uuid";
import { EngineContext } from "./contexts/EngineContext/EngineContext";
import { AnonHintChip } from "./comps/AnonHintChip";
import { ControlPanelSceneObject } from "./comps/ControlPanelSceneObject";
import { Controls3D } from "./comps/Controls3D";
import { ControlsTopButtons } from "./comps/ControlsTopButtons";
@@ -41,6 +42,7 @@ import { FocalLengthDisplay } from "./comps/FocalLengthDisplay/FocalLengthDispla
import { OnboardingHelper } from "./comps/OnboardingHelper";
import { Outliner } from "./comps/Outliner";
import { PoseModeSelector } from "./comps/PoseModeSelector";
import { PreviewBox } from "./comps/PreviewBox";
import { PreviewEngineCamera } from "./comps/PreviewEngineCamera";
import { SceneContainer } from "./comps/SceneContainer";
import { addCharacter, addObject, setCameraAspect } from "./actions";
@@ -74,6 +76,7 @@ export const Stage3DBody = () => {
(s) => s.setIsPromptBoxFocused,
);
const gridVisible = usePageSceneStore((s) => s.gridVisible);
const previewImageUrl = usePageSceneStore((s) => s.sceneMeta.previewImageUrl);
const isViewOnly = useIsViewOnly();
const addCamera = usePageSceneStore((s) => s.addCamera);
const updateCamera = usePageSceneStore((s) => s.updateCamera);
@@ -344,6 +347,9 @@ export const Stage3DBody = () => {
<div className="grid grid-cols-3 gap-4">
<ControlsTopButtons />
<Controls3D />
<div className="flex items-start justify-end gap-2 pr-2 pt-2">
<AnonHintChip />
</div>
</div>
</div>
)}
@@ -364,8 +370,7 @@ export const Stage3DBody = () => {
{!isViewOnly && <ControlPanelSceneObject />}
</div>
{/* Future: <PreviewBox /> slot when isViewOnly — shows the
author's generated still next to the camera. */}
{isViewOnly && <PreviewBox imageUrl={previewImageUrl} />}
{!isViewOnly && <PromptBox3D
cameras={cameras}
@@ -0,0 +1,33 @@
import { useContext } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faArrowRightToBracket } from "@fortawesome/pro-solid-svg-icons";
import { EngineContext } from "../../contexts/EngineContext/EngineContext";
import { usePageSceneStore, useIsViewOnly } from "../../PageSceneStore";
// Quiet "you're signed out" affordance shown in the editor playground for
// anonymous visitors. Hidden in view-only mode (the visitor is consuming
// someone else's scene; no upsell pressure needed). The actual per-action
// signup CTAs (Save, Generate, Upload) still fire when the user clicks
// those buttons — this chip just sets expectations up front.
export const AnonHintChip = () => {
const editor = useContext(EngineContext);
const currentUserToken = usePageSceneStore((s) => s.currentUserToken);
const isViewOnly = useIsViewOnly();
if (currentUserToken || isViewOnly) return null;
const handleClick = () => {
editor?.adapter.promptSignup?.("hint");
};
return (
<button
type="button"
onClick={handleClick}
className="glass flex items-center gap-2 rounded-full border border-white/10 px-3 py-1.5 text-xs font-medium text-white/80 shadow-md transition-colors hover:bg-ui-controls/100 hover:text-white"
>
<FontAwesomeIcon icon={faArrowRightToBracket} className="opacity-70" />
Sign up to save
</button>
);
};
@@ -0,0 +1 @@
export { AnonHintChip } from "./AnonHintChip";
@@ -14,7 +14,6 @@ import {
import { ButtonIconSelect } from "@storyteller/ui-button-icon-select";
import { Button } from "@storyteller/ui-button";
import { Tooltip } from "@storyteller/ui-tooltip";
import { SettingsModal } from "@storyteller/ui-settings-modal";
import { PopoverMenu } from "@storyteller/ui-popover";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
@@ -52,7 +51,6 @@ export const Controls3D = () => {
})),
);
const [showEmptySceneTooltip, setShowEmptySceneTooltip] = useState(false);
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
const [upload3DIsShowing, setUpload3DIsShowing] = useState(false);
const [isAddAssetPopoverOpen, setIsAddAssetPopoverOpen] = useState(false);
const [uploadImageIsShowing, setUploadImageIsShowing] = useState(false);
@@ -306,13 +304,6 @@ export const Controls3D = () => {
<AssetModal />
<SettingsModal
isOpen={isSettingsModalOpen}
onClose={() => setIsSettingsModalOpen(false)}
globalAccountLogoutCallback={() => editor?.adapter.performLogout()}
initialSection="accounts"
/>
{editor &&
editor.adapter.renderAssetUploader({
isOpen: upload3DIsShowing,
@@ -169,20 +169,26 @@ export const ControlsTopButtons = () => {
{
label: "Load my scene",
description: "Ctrl+O",
dialogProps: {
title: "Load a Saved Scene",
content: (
<LoadUserScenes onSceneSelect={handleSceneSelection} />
),
confirmButtonProps: {
label: "Load",
disabled: sceneTokenSelected === "",
onClick: handleButtonLoadScene,
},
closeButtonProps: { label: "Cancel" },
showClose: true,
className: "max-w-5xl",
},
...(currentUserToken
? {
dialogProps: {
title: "Load a Saved Scene",
content: (
<LoadUserScenes onSceneSelect={handleSceneSelection} />
),
confirmButtonProps: {
label: "Load",
disabled: sceneTokenSelected === "",
onClick: handleButtonLoadScene,
},
closeButtonProps: { label: "Cancel" },
showClose: true,
className: "max-w-5xl",
},
}
: {
onClick: () => editor?.adapter.promptSignup?.("load"),
}),
},
{
disabled: !canSave,
@@ -0,0 +1,42 @@
import { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faXmark } from "@fortawesome/pro-solid-svg-icons";
// Small fixed-position card shown next to the camera in view-only mode.
// Renders the author's generation result so a visitor can see what the
// scene was made to produce. Today this is scaffolding only — the URL
// data path (likely a new field returned by adapter.loadScene's response
// payload, propagated into sceneMeta.previewImageUrl) is wired up in a
// follow-up PR. Renders null when no URL is supplied, so it's safe to
// mount unconditionally inside the isViewOnly branch.
export interface PreviewBoxProps {
imageUrl?: string;
}
export const PreviewBox = ({ imageUrl }: PreviewBoxProps) => {
const [dismissed, setDismissed] = useState(false);
if (!imageUrl || dismissed) return null;
return (
<div className="glass absolute bottom-6 right-6 z-20 flex w-48 flex-col gap-2 rounded-lg border border-white/10 p-2 shadow-lg">
<button
type="button"
onClick={() => setDismissed(true)}
className="absolute right-1 top-1 flex h-5 w-5 items-center justify-center rounded-full text-white/60 hover:bg-white/10 hover:text-white"
aria-label="Hide preview"
>
<FontAwesomeIcon icon={faXmark} className="text-xs" />
</button>
<img
src={imageUrl}
crossOrigin="anonymous"
alt="Author's generation"
className="rounded-md"
/>
<span className="text-center text-[11px] text-white/60">
Author's generation
</span>
</div>
);
};
@@ -0,0 +1,2 @@
export { PreviewBox } from "./PreviewBox";
export type { PreviewBoxProps } from "./PreviewBox";