The spacebar branch in onkeydown called switchCameraView; remove it
along with the now-unused switchCameraView callback param/field. Editor
no longer passes that arg into MouseControls' constructor.
Drops the remaining 1-line forwarders on Editor and rewires their
callers to talk to the owning subsystem directly.
Inlined at call sites then deleted:
- updateOutliner → selection.refreshOutliner (3 actions/* callers)
- publishSelect → selection.publishSelect (1 action + helper.ts)
- updateSelectedUI → selection.updateSelectedUI (in-engine callers)
- cacheScene → save_manager.getSceneJson (EngineProvider)
- setColor → activeScene.setColor (ControlPanelSceneObject)
- isObjectLipsync deleted entirely (zero callers)
Relocated:
- lockUnlockObject + isObjectLocked moved onto SelectionBridge. The
bridge gains a SelectionBridgeDeps surface for the userData mutation
(still in SceneUtils as toggleObjectLocked) and the gizmo
attach/detach. Callers (Outliner, ControlPanelSceneObject) now go
through editor.selection.
- setSelectedObject (which despite the name wrote position/rotation/
scale onto the active selection) becomes
SceneManager.updateSelectedTransform — same data ownership, no more
reverse dependency from helper.ts into sceneManager.selected_objects.
helper.ts shrinks to removeTransformControls, isObjectLocked,
toggleObjectLocked, getSelectedSum, deleteObject. The pre-existing
TS errors in deleteObject (Mesh.texture / Material[] dispose) are
untouched — out of scope for this refactor.
GizmoController now reads/writes the store's transformSpace through a
new GizmoControllerDeps surface that Editor wires up in initialize().
- changeMode(type) reads the desired space from the store internally
(forces local for scale), so callers don't plumb space through.
- toggleTransformSpace() owns the read/flip/write/apply loop that used
to live on Editor.toggleTransformSpace.
Editor.change_mode and Editor.toggleTransformSpace are deleted.
Callers updated:
- actions/setTransformMode.ts → editor.gizmo.changeMode(...)
- Controls3D.tsx → editorEngine?.gizmo.toggleTransformSpace()
- keymap.ts → editor.gizmo.changeMode + .toggleTransformSpace; the
inline setGizmoMode body shrinks to one line and the duplicate
toggleTransformSpace helper is gone.
CameraController now owns switchCameraView. The body lifted from
SceneUtils.switchCameraView gets cross-subsystem capabilities
(scene access, transform-controls removal, selection write,
editor-state and object-panel store actions) through a new
CameraControllerDeps object that Editor wires up in initialize().
This keeps subsystems from importing each other directly: the
controller sees a narrow callback surface, not SelectionBridge or
SceneUtils. Same precedent as the existing ViewportController setters.
Deletes:
- SceneUtils.switchCameraView (helper.ts)
- Editor.switchCameraView facade
The space-bar handler in MouseControls now points directly at
cameraController.switchCameraView via the callback Editor passes in.
Removed Editor fields/methods left over from a removed video-export
feature: rendering, switchPreviewToggle, processingRecording,
processingHasFailed, recorder, globalSetTrackLengthSeconds,
startRenderHeight/Width, selectedCanvas, togglePlayback, updateLoad,
showLoading, endLoading. Every read of these flags was permanently
false; whole branches in renderScene/renderSingleFrame were unreachable.
The space-bar handler in keybinds_controls.ts was also dead today
(selectedCanvas was never set true) — rewired it to call
editor.switchCameraView() directly via a callback so pressing Space
toggles camera view as intended.
MouseControls' constructor drops the four dead params (selectedCanvas,
switchPreviewToggle, rendering, togglePlayback) and gains a single
switchCameraView callback in their place.
Owns the camera state, FreeCam plumbing, render aspect ratio, and the
per-frame camera tick. The camera-side of renderSingleFrame now reduces
to a single cameraController.tickPerFrame(delta) call that handles the
FOV-from-store sync, FreeCam integration with store write-back, the
camera_person_mode cam_obj transfer, and the render_camera mirror.
Editor still constructs the cameras and lockControls during initialize()
because they need the renderer's domElement, but the fields land on
cameraController. External callers in helper.ts, save_manager.ts,
useFreeCam.ts, setCameraAspect.ts, and ControlsTopButtons.tsx now reach
through editor.cameraController; no Editor-level forwarders.
editor.ts: 1074 -> 941 LOC.
Fourth step of the editor.ts decomposition. Pulls the engine→store
selection-and-outliner sync surface out of Editor into a dedicated
module.
SelectionBridge owns:
selected (current inspected object)
last_selected_sum (frame-to-frame transform-change check)
Methods take their dependencies as method arguments / closure-based
deps — no `editor` reference, no `import type Editor`, no circular
import. Constructor takes:
getSceneManager: () => SceneManager | undefined (lazy — created later)
cameraName: string
version: number
Methods:
setSelected, publishSelect, updateSelectedUI, refreshOutliner,
updateOutliner, getAssetType, getCharactersByUuid, isCharacterUuid.
Editor exposes the bridge as `editor.selection` and constructs it in
its constructor. Three facades stay on Editor for the external callers:
editor.publishSelect() — actions/selectObject.ts, helper.ts
editor.updateOutliner() — actions/add{Object,Character,Shape}.ts
editor.updateSelectedUI() — helper.ts
ControlPanelSceneObject reads `editor.selected` via a forwarding getter
that returns this.selection.selected.
The four near-identical "render_outliner + setOutlinerItems" snippets
that used to live in Editor.initialize / newScene / loadScene /
deleteObject collapse into one place: selection.refreshOutliner().
Dead code removed during the move:
- outliner_feature_flag was hardcoded `true` (with `// TODO REMOVE`),
making the false-branch fallback paths in updateSelectedUI /
publishSelect unreachable. Field and all `if` branches dropped.
- Editor.last_selected: passed by value to MouseControls (which
immediately overwrites it with []), and helper.ts wrote to it but
nothing read. Pure write-only dead state.
- The `return 0` on a void-returning method that triggered the
"Not all code paths return a value" typecheck error — gone with
updateSelectedUI's body.
- Unused AssetType / ClipGroup imports in editor.ts.
helper.ts updates:
- removeTransformControls drops the dead last_selected write.
- switchCameraView and deleteObject now write
editor.selection.selected (instead of editor.selected) so the
forwarding getter still surfaces the current value.
Net: editor.ts down to ~1074 LOC. Typecheck baseline drops by 1
(the void-return error is gone).
Third step of the editor.ts decomposition. Pulls TransformControls
ownership and the gizmo lifecycle out of Editor into a focused module.
GizmoController owns:
control (TransformControls).
Methods take their dependencies as method arguments — no `editor`
reference, no `import type Editor`, no circular import:
configure(camera, domElement, scene, callbacks)
changeMode(type, space)
setSpace(space)
canToggleSpace()
attach(object), detach()
addToScene(scene), removeFromScene(scene)
setVisible(v), isVisible()
Editor exposes the gizmo as `editor.gizmo` and constructs it empty in
the constructor; configure() is called from initialize() once camera
and renderer exist. The two existing public methods stay on Editor as
facades, so the external callers (actions/setTransformMode.ts,
comps/Controls3D.tsx, engine/keymap.ts) keep working unchanged:
Editor.change_mode reads the store's transformSpace and forwards.
Editor.toggleTransformSpace toggles the store and pushes into gizmo.
External readers updated:
helper.ts (SceneUtils.lockUnlockObject + removeTransformControls)
routes through editor.gizmo.attach/detach/addToScene/removeFromScene
instead of poking editor.control directly.
Editor.snapShotOfCurrentFrame uses editor.gizmo.setVisible /
isVisible instead of touching control.visible.
Editor passes editor.gizmo.control to the MouseControls constructor
where it previously passed editor.control.
Dead code removed:
- transform_interaction on Editor: passed by value to MouseControls
(which has its own internal copy that overrides) and the `= true`
write in change_mode mutated a field nothing read. Pure dead code.
- The "if (this.control == undefined) return;" guard immediately
after `new TransformControls(...)` — that branch was unreachable.
- Unused TransformControls import in editor.ts.
Net: editor.ts down to 1196 LOC. Typecheck error count drops by 3.
Second step of the editor.ts decomposition. Pulls both EffectComposer
chains (main + raw) and the post-process passes the editor actually uses
out of Editor into a dedicated subsystem.
PostProcessingPipeline owns:
composer, render_composer, outlinePass, customOutlinerPass, surfaceFinder.
Methods take their dependencies as method arguments — no `editor`
reference, no `import type Editor`, no circular import:
configureMain(renderer, scene, camera, w, h)
configureRaw(rawRenderer, scene, renderCamera, w, h)
resize(w, h)
dispose()
updateSurfaceIdAttributeToMesh(scene)
Editor exposes the pipeline as `editor.postProcessing` and constructs it
in the constructor (empty) before Scene, since Scene's load paths invoke
updateSurfaceIdAttributeToMesh as a callback. The Scene constructor now
receives a closure that dispatches into the pipeline.
ViewportController stays decoupled from composer internals: PR 1's
resizePostProcessing callback now delegates to postProcessing.resize(w, h),
which encapsulates the optional-chaining dance for composers that may not
exist yet at the first resize.
External readers updated:
helper.ts (SceneUtils.removeTransformControls) reads
editor.postProcessing.outlinePass instead of editor.outlinePass.
Editor.snapShotOfCurrentFrame and Editor.renderScene route through
editor.postProcessing.composer / .render_composer.
Editor.unmountEngine calls editor.postProcessing.dispose() instead of
the two composer.dispose() calls.
Dead code removed during the move:
- bloomPass (created but never added; the addPass call was already commented out)
- smaaPass, ssaoPass (declared but never instantiated)
- renderPass, rawRenderPass, outputPass, renderOutputPass fields (only
added to a composer once, never read again — now locals)
- setRenderDepth, setNormalMap, setOutlineRender (no callers — only
setColorMap was called from configureRaw, and it's now inlined)
- the corresponding three-of-four `Material` uniforms typecheck errors
- 9 unused three.js imports in editor.ts
(EffectComposer, RenderPass, OutlinePass, OutputPass, SMAAPass,
SAOPass, UnrealBloomPass, CustomOutlinePass, FindSurfaces, SSAOPass)
Net: editor.ts loses ~140 LOC and the post-processing concern entirely;
typecheck error baseline drops by 4 (3 dead mode setters + 1 unused
import).
First step of the editor.ts decomposition (refactor/3d-editor-cleanup).
Pulls the canvas/container DOM seam, the ResizeObserver, and the resize
cascade out of the ~1450-LOC Editor class into a standalone module.
ViewportController owns container, canvReference, canvasRenderCamReference,
the ResizeObserver, lastCanvasSize, plus the onWindowResize / observed
resize logic.
Engine fields are forwarded explicitly through the `engine` constructor
argument:
- cameras, renderer, render aspect ratio: closure-based getters that
resolve at call time
- post-processing: a single resizePostProcessing(w, h) callback rather
than three composer field getters
The single callback is deliberate. The original code creates the main
composer in _configurePostProcessing but the raw composer + custom
outline pass much later in _configurePostProcessingRaw, with the first
viewport.onWindowResize() landing between the two. Three separate
composer getters would have ViewportController peeking at fields that
don't exist yet at that first resize and silently no-op'ing via `?.`.
The callback hides that lifecycle concern inside the post-processing
module, where it belongs.
No `import type Editor`, no circular import. The dead `update*` and
`*MayReset` accessors that the original code carried as defensive
fallbacks (with self-deprecating "we should not need this" comments)
were never actually called and are dropped here. containerMayReset
stays — it has live internal callers.
Editor exposes the controller as `editor.viewport` and routes its
internal call sites through `this.viewport.<method>()`.
Net: editor.ts loses ~107 lines and one cohesive responsibility.
The last batch of PageScene-owned signals — `is3DPageMounted`,
`is3DEditorInitialized`, `is3DSceneLoaded`, and the three canvas-ref
signals (sceneContainer, editorCanvas, camViewCanvas) — now live on
the Zustand store. EngineProvider, the canvas components, the
useEditorCanvas hook, and the cross-app TopBar consumer all read from
the store instead. The helper signals in editor.ts are gone; the
`set3DPageMounted` / `setIs3DEditorInitialized` / `setIs3DSceneLoaded`
exports stay as thin wrappers around the store actions for existing
callers.
PromptBox3D and CameraSettingsModal now accept plain values + setters
instead of `Signal<T>` props. The camera/focalLengthDragging/
isPromptBoxFocused signals are gone — store is the single source of
truth. Engine code (editor/save_manager/scene) reads/writes via
`usePageSceneStore.getState()` and store actions. Demo asset signals
are now plain constants (they were never reactive).
api_manager.ts goes from 460 lines to ~135. The video-render
pipeline (uploadMediaFrameGeneration, stylizeVideo,
StylizeVideoParams, getMediaBatch, getMediaFile, uploadMedia,
gltfExport) was unreachable after videoGeneration was deleted.
Surviving public methods:
- saveSceneState (called by save_manager)
- loadSceneState (called by save_manager)
- uploadMediaSceneThumbnail (used internally by saveSceneState)
Also drops the local ArtStyle / Visibility / MediaFile types —
they only fed the deleted methods. The ArtStyle enum has a
separate copy in ~/enums/ArtStyle.ts that other code uses.
Delete two files that became unreferenced:
- engine/APIManagerResponseError.ts (duplicate of the local class
in api_manager.ts; nothing imported it)
- models/generationOptions.ts (IGenerationOptions/GenerationOptions
fed editor.generation_options, deleted last commit)
Net: -1 typecheck error, -326 lines.
Files with zero importers across the artcraft app:
- constants/RandomTexts.ts — RandomTextsPositive/Negative seeded
prompt suggestions in the deleted StylizeTab.
- composable_engines/scene_format.tsx — empty (1 line).
- constants/page.ts and the matching currentPage signal in
signals/global.ts — Pages enum + signal, neither read.
- enums/StylizeTabPages.ts and enums/AudioTabPages.ts — re-exported
via enums/index.ts but no consumer.
- models/track.ts, models/message.ts, models/voice.ts, models/tts.ts
— timeline / TTS / audio-panel types from deleted features.
- styleList.ts — used to seed selectedArtStyle's default; that
store field is gone.
- models/types.tsx had a duplicate AudioTabPages enum (separate
from the deleted one in enums/) — drop the duplicate too.
models/index.ts and enums/index.ts trimmed to match. Error count
unchanged (151) — none of these had broken type errors anyway.
Strip the stylize fields that nothing reads anymore now that the
StyleMoreOptions / StyleSelection UI is gone. From PageSceneStore:
selectedArtStyle, upscale, faceDetail, styleStrength, lipSync,
cinematic, enginePreProcessing, globalIPAMediaToken, adapterImage,
positivePrompt, negativePrompt, isUserInputPositive,
isUserInputNegative, showNegativePrompt — and the matching setters
+ resetSceneGenerationMetadata. Drop the styleList / ArtStyle
imports; both go unused here.
From editor.ts: drop negative_prompt, art_style,
engine_preprocessing, generation_options, globalIpAdapterImage.
Keep positive_prompt — PromptBox3D writes it via setEnginePrompt
(PageEditor.tsx) and save_manager round-trips it.
Trim save_manager.loadFromJson to the prompt-only path (legacy
saves with stylize keys load fine; the now-removed if-guards just
silently ignored them anyway). Trim SceneMetadata to
{ positivePrompt, cameraAspectRatio } and the matching
SceneGenereationMetaData interface.
ControlsTopButtons.handleResetScene drops its now-removed store
call.
Both component trees survived earlier cleanup but never render
from PageEditor or PageScene. They were the last UI consumers
of the stylize store fields; deleting them sets up a follow-up
that drops those store fields entirely.
Net: -17 typecheck errors (the orphan files were the source of
many missing-import errors).
cardLayout.ts, MoodboardHtmlColorPicker.tsx, findDropTarget.ts, and
CardNodeHtml.tsx were swept into the PageEnigma -> PageScene rename
commit (76add858c9) on this 3D-refactor branch. They belong on
moodboard-page-html.
The file content is preserved on the local-only moodboard-rescue
branch (pointing at 01e0e27037). To carry them back over:
git checkout moodboard-page-html
git checkout moodboard-rescue -- frontend/apps/artcraft/app/src/pages/PageMoodboard/cardLayout.ts frontend/apps/artcraft/app/src/pages/PageMoodboardHtml/MoodboardHtmlColorPicker.tsx frontend/apps/artcraft/app/src/pages/PageMoodboardHtml/interactions/findDropTarget.ts frontend/apps/artcraft/app/src/pages/PageMoodboardHtml/nodes/CardNodeHtml.tsx
git commit
The Enter Pose Mode button reached through a window.__mouseControls
global handle that MouseControls' constructor set up. With
getActiveEditor() already exposing the live editor instance for
non-React callers (used in the camera-view toggle, the keymap),
the global is unnecessary.
Drop the (window as any).__mouseControls = this assignment in
MouseControls and the matching Window augmentation in
PoseModeSelector. Replace the click handler's reach with
getActiveEditor()?.mouse_controls?.toggleFKMode() — same shape
the keymap entry already uses.
The Tab interface in this file was the type for the deleted
SidePanel/tabList.tsx tab list. Grep across app/src/ confirms no
consumer remains; AssetModal uses its own local AssetTab type.
Drop the file and the matching re-export in models/index.ts.
EditorStates is no longer referenced from editor.ts now that
PREVIEW is gone from the enum and switchPreview/switchEdit are
deleted. EngineCanvases.tsx never used useContext/EngineContext;
the imports were leftover from earlier work.
Net: -3 typecheck errors.
The mouseOverEventHandler / enableFreeCamControls /
disableFreeCamControls trio existed to track when the cursor was on
the viewport canvas and toggle FreeCam.enabled. With useFreeCam
gating itself on editorState === CAMERA_VIEW, that gate is
redundant; the only remaining consumer of selectedCanvas was
MouseControls.onkeydown's space-bar branch, which is unreachable
after the previous keyboard rewrite.
Removes the last document.addEventListener call from PageScene.
Replace MouseControls.onkeydown's giant if-else chain with a
declarative keymap:
- engine/keymap.ts: KeyBinding type + buildKeymap() returning every
one-shot viewport shortcut as one entry. Each carries label /
group metadata so a future Ctrl-hold cheatsheet overlay can read
the same array. dispatchBinding() does the modifier match
(treating Ctrl and Meta as equivalent for macOS).
- hooks/useViewportKeyboard.ts: attaches a single keydown listener
that gates on hotkeyStatus + editable-element checks, then walks
the keymap. Held movement keys (W/A/S/D etc.) live in useFreeCam
since they're continuous; this hook is for one-shots only.
The listener is on document, not the canvas, deliberately: canvas
focus is finicky and would require the user to click the viewport
before T/R/G work. The editable-element guard prevents the keymap
from firing while inputs are focused.
MouseControls.onkeydown is now unreachable (SceneManager's window
listeners are no-op since the previous commit, and useViewportKeyboard
goes straight to keymap dispatch). The class itself stays for now —
it still implements selection / FK / pointer-lock state.
The old SceneManager.attachEventListeners() attached window-level
mousemove / click / mousedown / mouseup / keydown handlers in dev
mode and dispatched them into MouseControls. Replace with two pieces:
- hooks/useViewportPointer.ts: canvas-scoped pointer listeners
that call into editor.mouse_controls. NDC pointer coords are
computed via the new engine/pointer.ts helpers (ndcFromClient,
applyNdcToVector2). Selection clicks suppress while the
free-cam owns input (CAMERA_VIEW state).
- engine/pointer.ts: pure NDC coordinate translation, no DOM
side-effects.
SceneManager.attachEventListeners / detachEventListeners are now
no-ops; kept for back-compat with the editor lifecycle so future
follow-ups can carve them out cleanly.
MouseControls itself still owns selection / FK / drag state — its
dispatch methods (onMouseMove, onMouseClick, etc.) are now invoked
by the React hook instead of window listeners. A future PR can
dissolve MouseControls further; the goal here is "no window
listeners" + "React owns lifecycle".
The 446-line FreeCam class is gone. Its responsibilities split:
- Math (move/rotate vectors, pan/zoom from inputs, lookAt) lives in
cameraMath.ts as pure functions, plus a freeCamFrameTick() that
integrates the per-frame state.
- Listener attachment + drag/key state ownership moves into
hooks/useFreeCam.ts. Listeners are scoped to the canvas (no
window/document handlers); held-key state is reset on
CAMERA_VIEW exit so a stale W keypress can't drift the camera.
- The shared FreeCamControlState struct flows through Editor via
editor.setFreeCamState(state); the render loop reads it and
calls freeCamFrameTick() each frame.
Side effects: keybinds_controls.ts now imports
isPointerLockSupported from browserChecks; helper.ts and
PageEditor's camera-select handler stop poking
.cameraViewControls and reset velocity via freeCamState instead.
PreviewEngineCamera's camera-view toggle now just flips the store
state — useFreeCam picks it up on its own (no more
enableFreeCamControls/disableFreeCamControls call site).
Pure-function helpers carved out of FreeCam in preparation for the
hook rewrite:
- engine/cameraMath.ts: HeldMoveKeys / HeldRotateKeys types,
moveVectorFromKeys, rotationVectorFromKeys, panFromDrag,
zoomFromWheel, lookAtFromCamera, lerpVelocity. No DOM access, no
signals.
- engine/browserChecks.ts: isSafariOrWebKit, hasApplePay, isSafari,
isPointerLockSupported. Unrelated to camera math; were just
hitchhiking in free_cam.ts.
free_cam.ts re-exports the browser checks for back-compat with
keybinds_controls.ts; both consumers will switch to the direct
imports when free_cam.ts is deleted in the next commit.
The Phase 6 hooks (useFreeCam, useViewportPointer, useViewportKeyboard)
need a React-shaped accessor for the canvas DOM node so their
useEffects re-run when the canvas mounts/unmounts. Promote the
existing editorCanvasSignal / camViewCanvasSignal into useEditorCanvas
/ useCamViewCanvas hooks that bridge signal -> React state via a
subscribe.
Add tabIndex={0} and outline:none to the editor canvas so it can
receive keyboard focus without showing a focus ring.
handleButtonCameraView in PreviewEngineCamera was a TODO no-op.
Flip editorState (EDIT <-> CAMERA_VIEW) via the store, then call
editor.enableFreeCamControls() / disableFreeCamControls() on the
active editor. Drop the stale console.log useEffect that fired on
every state change.
The 3D editor no longer generates videos. Remove:
- engine/video_generation.ts (the VideoGeneration class)
- comps/GenerateModals/ (modal + MyMovies/Sharing/Activity/cards)
- comps/PreviewFrameImage.tsx (PREVIEW-state-only preview overlay)
- comps/ControlsTopButtons/TestFeaturesButtons.tsx (dev-only,
rendered with debug={false})
- ~/components/signaled/TopBar/MyMoviesButton.tsx (was orphaned;
no consumer)
editor.ts: drop the VideoGeneration import, field, instantiation,
and the dead helpers it powered (useCachedMediaTokens,
stopPlaybackAndUploadVideo, switchPreview/switchEdit, generateFrame,
generateVideo, checkAndUseCache).
PageSceneStore: drop previewSrc/viewMyMovies/generateMovieId/
generateProgress fields and setters.
EditorStates: drop the PREVIEW variant; PageEditor's state machine is
now EDIT <-> CAMERA_VIEW only. PREVIEW was set only by the deleted
video_generation.ts and read by PreviewFrameImage (deleted) and a few
gates in PreviewEngineCamera + ControlPanelSceneObject (now removed).
Net: -8 typecheck errors.
sidePanelWidth, sidePanelVisible, dndSidePanelWidth and their setters
are no longer read by any rendered component. Drop them from
PageSceneStore and the lone setSidePanelWidth(340) call in
PageEditor's mount effect. PreviewFrameImage's width calc no longer
subtracts the (always-zero) panel width.
The <SidePanel /> component was never rendered from PageEditor.tsx; the
entire comps/SidePanel/, comps/SidePanelMenu/, and most of comps/SidePanelTabs/
tree was unreachable. Relocate the few files still consumed by
AssetModal (sharedComps/ItemElement{,s}.tsx, hooks/useUserObjects,
hooks/useFeaturedObjects, utilities/fetchMediaItems + misc) to
comps/AssetMenu/{shared,hooks,utilities}/, and the StyleOptions panel
used by StyleMoreOptions to its parent folder. Then delete the rest.
Net: -33 typecheck errors (the dead tree had its own type problems).
Editor/ -> engine/:
- Rename pages/PageScene/Editor/ -> pages/PageScene/engine/
- Update every "./Editor/...", "../Editor/...", "../../Editor/...",
"~/pages/PageScene/Editor/..." import path. The class name `Editor`,
`EditorState` types, and `editorState` field are unchanged -- only
the directory is renamed
- ImageTo3DExperience also gets the new path
useSignals() drops where the component reads zero .value signals:
- comps/GenerateModals/Activity.tsx
- comps/SidePanelTabs/hooks/useFeatureFlags.tsx
- comps/SidePanelTabs/sharedComps/ItemElement.tsx
- comps/SidePanelTabs/tabComps/StylizeTab/PageStyleSelection.tsx
- comps/StyleMoreOptions/StyleMoreOptions.tsx
The remaining useSignals() callers still subscribe to genuine signals
(pageHeight/pageWidth from ~/signals, outlinerIsShowing,
sceneContainerSignal, etc.) and need to stay until those signals migrate.
Final grep audit confirms no residue:
- "PageEnigma" -- 0 hits
- "from \"...Editor/...\"" import paths -- 0 hits
- Queue.publish / Queue.subscribe / fromEngineActions /
toEngineActions / toTimelineActions -- 0 hits
- TimeLine, AudioEngine, TransformEngine, EmotionEngine, LipSyncEngine,
CharacterAnimationEngine, ClipUI, TimelineState -- 0 hits
Typecheck unchanged at 144 errors in PageScene; zero new errors.
The directory was named after a stale codename. The route is
/$sceneToken, the top bar reads "Edit Scene", and the page is the main
scene-editing workspace. Renaming the directory, top-level component,
and Zustand store accordingly:
- pages/PageEnigma/ -> pages/PageScene/
- pages/PageScene/PageEnigma.tsx -> PageScene.tsx
- pages/PageScene/PageEnigmaStore.ts -> PageSceneStore.ts
- usePageEnigmaStore -> usePageSceneStore (function rename across the
whole codebase via sed)
- All ~/pages/PageEnigma/* import paths -> ~/pages/PageScene/*
- Component name PageEnigma -> PageScene in routes/_index.tsx and
routes/$sceneToken.tsx
234 files touched, all mechanical. Typecheck error count holds at 144;
zero new errors introduced by the rename.
stylizeTab.ts (deleted). 11 consumers migrated:
UI components:
- StyleOptions: cinematic / upscale / faceDetail / lipSync /
enginePreProcessing toggles drive the matching store setters; the
upscale<->cinematic mutual-exclusion logic preserved
- StyleStrength: slider + number input read styleStrength from store,
call setStyleStrength
- Prompts: textBufferPositive / textBufferNegative now positivePrompt /
negativePrompt on the store; isUserInputPositive /
isUserInputNegative / showNegativePrompt as fields with setters
- IPAdapter: adapterImage + globalIPAMediaToken from store; useShallow
subscribes to all four needed fields/setters in one selector
- StylizeTab: selectedArtStyle from store, setSelectedArtStyle replaces
the old setArtStyleSelection helper
- StyleSelection: same pattern -- selectedArtStyle and
setSelectedArtStyle
- GenerateMovieButton: snapshots the entire stylize bag from the store
with usePageEnigmaStore.getState() inside the click handler
Engine + persistence:
- SceneMetadata.getSceneGenerationMetaData reads cameraAspectRatio,
globalIPAMediaToken, upscale, faceDetail, styleStrength, lipSync,
cinematic, enginePreProcessing all from the store in one getState()
- save_manager.loadFromJson restores all stylize state through the
store's setters instead of the old restoreSceneGenerationMetadata
signal helper. globalIpAdapterImageMediaToken restoration also goes
through setGlobalIPAMediaToken
- video_generation: setGlobalIPAMediaToken on upload + reads
globalIPAMediaToken from store for the gen request payload
- ControlsTopButtons: new-scene flow calls
usePageEnigmaStore.getState().resetSceneGenerationMetadata()
Bonus cleanup:
- SidePanelTabs reverted to inlining pageHeight - 64 (the previous
edit got lost in a stash round-trip)
- IPAdapter dropped useSignals and uses useShallow for its 4-field
selector
Net effect on typecheck: 145 -> 144 errors in PageEnigma.
sizing.ts (deleted). Was already minimal -- just sidePanelWidth, the
last survivor after the timeline-related sizing exports were stripped
in Phase 2. Three consumers migrated:
- PageEditor's mount-time setup: store.setSidePanelWidth(340)
- PreviewFrameImage: pulls sidePanelWidth off the store alongside
sidePanelVisible
- SidePanel: same pattern (already reading sidePanelVisible /
dndSidePanelWidth from the store; sidePanelWidth joins them)
- StyleSelection: subscribes to sidePanelWidth via the store
signals/ down to 10 files: camera, demoAssets, dragAndDrop, engine,
objectPanel, outliner, precisionSelectorMenu, promptBox, stylizeTab.
selectedMode.ts (deleted):
- PoseModeSelector reads poseMode + showPoseControls from store, calls
setPoseMode to toggle
- keybinds_controls migrates the t/r/g hotkeys (mode change),
x (transform space toggle), 'Escape' (exits pose mode), object
selection (sets showPoseControls), and FK toggle to the store
- editor.change_mode + toggleTransformSpace read/write transformSpace
through usePageEnigmaStore.getState()
- Controls3D's mode dropdown + world/local pill read selectedMode and
transformSpace from the store
- actions/setTransformMode no longer dual-writes the legacy signal
- store's PoseMode union changed from "select" | "edit" to
"select" | "pose" to match what the engine actually checks
sidePanel.ts (deleted):
- SidePanel reads sidePanelVisible + dndSidePanelWidth from store
- TabTitle's close button calls setSidePanelVisible
- SidePanelMenu pulls sidePanelVisible + setSidePanelVisible
- PreviewFrameImage reads sidePanelVisible (was a signal, now store)
- sidePanelHeight (was a computed signal of pageHeight - 64) inlined
at its three call sites: PreviewEngineCamera, SidePanelTabs,
StyleSelection. cameraFilter has no consumers; it lives on the store
for the next round.
signals/ down to 11 leaves: camera, demoAssets, dragAndDrop, engine,
objectPanel, outliner, precisionSelectorMenu, promptBox, sizing,
stylizeTab.
hotkeys.ts (deleted):
- keybinds_controls reads usePageEnigmaStore.getState().hotkeyStatus.disabled
- ControlPanelSceneObject pulls disableHotkeyInput / enableHotkeyInput
off the store and passes them to InputVector
- PageEditor pulls the same and passes them to LoginModal +
PromptBox3D. DomLevels imported from PageEnigmaStore (replacing the
signals/hotkeys re-export)
selectedItem.ts (deleted):
- No external consumers (dead since the timeline removal).
selectedObject lives on the store; the legacy signal had no readers
outside its own file.
signals/index.ts no longer re-exports either slice. Down to 13 files
in signals/.
assetModal.ts (deleted):
- AssetMenu: setAssetModalVisibleDuringDrag(true) on open
- AssetModal: reads assetModalVisible / assetModalVisibleDuringDrag /
reopenAfterDrag from the store, writes via the matching setters.
Modal close drives setAssetModalVisible(false); the reopen-after-add
checkbox writes through setReopenAfterDrag (note: store field is
reopenAfterDrag, not the old reopenAfterDragSignal name)
- Controls3D: handleOpenModal flips both setters from the store; the
empty-scene tooltip useEffect reads assetModalVisible from the store
- keybinds_controls: 'b' shortcut opens the asset modal via the store
- DragAndDrop/DndAsset: pointerdown clears
assetModalVisibleDuringDrag, pointerup restores it from
reopenAfterDrag, click (no drag) restores it to true. All through
the store now.
signals/index.ts no longer re-exports assetModal. Four signal files
gone so far in Phase 5; signals/ now down to 13 leaves (camera,
demoAssets, dragAndDrop, engine, hotkeys, objectPanel, outliner,
precisionSelectorMenu, promptBox, selectedItem, selectedMode,
sidePanel, sizing, stylizeTab).
Three more signal files retired in favour of the Zustand store.
error.ts (deleted):
- ErrorDialog (in ~/components/signaled): reads showErrorDialog /
errorDialogTitle / errorDialogMessage from the store, calls
setShowErrorDialog
- SceneTitleInput (in TopBar): handleShowErrorDialog uses the store's
setErrorDialog action
myMovies.ts (deleted):
- MyMoviesButton (in TopBar): writes via setGenerateMovieId /
setViewMyMovies
- GenerateModals: useEffect-on-store replaces useSignalEffect; reads
generateMovieId from store
- MyMovies and Sharing: read viewMyMovies from store, close via
setViewMyMovies
- Sharing also reads generateMovieId from store
editorOverlays.ts (deleted):
- PageEditor's LoadingDots: reads editorLoader from store
- SceneContainer: reads editorLetterBox from store and passes through
to Letterbox
- PreviewEngineCamera: reads editorLetterBox + toggleEditorLetterBox
from store
- save_manager: showEditorLoader/hideEditorLoader become thin wrappers
over the store actions, keeping the same call sites
signals/index.ts no longer re-exports any of the three deleted files.
Migrates components and engine code from reading signals/engine.ts
(cameraAspectRatio, editorState, gridVisibility, previewSrc) to reading
the Zustand store.
Components migrated to usePageEnigmaStore reads:
- AspectRatioMenu (also dropped useSignals + collapsed two
duplicated lookup ladders into ICON_BY_RATIO / LABEL_BY_RATIO)
- PreviewFrameImage
- Letterbox (no longer reactive via signals; reads camAspect from store)
- StyleButtons (editorState, previewSrc, setEditorState)
- Outliner (cameraAspectRatio)
- SidePanelMenu (editorState + setEditorState)
- ControlPanelSceneObject (editorState)
- PreviewEngineCamera (cameraAspectRatio + editorState)
- PageEditor (cameraAspectRatio for getScale; the signal version still
flows into PromptBox3D as a prop)
Engine code migrated to usePageEnigmaStore.getState():
- editor.ts: snapShotOfCurrentFrame reads cameraAspectRatio + grid;
switchPreview / switchEdit set editorState
- helper.ts: switchCameraView writes editorState
- video_generation.ts: generateFrame writes previewSrc + editorState
- SceneMetadata.ts: cameraAspectRatio for snapshot metadata
- scene.ts: subscribes to store gridVisible (not the signal)
signals/engine.ts becomes a thin compatibility layer for PromptBox3D's
signal-typed props. Its setters dual-write into the store so any
PromptBox3D-driven changes (grid toggle, aspect ratio dropdown) keep
the store authoritative. editorState / previewSrc / setEditorState
removed entirely -- nothing outside the store needs them anymore.
Net effect on typecheck: 148 -> 147 errors in PageEnigma. Zero new
errors introduced.
The auto-success useEffect in UploadModalSplat / UploadModalImage /
UploadModal3D was firing on UploaderStates.success and immediately
calling onClose(), which short-circuited the UploaderModalContent
switch before the UploaderStates.success branch (UploadSuccess) could
render. The OK button on UploadSuccess was effectively dead code.
Move the success-side actions (open the gallery, fire onSuccess, close
the modal) into the UploadSuccess onOk handler. The user now sees the
"upload successful" dialog and can dismiss it intentionally; the
gallery opens at dismissal so they can drag the freshly uploaded asset
onto the canvas.
Pre-refactor the splat upload modal had two paths: the (developer-only)
local-bytes path that handed the in-memory buffer to a now-deleted
editor.timeline.addLocalSplat, and a real upload path that registered
the file in Storyteller's media library but did nothing else with it.
The local-bytes branch was the only thing letting a freshly uploaded
splat reach the canvas. With it gone, splat upload was a dead end --
the file landed in the user's media library but they had no path back
to the 3D editor.
Align with the existing image / 3D-model upload convention: on
UploaderStates.success, set galleryModalVisibleViewMode and call
onSuccess(selectedCategory). The user can then drag the freshly
uploaded splat from the auto-opened gallery onto the canvas, which
flows through PageEditor's onImageDrop handler and into
actions/addObject.
Other changes:
- onSuccess signature: (ArrayBuffer, boolean) => (FilterEngineCategories)
to match UploadModalImage / UploadModal3D
- Drop the unused onLocalBytes prop and plumbing in UploadFilesSplat;
no callsite invoked it. The two existing consumers
(PageEnigma/Controls3D, GlobalFileDropHandler) ignore the category
argument, which is fine -- () => void is assignable.
Replaces the four-hop signal-stub pattern (drop -> store-only update,
engine never told) with explicit action helpers that mutate the engine
and the store in one step.
actions/:
- addObject / addCharacter / addShape: call editor.sceneManager.create
to load the THREE.js object, then update the Zustand store. addCharacter
also stamps userData.isCharacter on the loaded object
- deleteObject: thin wrapper over editor.deleteObject (which already
goes through SceneUtils to update the store)
- selectObject / deselectObject: invoke sceneManager.select_object;
Editor.publishSelect already writes the selection to the store
- setTransformMode: drives editor.change_mode + store + the legacy
selectedMode signal during the migration window
- setCameraAspect: drives editor.changeRenderCameraAspectRatio + store +
the legacy cameraAspectRatio signal
- index.ts barrel
Wiring updates:
- PageEditor drop handler (gallery -> 3D) now calls actions/addObject
and actions/addCharacter with the editor and world position
- DragAndDrop/DndAsset takes the editor at pointerdown time and calls
actions/* on drop. ItemElement passes the editor in via EngineContext
- AspectRatioMenu calls actions/setCameraAspect; PageEditor's bottom-bar
aspect-ratio select uses the action too
- Controls3D's mode toggle (move/rotate/scale) goes through
actions/setTransformMode and the four little redundant handlers are
collapsed into one
- ImageTo3DExperience's "Open in 3D Editor" button uses getActiveEditor
to pick up the live editor across pages
Plumbing:
- New module-level activeEditor handle in EngineContext (with
getActiveEditor / setActiveEditor) so non-React callers and cross-page
actions can reach the engine; EngineProvider sets it on init and
clears on unmount
- Drop the now-orphaned signals/objectGroup, signals/characterGroups,
signals/shape entirely. Their job (store update + engine call) lives
in actions/. signals/index.ts loses the corresponding re-exports
- Remove the placeholder addLocalSplat onSuccess in Controls3D (a
developer-test path for direct splat loading; the real splat upload
goes through GlobalFileDropHandler) and delete the now-confirmed-dead
recordScene method in editor.ts
- video_generation.ts zipBlob: type as Blob | null (matches the
UploadStudioShot signature; pre-existing TS error)
Net effect on typecheck: 144 -> 147 errors in PageEnigma + experiences;
zero errors introduced in actions/, PageEnigmaStore, or DragAndDrop. All
new errors are pre-existing issues in adjacent files surfaced by the
edits, not regressions.
editor.ts surgery (1864 -> 1665 lines):
- Drop imports + field declarations + constructor instantiations for
AudioEngine, TransformEngine, EmotionEngine, LipSyncEngine,
CharacterAnimationEngine, and TimeLine; drop the
Queue/QueueNames/fromEngineActions/toEngineActions imports
- Drop timeline state fields: can_playback, playback_location,
current_frame, last_scrub. generating_preview stays (it tracks the
stylize preview render, not the timeline)
- Replace Queue.publish in changeRenderCameraAspectRatio, newScene,
loadScene, and publishSelect with direct store actions
(usePageEnigmaStore.getState().setSelectedObject / resetScene)
- Replace timeline.characters lookups (used by SceneManager outliner)
with a getCharactersByUuid helper that reads the store
- Replace timeline.isCharacter (passed into SceneManager) with an
isCharacterUuid helper that checks the store's character list
- recordScene becomes a no-op stub: timeline-driven frame capture is
gone, video generation needs to drive its own frame loop. TODO marker
for the rewiring
- generateVideo and togglePlayback drop their timeline.is_playing /
scrubber gating; renderSingleFrame drops the timeline.update branch
- Bonus: kill the meraAspectRatio typo (a pre-existing bug in the
recordScene branch I deleted)
Cascading fixes:
- save_manager.ts loadScene/loadCache: drop calls to the deleted
timeline.scrub and timeline.checkEditorCanPlay
- video_generation.ts videoProcessingPipeline: stub the
engineFrameBuffers.payloadZip path with a TODO; the video upload now
always errors via the existing handleError. The full rewiring is the
same TODO as recordScene
- Controls3D UploadModalSplat onSuccess: drop the
editor.timeline.addLocalSplat call
- SidePanelTabs: drop a long-broken import of useMouseEventsSidePanel
(the Timeline/utils path it pointed to was already deleted before
this refactor); resize handle gets a TODO marker for Phase 6
Net effect on typecheck: 162 -> 144 errors in PageEnigma. Every
remaining error in PageEnigma predates this branch; no new TypeScript
errors introduced by the refactor.
Removes references to the deleted Queue/timeline subsystem from:
- Engine helpers: scene_manager_api, video_generation, helper. Their
Queue.publish calls (UPDATE_CHARACTER_ID/ADD_OBJECT/POP_A_TOAST/
DELETE_OBJECT) now write to the new Zustand store / addToast directly.
- Editor/EditorState: drop TimelineState class and its JSON variant; trim
EditorState/EditorStateJson down to just sceneState and metadata.
- Editor/scene.ts: drop the timeline animation-clip seeding path; restore
placeholder_manager (incorrectly classified as dead in the audit).
- Signals: collapse signals/objectGroup, signals/characterGroups, and
signals/shape to non-timeline scaffolds that update the store directly
with a TODO marker for the engine-side rewiring. Drop
loadObjectData/loadCharacterData/updateCharacters/updateObjectGroup
files (all timeline-only). Strip dndTimelineHeight + overTimeline from
signals/dragAndDrop. Add setEditorState helper to signals/engine.
- Comps: drop Queue.publish calls. Aspect-ratio menu and stylize-tab
state transitions go through signal setters; TOGGLE_CAMERA_STATE,
REFRESH_PREVIEW, GENERATE_VIDEO are stubbed with TODOs pending the
editor.ts rewiring. DragComponent simplified (timeline-drop branch was
unreachable). PageEditor drops timelineHeight/dndTimelineHeight layout
math.
- PageEnigma drops the useQueueHandler call.
The branch still does not compile: editor.ts (1864 lines) is the next
target -- it instantiates TimeLine + the five timeline engines and
publishes select/aspect-ratio events through the queue. Cleaning it up
is the focused work for the next commit.
Phase 1-3 of the 3D page refactor:
- Delete the timeline + event-queue subsystem (Queue/, useQueueHandler,
Editor/timeline.ts, Editor/Engines/, clips/, and the timeline-only
signal slices: timeline, timing, audioGroup, cameraGroup,
promptTravelGroup, utils/publishClip)
- Delete dead-code files with no live importers: lipsync.js,
retargeting.ts, preview_video_generation.ts, shaders.js,
placeholder_manager.ts, both composable_engines/*.tsx stubs,
contexts/TrackContext/, comps/TimerGrid, comps/ControlsVideo
- Add PageEnigmaStore.ts with the full state shape that will replace
preact-signals (scene objects, cameras, transform mode, layout, error
overlays, generation/stylize state)
- Strip signals/index.ts of deleted slice exports and reduce
signals/sizing.ts to just sidePanelWidth
Branch does not yet compile -- subsequent commits will strip
Queue/timeline references from editor.ts, the remaining signal files,
and component consumers, then build out actions/ and event hooks.
- Pressing **Enter** in any prompt box now adds a new line instead of
submitting - fixes complaints about accidental generations
- **Shift+Enter** is the new submit shortcut
- Desktop app gets an "Enter to generate" toggle in Settings → General
(off by default) for users who prefer the old behavior
- Applies to all prompt boxes in the Artcraft desktop app and the
website
- Ctrl+Z/Cmd+Z was calling redo() instead of undo(); fixed and added
missing Ctrl+Shift+Z/Cmd+Shift+Z and Ctrl+Y redo bindings
- Wrap all async keybind handlers in try/finally so isProcessing is
always cleared even if undo/redo/copy/paste throws
- Fix === typo in disableHotkeyInput (comparison instead of assignment),
and use full value replacement so the Preact signal update is reactive
- Quick actions on gallery cards: Recreate, Share, and Download buttons
now appear on hover directly on every image/video card, so users can act
on content without opening the lightbox first.
- Media type and model badges are larger and restyled for better
readability on thumbnails.
- The library page now uses the same card component as the generation
gallery, so any future card improvements land everywhere at once.
- Library grid tightened from up to 6 columns to a cleaner max of 5 per
row.
- Removed the dotted background on the library page so skeletons and
cards sit on a solid surface.
## Artcraft website: unified lightbox, recreate, desktop CTA, prompt
fixes
**Lightbox**
- Gallery modal and `/media/:id` page now share one details component -
no more drift.
- Author sticks to the top when you scroll; `X` moved to the details
panel; "Show more" prompt toggle added to the media page.
- Shows aspect ratio, resolution, duration, and audio on/off.
**Recreate (new)**
- Button in the lightbox restores prompt, refs (image / end-frame /
video / audio), model, aspect, resolution, duration, audio toggle, and
input mode from a past generation.
- Routes to Create Image vs Create Video based on the saved model class,
not URL guessing.
- Does not auto-generate.
**Desktop CTA**
- Dismissible banner above the promptbox linking to the desktop app.
Hidden on mobile, reappears 24h after dismissal.
**Model names**
- Model selector uses `full_name` from the OmniGen API so new models
show up correctly without a code change.
- Added Seedance 2.0 Fast to the shared model list for the lightbox
details panel.
**Promptbox `@mentions`**
- Triggers correctly after Chinese, punctuation, or emoji - not just
whitespace.
- Case-insensitive: `@image1` highlights as `@Image1`.
fixes:
- prompt box no longer overlaps bottom buttons or wastes space on big
screens
- long unbroken text wraps instead of escaping the box
- resizing the window updates the layout without needing to refocus
## Website bug fixes: Seedance Fast + Characters + Toasts
### Create Video page
- Switching between Seedance and Seedance Fast no longer loses your
attached references
- Seedance Fast now supports Characters (was missing from the UI)
- Characters work in both Keyframe and Reference modes
- Fixed a bug where typing `@Bob2` could accidentally attach an older
character named `@Bob`
### Task queue notifications
- No more ghost "job complete" toasts every time you reload the site
- Cleaner wording: "Image creation complete" instead of "Image complete
— Image Generation"
- Character jobs now say "Character" instead of the generic "Task"
### Toast redesign
- Matches the prompt box style instead of the old bright green
- Success = green circle with a white checkmark, error = red circle with
a white exclamation
- Centered on mobile, stays top-right on desktop
- Message no longer awkwardly wraps onto multiple lines on mobile
Should have done this all along for ease of development.
- Gets rid of refs between data structures.
- Clones won't impact our SLA or performance at our scale, this isn't
systems programming.
getCurrentAspectRatioIcon() was reading from aspectRatioList (useState),
which is only updated on manual picker clicks, not external store
writes. Reading aspectRatio from the store directly fixes the stale
icon.
## Video reference library picker
- Added "Pick from library" option to the Video Ref slot, with gallery
filtered to videos
- Added loading state to "Use selected" button in gallery modal
- Enforced seedance-2 limits (max 3 videos / 15s total) for both upload
and library flows, with a deduped video-ref-limit toast so users see a
warning on count overflow, duration overflow, or submitting when already
at max
Two bugs were preventing @image / @video / @character references from
being tagged in prompts written with Chinese, Japanese, or Korean text:
1. The mention-highlight regex was built with just the "g" flag, so a
user-typed "@image6" never matched the canonical label "@Image6" and the
reference was rendered as plain text. Make the regex case-insensitive
and do the colorMap lookup via a lowercase-keyed table so "@image6"
still finds the color for "@Image6".
2. detectMention required the character immediately before "@" to match
/\s/, which never matches CJK characters. Typing "@" right after a
Chinese character like "从" therefore never opened the autocomplete
dropdown, leaving users to type the mention by hand (which then hit bug
1). Replace the whitespace check with a "not an ASCII identifier char"
check so CJK, quotes, and punctuation all count as valid word boundaries
before an @mention.
- **Characters for Seedance 2.0** - Character creation/selection modal,
@-mention support with colored inline highlights via contentEditable
MentionTextarea, character tokens sent in video generation API requests
- **Mobile responsive navbar** - Hamburger menu with slide-down panel,
all nav items accessible, credits/library/sign out included
- **Mobile responsive PromptBox** - Tighter padding, stacked toolbar on
small screens, flex-wrap for option buttons
- **Mobile responsive library** - Proper sticky header offset, icon-only
filter tabs on mobile
- **Task queue overhaul** - Prompt text with marquee hover, copy button,
model creator icons, detailed failure reasons on all card types
(matching desktop app)
- **Gallery improvements** - 512px thumbnails (was 250px), 12px grid
gap, model + media type badges on hover, model icon + prompt on
in-progress cards
- **Quality selector** - QualityPicker for image models
(High/Medium/Low), wired to cost estimate and generation API
- **Video batch enqueue** - Multiple videos enqueue as separate jobs so
each completes independently
- **Form consistency** - All auth pages use shared `<Input>` component,
`<form onSubmit>` for Enter key, consistent styling
- **Bug fixes** - Lightbox type error, library carousel disabled,
popover icon alignment, end frame library picker, image ref max default
(1 → 6), task queue mobile overflow, clear session button removed
- **Mobile model selector** - Model picker now appears in the PromptBox
toolbar on mobile/tablet (`lg:hidden`), since the floating bottom-left
selector is desktop-only
- **Responsive image prompt row** - Start frame and end frame sections
stack vertically on mobile with a horizontal divider, instead of
side-by-side
- **Responsive lightbox** - Stacks vertically on mobile (media top, info
below), nav arrows always visible (not hover-only), sidebar goes
full-width, darker backdrop (`bg-black/80`)
- **Safari video inline playback** - Lightbox videos use
`disablePictureInPicture`, `controlsList` restrictions, and
vendor-prefixed attributes to prevent fullscreen/PiP pop-out on Safari
When listUserMediaFiles fails (HTTP error, network error, null
reference, or any other exception), the gallery now shows the error
message with a "Copy error" button instead of silently falling through
to "No items yet".
This is a diagnostic aid for a macOS-only blank gallery bug where the
root cause is unknown (CORS, session cookie, or runtime exception) and
there is no access to production logs.
setInitialLoading(true) and setUsername(name) are batched together so
React never produces a frame where username is set but initialLoading is
still false, which would fall through to the No items yet branch before
refreshGallery has a chance to set initialLoading(true) itself.
This is a substantial number of tests to ensure we don't screw up parameters when migrating over to "omni image generation" from the distinct one-off, model-by-model endpoints.
- Remove useless forceFilterRef and its stale useEffect; use forceFilter
prop directly in JSX
- Show skeleton immediately while username is being fetched (!username
condition) instead of flashing "No items yet"
- Track username fetch failure with usernameError state and surface an
error message with a working Retry button
- Fix retry: setUsername("") was a no-op (same value → React bails out);
use usernameRetryCount in deps to actually re-trigger the fetch
- Guard error UI with allItems.length === 0 so stale items stay visible
if a retry fails mid-session
Notable behavior:
- Mixing supported and/or unsupported file types will only keep files of
file type that match the first supported file type in the list received
from browser (e.g. PNG, OBJ, EXE, JPG, OBJ order selection of files will
only show PNG, JPG for image upload)
- Removing files until only one is left will result in the sidebar
vanishing and only a single preview showing. More files can be added by
selecting multiple files again through the file selection button
- Failure to upload has been handled but not tested. It is expected any
failed uploads could be retried with a "retry failed" button. Any
individual fail can also be retried via a retry button upon hovering on
the exclamation mark by the failed file's name
- All multi-file uploads are queued async and likely wouldn't be ordered
as shown in the preview list (first-come-first-serve)
- Potentially deadlocks if a task never responds. Likely handled by
backend timeouts. This deadlock state is only partial and not lossy as
successfully uploaded files will appear in gallery even if the task is
cancelled. Cancellation can be attempted at any point during the upload.
Uploads shown in progress may be successful after cancellation depending
on the internal upload progress.
- TopBar upload images button now opens the image modal instead for
consistency and same-component use.
---------
Co-authored-by: bflatastic <bombayapirat@gmail.com>
## Summary
- **Fix blank screen crash** during video recreate flow: typing text,
adjusting time slider, typing again, then clicking the text box caused
an uncaught DOM exception in MentionTextarea's contentEditable caret
operations, which unmounted the entire React tree (no error boundaries
existed)
- **Fix text selection disappearing** on mouse release in
MentionTextarea: click handler and sync useEffect were destroying active
selections by rewriting innerHTML and restoring only a collapsed caret
- **Add React Error Boundary** around PromptBoxVideo as a safety net
## Changes
### MentionTextarea.tsx
- Wrap `getCaretOffset`, `setCaretOffset`, `scrollCaretIntoView`,
`handleInput`, sync `useEffect`, and `getOffsetRect` in try/catch to
prevent uncaught DOMExceptions
- Add null guard for `childNodes` iteration and `parentNode` access
- Skip `detectMention` in `handleClick` when user has active text
selection
- Skip innerHTML rewrite in sync useEffect when editor has selected text
### PromptBoxVideo.tsx
- Stabilize `characterNames` with `useMemo` to prevent `mentionColorMap`
from rebuilding every render
- Move empty characters array to module-level constant for stable
reference
- Add cleanup `useEffect` for `durationTimerRef` to prevent stale timer
after unmount
### New: PromptBoxErrorBoundary.tsx
- React Error Boundary with "Try again" recovery button
### ImageToVideo.tsx
- Wrap `<PromptBoxVideo>` with `<PromptBoxErrorBoundary>`
## Reproduction steps (original bug)
1. Click "Recreate" on a video
2. Type text into the prompt box
3. Adjust the time slider
4. Type text again
5. Adjust the time slider and click the text box → **UI goes blank**
## Test plan
- [x] Reproduced blank screen bug on Mac before fix
- [x] Verified fix: repeated recreation flow no longer causes blank
screen
- [x] Verified text selection persists after mouse release
- [x] Video generation with @mentions, reference images, and duration
changes works correctly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of looping within the image set, pressing Right on the last
image advances to the next set and Left on the first image goes to the
previous set, consistent with Up/Down behavior.
- was still getting dozens of pages from these two endpoints
- fixed the error chain so this is semantically correct
- improved code quality of legacy code/endpoints
On Windows production builds, convertFileSrc() generates
https://asset.localhost/... URLs. The connect-src CSP only had "asset:"
(covering asset:// on macOS/Linux) but not https://asset.localhost,
causing the fetch() in GlobalFileDropHandler to be silently blocked —
overlay appeared but no modal opened on drop.
- grouped fail message with fail reason under one tooltip for cleaner ui
- prompt / copy prompt for every type of task card
- larger thumbnails, wider task queue dropdown
- removed prompt hover on card thumbnails
- marquee css updated
- other styling cleanups
- Copy button on failed task cards copies the prompt to clipboard with a
3s check/green feedback
- Prompt preview row with marquee scroll on hover (only when text
overflows), tooltip showing full prompt up to 300 chars
- Fix dismiss and action button icon centering across all card types
(InProgress, Completed, Failed)
- Add marquee keyframe animation to tailwind.config.ts
PageEdit was never rendered in the app. Removes all PageEdit files
(~4000 lines) and migrates the shared types (BaseSelectorImage,
ImageBundle) and HistoryStack component to @storyteller/ui-pagedraw
which already owns the canonical definitions.
Storyboy, Story Girl, Knight, and News Anchor are now pinned to the
front of the Characters row in that order. AI Trump is moved from
Characters into Memes. Both are resolved client-side after the featured
items fetch by matching on lowercased item names.
<img width="1219" height="790"
alt="{A15AF77F-C555-4C85-BC15-B4B8D44D2248}"
src="https://github.com/user-attachments/assets/ec4b52a2-68c4-42b0-88fc-00ee3b322321"
/>
A "recreate" button in the media lightbox modal.
On clicked, it will prefill the image or video promptbox with the same
model, prompt and ref images that was used to generate that media the
button was clicked from.
Temporary and doesnt support seedance video/audio ref yet.
- move a lot of "deprecated" crates so we don't use them
- kill several dead crates
- rename crates to be more semantic/clear
- roll back broken sha3 version bump
Paint/Mask cursor now draws properly regardless of canvas scale and drag
offset.
Inpainting now has its own brush with add and subtract actions.
A total node array is used for ordered-rendering of nodes, which fixes
the layer forward/backward operations.
3D nodes and drawings on the canvas were missing from the reference
image sent to the model in the default generation path. The fix mirrors
the Nano Banana Pro approach: render a composite of the base image and
the draw layer, upload it via uploadSceneSnapshot, and pass it as
scene_image_media_token rather than putting editedImageToken in
image_media_tokens.
Possible problem in our Seedance 2.0 provider means we need a backup.
This is here in case we need it.
Muapi is inferior for several reasons:
- Videos contain watermarks
- Videos cost 2x as much
- Not as many aspect ratio controls
- Not as many duration controls
- No starting frame / ending frame
- No video references
- No audio references
It does seem fast, however.
Generous use of caching used for react elements. Made a pretty
significant difference too.
Minor refactor to move helper functions outside instead of within
component tree.
3D objects can now be drag-n-dropped from gallery onto 2D canvas, with
support for scale, rotation and FOV changes.
History is tracks so changes support undo/redo as well.
We're totally blind. Google Analytics helps a little bit, but we need to
know which communities find ArtCraft the most valuable. (Am I wasting my
time posting to Reddit?)
- failed tasks to show in image generation session view
- failed tasks now show in task queue dropdown
- completed and failed tasks clearable on "clear all" click
- styling updates
## Library/Gallery Modal Improvements
### Performance
- Library items are cached so reopening the modal is instant instead of
loading from scratch every time
- Only visible sections are rendered - off-screen groups are swapped out
as you scroll, keeping things smooth even with lots of media
- Username session is cached to skip a redundant API call on every open
### UX
- Replaced loading spinner with a skeleton grid placeholder
- New items from completed generations appear at the top without wiping
the library
- Library auto-refreshes when a generation finishes (if open)
- Refresh icon spins while refreshing
### UI Refinements: Topbar, Menu, and Tooltips
- **Top bar organization:** Removed the extra logo from the top bar and
added the Artcraft icon to the center text for a cleaner layout.
- **Menu interaction:** Improved the behavior of the menu icon selector
so that confusing tooltips no longer appear when the menu is temporarily
disabled (e.g., during product switching).
- **Tooltip positioning:** Fixed an issue where tooltips were
occasionally extending beyond the edges of the screen, ensuring they now
stay fully visible.
- shows credits to be deducted on generate buttons
- credits usage modal to show costs in detail (toggleable from bottom
right)
Do not merge. Awaiting backend cost calculation implementation.
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
**Lightbox & Gallery Navigation Improvements**
- Added prev/next navigation buttons to lightbox (hover-visible arrows
on left/right of image)
- Added arrow key (←/→) keyboard navigation between gallery items in
lightbox
- Gallery builds a flat ordered item list to power sequential lightbox
navigation
- Debounced prompt/batch API calls (180ms) to prevent spam when rapidly
navigating
- Stale in-flight responses are cancelled so wrong data never flashes
- Added shimmer loading skeletons for prompt, reference images, and
generation details
- Removed focus outlines on nav buttons and modal when using arrow keys
- Fixed toggleBulkSelect used-before-declaration error in gallery modal
- Added display name for flux_dev_juggernaut
- fixed the bug where long prompts overflow the lighbox modal causing
buttons to be pushed off the screen and non interactable.
- added a generic billing popup if user doesn't have a subscription plan
We need to remove irrelevant FakeYou records that do not impact the
ArtCraft product
- 87M irrelevant media file records (no relation to ArtCraft)
- 105M jobs records (which we purge occasionally anyway)
- lots of other files: audit logs, old/irrelevant stripe event logs,
feed data, etc. etc.
Two reasons for this:
- super brittle when pinned to non-main commit
e1a70095c4409478c1ded499fb8dd4afa540fb90
- I was recently stuck on a flight without good wifi and setting up a
new devenv. Cargo was totally unable to download the remote github
dependency, but was fine downloading the entire artcraft repo. Seems bad
to link to other repos that Cargo can't quickly pull on low bandwidth.
We'll need to slim this down later and eventually get back on main
branch.
- in the library/gallery modal, added a checkbox on top left of each
item
- when checkbox is checked it enters toiggle select mode which allows
multi selecting and bulk deleting objects via one delete button on the
bottom right of the modal.
- added trash can icon to lightbox delete button
New endpoints:
- checkout flow with integrated account creation (!!!)
- change password
- change email
New updates:
- Session info handler returns onboarding state
- added login / sign up and pricing flow
- download page in nav and signup and pricing links/buttons
currently missing features:
- forget password function
- google social login
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Bumps [pnpm](https://github.com/pnpm/pnpm/tree/HEAD/pnpm) from 10.24.0
to 10.27.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/pnpm/pnpm/releases">pnpm's
releases</a>.</em></p>
<blockquote>
<h2>pnpm 10.27</h2>
<h2>Minor Changes</h2>
<ul>
<li>
<p>Adding <code>trustPolicyIgnoreAfter</code> allows you to ignore trust
policy checks for packages published more than a specified time ago<a
href="https://redirect.github.com/pnpm/pnpm/issues/10352">#10352</a>.</p>
</li>
<li>
<p>Added project registry for global virtual store prune support.</p>
<p>Projects using the store are now registered via symlinks in
<code>{storeDir}/v10/projects/</code>. This enables <code>pnpm store
prune</code> to track which packages are still in use by active projects
and safely remove unused packages from the global virtual store.</p>
</li>
<li>
<p><strong>Semi-breaking.</strong> Changed the location of unscoped
packages in the virtual global store. They will now be stored under a
directory named <code>@</code> to maintain a uniform 4-level directory
depth.</p>
</li>
<li>
<p>Added mark-and-sweep garbage collection for global virtual store.</p>
<p><code>pnpm store prune</code> now removes unused packages from the
global virtual store's <code>links/</code> directory. The algorithm:</p>
<ol>
<li>Scans all registered projects for symlinks pointing to the
store</li>
<li>Walks transitive dependencies to mark reachable packages</li>
<li>Removes any package directories not marked as reachable</li>
</ol>
<p>This includes support for workspace monorepos - all
<code>node_modules</code> directories within a project (including those
in workspace packages) are scanned.</p>
</li>
</ul>
<h2>Patch Changes</h2>
<ul>
<li>Throw an error if the value of the <code>tokenHelper</code> or
<code><url>:tokenHelper</code> setting contains an environment
variable.</li>
<li>Git dependencies with build scripts should respect the
<code>dangerouslyAllowAllBuilds</code> settings <a
href="https://redirect.github.com/pnpm/pnpm/issues/10376">#10376</a>.</li>
<li>Skip the package manager check when running with --global and a
project packageManager is configured, and warn that the check is
skipped.</li>
<li><code>pnpm store prune</code> should not fail if the dlx cache
directory has files, not only directories <a
href="https://redirect.github.com/pnpm/pnpm/pull/10384">#10384</a></li>
<li>Fixed a bug (<a
href="https://redirect.github.com/pnpm/pnpm/issues/9759">#9759</a>)
where <code>pnpm add</code> would incorrectly modify a catalog entry in
<code>pnpm-workspace.yaml</code> to its exact version.</li>
</ul>
<h2>Platinum Sponsors</h2>
<!-- raw HTML omitted -->
<h2>Gold Sponsors</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/pnpm/pnpm/blob/v10.27.0/pnpm/CHANGELOG.md">pnpm's
changelog</a>.</em></p>
<blockquote>
<h2>10.27.0</h2>
<h3>Minor Changes</h3>
<ul>
<li>
<p>Adding <code>trustPolicyIgnoreAfter</code> allows you to ignore trust
policy checks for packages published more than a specified time ago<a
href="https://redirect.github.com/pnpm/pnpm/issues/10352">#10352</a>.</p>
</li>
<li>
<p>Added project registry for global virtual store prune support.</p>
<p>Projects using the store are now registered via symlinks in
<code>{storeDir}/v10/projects/</code>. This enables <code>pnpm store
prune</code> to track which packages are still in use by active projects
and safely remove unused packages from the global virtual store.</p>
</li>
<li>
<p><strong>Semi-breaking.</strong> Changed the location of unscoped
packages in the virtual global store. They will now be stored under a
directory named <code>@</code> to maintain a uniform 4-level directory
depth.</p>
</li>
<li>
<p>Added mark-and-sweep garbage collection for global virtual store.</p>
<p><code>pnpm store prune</code> now removes unused packages from the
global virtual store's <code>links/</code> directory. The algorithm:</p>
<ol>
<li>Scans all registered projects for symlinks pointing to the
store</li>
<li>Walks transitive dependencies to mark reachable packages</li>
<li>Removes any package directories not marked as reachable</li>
</ol>
<p>This includes support for workspace monorepos - all
<code>node_modules</code> directories within a project (including those
in workspace packages) are scanned.</p>
</li>
</ul>
<h3>Patch Changes</h3>
<ul>
<li>Throw an error if the value of the <code>tokenHelper</code> or
<code><url>:tokenHelper</code> setting contains an environment
variable.</li>
<li>Git dependencies with build scripts should respect the
<code>dangerouslyAllowAllBuilds</code> settings <a
href="https://redirect.github.com/pnpm/pnpm/issues/10376">#10376</a>.</li>
<li>Skip the package manager check when running with --global and a
project packageManager is configured, and warn that the check is
skipped.</li>
<li><code>pnpm store prune</code> should not fail if the dlx cache
directory has files, not only directories <a
href="https://redirect.github.com/pnpm/pnpm/pull/10384">#10384</a></li>
<li>Fixed a bug (<a
href="https://redirect.github.com/pnpm/pnpm/issues/9759">#9759</a>)
where <code>pnpm add</code> would incorrectly modify a catalog entry in
<code>pnpm-workspace.yaml</code> to its exact version.</li>
</ul>
<h2>10.26.2</h2>
<h3>Patch Changes</h3>
<ul>
<li>
<p>Improve error message when a package version exists but does not meet
the <code>minimumReleaseAge</code> constraint. The error now clearly
states that the version exists and shows a human-readable time since
release (e.g., "released 6 hours ago") <a
href="https://redirect.github.com/pnpm/pnpm/issues/10307">#10307</a>.</p>
</li>
<li>
<p>Fix installation of Git dependencies using annotated tags <a
href="https://redirect.github.com/pnpm/pnpm/issues/10335">#10335</a>.</p>
<p>Previously, pnpm would store the annotated tag object's SHA in the
lockfile instead of the actual commit SHA. This caused
<code>ERR_PNPM_GIT_CHECKOUT_FAILED</code> errors because the checked-out
commit hash didn't match the stored tag object hash.</p>
</li>
<li>
<p>Binaries of runtime engines (Node.js, Deno, Bun) are written to
<code>node_modules/.bin</code> before lifecycle scripts (install,
postinstall, prepare) are executed <a
href="https://redirect.github.com/pnpm/pnpm/issues/10244">#10244</a>.</p>
</li>
<li>
<p>Try to avoid making network calls with preferOffline <a
href="https://redirect.github.com/pnpm/pnpm/pull/10334">#10334</a>.</p>
</li>
</ul>
<h2>10.26.1</h2>
<h3>Patch Changes</h3>
<ul>
<li>Don't fail on <code>pnpm add</code>, when
<code>blockExoticSubdeps</code> is set to <code>true</code> <a
href="https://redirect.github.com/pnpm/pnpm/issues/10324">#10324</a>.</li>
<li>Always resolve git references to full commits and ensure
<code>HEAD</code> points to the commit after checkout <a
href="https://redirect.github.com/pnpm/pnpm/pull/10310">#10310</a>.</li>
</ul>
<h2>10.26.0</h2>
<h3>Minor Changes</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/pnpm/pnpm/commit/6bdba72ad31e4d6b79821405e09c6bdcc93894ee"><code>6bdba72</code></a>
chore(release): 10.27.0</li>
<li><a
href="https://github.com/pnpm/pnpm/commit/512f18817d610ae83646c4f864089710e1a28f17"><code>512f188</code></a>
fix: git dependencies respect <code>dangerouslyAllowAllBuilds</code> (<a
href="https://github.com/pnpm/pnpm/tree/HEAD/pnpm/issues/10387">#10387</a>)</li>
<li><a
href="https://github.com/pnpm/pnpm/commit/c23bdf7311cdc5763e2c9527e1b0a130448301c4"><code>c23bdf7</code></a>
feat(cli): skip package manager check when using --global option (<a
href="https://github.com/pnpm/pnpm/tree/HEAD/pnpm/issues/10368">#10368</a>)</li>
<li><a
href="https://github.com/pnpm/pnpm/commit/3f2c5f4d3958e6a150f734917eabd6ad88452e98"><code>3f2c5f4</code></a>
feat: add <code>trustPolicyIgnoreAfter</code> (<a
href="https://github.com/pnpm/pnpm/tree/HEAD/pnpm/issues/10359">#10359</a>)</li>
<li><a
href="https://github.com/pnpm/pnpm/commit/8ec7939657b78167c2753f05dacd62feabd59180"><code>8ec7939</code></a>
chore(release): 10.26.2</li>
<li><a
href="https://github.com/pnpm/pnpm/commit/4986c46b48f48ae7c5aeef2aca90820ab7b3775c"><code>4986c46</code></a>
chore(release): 10.26.1</li>
<li><a
href="https://github.com/pnpm/pnpm/commit/244e33b4e963b257952dadf192d829bfec858fb9"><code>244e33b</code></a>
chore(release): 10.26.0</li>
<li><a
href="https://github.com/pnpm/pnpm/commit/40775391d54d400c7d516bffaca667215a5e472d"><code>4077539</code></a>
fix(git-fetcher): block git dependencies from running prepare scripts
unless ...</li>
<li><a
href="https://github.com/pnpm/pnpm/commit/b0cd2dea489e28162a233cf02af3646cdd57bb8c"><code>b0cd2de</code></a>
chore(release): 10.25.0</li>
<li><a
href="https://github.com/pnpm/pnpm/commit/fa82ec393a2627fee47956b639b6ce57a4679318"><code>fa82ec3</code></a>
docs: update sponsors</li>
<li>Additional commits viewable in <a
href="https://github.com/pnpm/pnpm/commits/v10.27.0/pnpm">compare
view</a></li>
</ul>
</details>
<br />
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
<details>
<summary>Dependabot commands and options</summary>
<br />
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/storytold/artcraft/network/alerts).
</details>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Only allow free users one Nano Banana (Pro) or GPT Image at a time as
this can lead to high costs.
Users can bring their own subscription or API key, or they can subscribe
to ArtCraft to bypass this.
- refactored all markdown content as a shared library for use across
both the ArtCraft website and the app (FAQ, tutorials, news)
- added a news page to the website
- added a news & updates option to the app's bottom right help context
menu
Instructions to add and build News Posts for both the site and app is in
``NEWS_POSTS.md`` but in short:
- To add a new news post, create an .md file in
``libs/markdown-content/src/lib/content/news/`` folder
- Restart the artcraft-website dev server after adding new posts (or in
/frontend directory, run ``node scripts/generate-news-json.mjs``
manually) - this will update the public ``news.json`` file on the
website.
- This is just a temporary workaround - this way if the ArtCraft app
itself hasn't updated, it'll still be able to load the latest public
``news.json`` file from the website to show within the app.
**UX: Improve video generation validation feedback**
- Enable "Generate" button interactions to provide actionable error
toasts instead of silent disabling.
- Add inline "Starting frame required" warning with icon when a
reference image is missing.
- Update tooltip component to support conditional disabling for better
UX control.
- working red marker
- aspect ratio and resolution buttons
- styling fixes, canvas centering/padding
- nano banana not stuck at 1 image per generation anymore
- context images working for image edit
- Disabled Outline Pass
- Disabled SAO Pass
- Disabled Bloom Pass
- Disabled SMAA Pass
SAO and Outline passes are necessary to disable for adequate
performance. No idea why.
edit product ui refactor:
- nano banana model now has marker and eraser
- marker color can be changed, all brush, marker and eraser can be size
adjusted separately
- flux ones have edit region and eraser (removed select since i dont
think thats neccessary)
- smooth fade between history image switches
Note: this should not be necessary. Nx/Vite should build all of this for
us. Something in our build configuration fails to do this, which causes
us to need to build these packages.
- clicking from text to image page results now shows action buttons in
lightbox
- resizing modal from sides or corners that are not bottom or right now
doesn't move the modal once it reaches min size
- limited the min size of the lightbox modal so that the layout doesnt
break
- colors will match selected theme now
- buttons relayout and added "turn into video" button for images that
will take you to the Image to Video page
- reference image matches imapge prompt row component now - opens full
preview in a separate resizable modal
Fal is causing really inflated build slowdowns on account of the library
using so much Serde/proc macros. We really need to rethink FAL client
workings before adding it.
shared component for tutorials
app:
- tutorial modal accessible from any page on the app on the bottom right
- plays right in the modal through youtube embed
- it remembers the play progress and saves it in the zustand state so in
that current session if modal is opened again, it will continue where
the user left off
website:
- navbar updates (home and tutorials link)
- tutorial page with category buttons to filter by product or show all
- plays through youtube embed on website page
```
Run # Build the frontend app from the correct directory
Building with npx nx...
NX Failed to process project graph.
An error occurred while processing files for the @nx/eslint/plugin
plugin (Defined at nx.json#plugins[4])
.
- apps/editor3d/.eslintrc.cjs: Failed to load plugin 'storybook'
declared in '.eslintrc.cjs': Cannot find module
'eslint-plugin-storybook'
Require stack:
-
D:\a\storyteller-rust\storyteller-rust\frontend\apps\editor3d\__placeholder__.js
Referenced from:
D:\a\storyteller-rust\storyteller-rust\frontend\apps\editor3d\.eslintrc.cjs
```
- huge css changes throughout app to support theme switching
- 5 themes: light (white), gray(default), black, and gradient themes:
aurora and sunset
note:
- light theme still needs a second pass since some styling / text color
on white isn't fully updated yet there's some area that hasn't been
covered but most have been
- added 6 memes models and custom thumbnails
- upload modal now has checkbox to allow character uploads
- gallery drag and drop now works properly for characters
- make top image prompt row always show for video
- video promptbox pops a toast when reference image is empty
- description changes for ending frame section
- hid the add image button for video generation for now since that's
almost always required
- video prompt box now supports image prompt (starting and ending frame)
- shared image prompts section between image / video generation prompt
boxes
- video history / pending generation like text to image
- background change
- the history stack is commented out for now because backend doesnt
support it yet.
- reverted to previous ux
- added placeholder loaders for generating images (matched by frontend
subscriber id)
- can now generate multiple images at a time
- auto scrolls to top of history stack on enqueue
- image prompts linked to models (maximum and if it's allowed or not)
- lots of styling updates
- promptbox doesn't overflow bottom anymore (dynamic height detection)
- selected image prompts can be clicked to be previewed in a modal
<img width="938" height="472" alt="image"
src="https://github.com/user-attachments/assets/e4fc7445-0031-4adf-a6f6-dbcbe1c3ea67"
/>
- @echelon on handleEnqueue - image prompts need to be added
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
"query":"\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n jobs.attempt_count,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.product_category as `product_category: enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory`,\n jobs.inference_category as `inference_category: enums::by_table::generic_inference_jobs::inference_category::InferenceCategory`,\n jobs.maybe_model_type,\n jobs.maybe_model_token,\n jobs.maybe_raw_inference_text,\n jobs.maybe_inference_args,\n\n jobs.frontend_failure_category as `maybe_frontend_failure_category: enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory`,\n\n jobs.on_success_result_entity_type as maybe_result_entity_type,\n jobs.on_success_result_entity_token as maybe_result_entity_token,\n jobs.on_success_result_batch_token as maybe_result_batch_token,\n\n model_weights.title as maybe_model_weights_title,\n tts_models.title as maybe_tts_model_title,\n voice_conversion_models.title as maybe_voice_conversion_model_title,\n\n tts_results.public_bucket_wav_audio_path as maybe_tts_public_bucket_path,\n voice_conversion_results.public_bucket_hash as maybe_voice_conversion_public_bucket_hash,\n\n media_files.public_bucket_directory_hash as maybe_media_file_public_bucket_directory_hash,\n media_files.maybe_public_bucket_prefix as maybe_media_file_public_bucket_prefix,\n media_files.maybe_public_bucket_extension as maybe_media_file_public_bucket_extension,\n\n jobs.assigned_worker as maybe_assigned_worker,\n jobs.assigned_cluster as maybe_assigned_cluster,\n\n jobs.is_keepalive_required,\n\n jobs.created_at,\n jobs.updated_at,\n\n jobs.first_started_at as maybe_first_started_at,\n jobs.successfully_completed_at as maybe_successfully_completed_at,\n\n NOW() as database_clock\n\nFROM generic_inference_jobs as jobs\n\nLEFT OUTER JOIN model_weights ON jobs.maybe_model_token = model_weights.token\nLEFT OUTER JOIN tts_models ON jobs.maybe_model_token = tts_models.token\nLEFT OUTER JOIN voice_conversion_models ON jobs.maybe_model_token = voice_conversion_models.token\n\nLEFT OUTER JOIN tts_results ON jobs.on_success_result_entity_token = tts_results.token\nLEFT OUTER JOIN voice_conversion_results ON jobs.on_success_result_entity_token = voice_conversion_results.token\nLEFT OUTER JOIN media_files ON jobs.on_success_result_entity_token = media_files.token\n\nWHERE jobs.token = ?\n ",
"query":"\n SELECT\n token as `token: tokens::tokens::wallets::WalletToken`\n FROM wallets\n WHERE owner_user_token = ?\n AND wallet_namespace = ?\n ORDER BY id ASC\n LIMIT 1\n ",
"query":"\nSELECT\n u.id as `id: u64`,\n u.token as `token: tokens::tokens::users::UserToken`,\n u.username,\n u.display_name,\n u.username_is_not_customized as `username_is_not_customized: bool`,\n u.email_address,\n u.email_confirmed as `email_confirmed: bool`,\n u.is_without_password as `is_without_password: bool`,\n u.ip_address_creation,\n u.maybe_source as `maybe_source: enums::by_table::users::user_signup_source::UserSignupSource`,\n u.maybe_signup_method as `maybe_signup_method: enums::by_table::users::user_signup_method::UserSignupMethod`,\n u.created_at,\n u.maybe_referral_url,\n u.is_temporary as `is_temporary: bool`\nFROM users AS u\nWHERE\n u.id < ?\n AND u.user_deleted_at IS NULL\n AND u.mod_deleted_at IS NULL\nORDER BY u.id DESC\nLIMIT ?\n ",
"query":"\n UPDATE tasks\n SET task_status = \"cancelled_by_user\"\n WHERE task_status NOT IN (\n \"complete_success\",\n \"complete_failure\",\n \"dead\",\n \"cancelled_by_user\",\n \"cancelled_by_provider\",\n \"cancelled_by_us\"\n )\n ",
"query":"\nSELECT\n user_subscriptions.subscription_namespace as `subscription_namespace: enums::common::payments_namespace::PaymentsNamespace`,\n user_subscriptions.maybe_stripe_subscription_id,\n users.token as `user_token: tokens::tokens::users::UserToken`,\n users.email_address,\n users.username,\n users.display_name\nFROM user_subscriptions\nINNER JOIN users ON users.token = user_subscriptions.user_token\nWHERE\n user_subscriptions.maybe_stripe_customer_id = ?\n AND user_subscriptions.deleted_at IS NULL\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\nORDER BY user_subscriptions.created_at DESC\n ",
"query":"\nSELECT\n w.token as `token: tokens::tokens::wallets::WalletToken`,\n w.wallet_namespace as `wallet_namespace: enums::common::payments_namespace::PaymentsNamespace`,\n w.owner_user_token as `owner_user_token: tokens::tokens::users::UserToken`,\n w.banked_credits as `banked_credits: u32`,\n w.monthly_credits as `monthly_credits: u32`,\n w.version,\n w.created_at as `created_at: DateTime<Utc>`,\n w.updated_at as `updated_at: DateTime<Utc>`\nFROM wallets as w\nWHERE w.token = ?\nLIMIT 1\n ",
"query":"\nSELECT\n user_token,\n subscription_namespace,\n subscription_product_slug,\n subscription_expires_at\n\nFROM user_subscriptions\n\nWHERE\n user_token = ?\n AND subscription_expires_at > CURRENT_TIMESTAMP\n ORDER BY id ASC\n ",
"query":"\nSELECT\n p.token as `token: tokens::tokens::prompts::PromptToken`,\n\n p.prompt_type as `prompt_type: enums::by_table::prompts::prompt_type::PromptType`,\n\n p.maybe_model_type as `maybe_model_type: enums::common::generation::common_model_type::CommonModelType`,\n p.maybe_generation_provider as `maybe_generation_provider: enums::common::generation_provider::GenerationProvider`,\n\n p.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n\n p.maybe_positive_prompt,\n p.maybe_negative_prompt,\n\n p.maybe_generation_mode as `maybe_generation_mode: enums::common::generation::common_generation_mode::CommonGenerationMode`,\n p.maybe_aspect_ratio as `maybe_aspect_ratio: enums::common::generation::common_aspect_ratio::CommonAspectRatio`,\n p.maybe_resolution as `maybe_resolution: enums::common::generation::common_resolution::CommonResolution`,\n p.maybe_batch_count as `maybe_batch_count: u8`,\n p.maybe_generate_audio as `maybe_generate_audio: bool`,\n p.maybe_duration_seconds as `maybe_duration_seconds: u32`,\n\n p.maybe_other_args,\n\n p.creator_ip_address,\n p.created_at\n\nFROM prompts as p\nWHERE\n p.token = ?\n ",
"query":"\nSELECT\n token as `token: tokens::tokens::user_subscriptions::UserSubscriptionToken`,\n user_token as `user_token: tokens::tokens::users::UserToken`,\n subscription_namespace as `subscription_namespace: enums::common::payments_namespace::PaymentsNamespace`,\n subscription_product_slug,\n maybe_stripe_subscription_status,\n maybe_stripe_customer_id,\n maybe_stripe_product_id,\n maybe_stripe_subscription_id\n\nFROM user_subscriptions\n\nWHERE\n user_token = ?\n AND subscription_namespace = ?\n AND maybe_stripe_customer_id IS NOT NULL\n AND maybe_stripe_subscription_id IS NOT NULL\n AND maybe_stripe_subscription_status IS NOT NULL\n AND maybe_stripe_subscription_status NOT IN ('canceled', 'incomplete_expired')\n AND subscription_expires_at > CURRENT_TIMESTAMP\n ORDER BY id ASC\n LIMIT 1\n ",
"query":"\nSELECT\n token as `token: tokens::tokens::user_subscriptions::UserSubscriptionToken`,\n user_token as `user_token: tokens::tokens::users::UserToken`,\n subscription_namespace as `subscription_namespace: enums::common::payments_namespace::PaymentsNamespace`,\n subscription_product_slug,\n maybe_stripe_subscription_status,\n maybe_stripe_customer_id,\n maybe_stripe_product_id,\n maybe_stripe_subscription_id\n\nFROM user_subscriptions\n\nWHERE\n user_token = ?\n AND subscription_namespace = ?\n AND maybe_stripe_customer_id IS NOT NULL\n AND maybe_stripe_subscription_id IS NOT NULL\n AND maybe_stripe_subscription_status IS NOT NULL\n ORDER BY id ASC\n LIMIT 1\n ",
"query":"\nSELECT\n user_token,\n subscription_namespace as `subscription_namespace: enums::common::payments_namespace::PaymentsNamespace`,\n subscription_product_slug,\n subscription_expires_at\n\nFROM user_subscriptions\n\nWHERE\n user_token = ?\n AND subscription_expires_at > CURRENT_TIMESTAMP\n ORDER BY id ASC\n ",
"query":"\nSELECT\n w.token as `token: tokens::tokens::wallets::WalletToken`,\n w.wallet_namespace as `wallet_namespace: enums::common::payments_namespace::PaymentsNamespace`,\n w.banked_credits as `banked_credits: u32`,\n w.monthly_credits as `monthly_credits: u32`,\n w.version,\n w.created_at as `created_at: DateTime<Utc>`,\n w.updated_at as `updated_at: DateTime<Utc>`\nFROM wallets as w\nWHERE w.owner_user_token = ?\nORDER BY w.id DESC\n ",
"query":"\nSELECT\n user_token as `user_token: tokens::tokens::users::UserToken`,\n payments_namespace as `payments_namespace: enums::common::payments_namespace::PaymentsNamespace`,\n stripe_customer_id\n\nFROM user_stripe_customer_links\n\nWHERE\n user_token = ?\n AND payments_namespace = ?\n ORDER BY id ASC\n LIMIT 1\n ",
"query":"\nSELECT\n token as `token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n wallet_token as `wallet_token: tokens::tokens::wallets::WalletToken`,\n entry_type as `entry_type: enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType`,\n maybe_entity_ref,\n credits_delta,\n banked_credits_before as `banked_credits_before: u32`,\n banked_credits_after as `banked_credits_after: u32`,\n monthly_credits_before as `monthly_credits_before: u32`,\n monthly_credits_after as `monthly_credits_after: u32`,\n created_at as `created_at: DateTime<Utc>`,\n is_refunded as `is_refunded: bool`,\n maybe_linked_refund_ledger_token as `maybe_linked_refund_ledger_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`\nFROM wallet_ledger_entries\nWHERE wallet_token = ?\nORDER BY id DESC\n ",
"query":"\nSELECT\n u.id as `id: u64`,\n u.token as `token: tokens::tokens::users::UserToken`,\n u.username,\n u.display_name,\n u.username_is_not_customized as `username_is_not_customized: bool`,\n u.email_address,\n u.email_confirmed as `email_confirmed: bool`,\n u.is_without_password as `is_without_password: bool`,\n u.ip_address_creation,\n u.maybe_source as `maybe_source: enums::by_table::users::user_signup_source::UserSignupSource`,\n u.maybe_signup_method as `maybe_signup_method: enums::by_table::users::user_signup_method::UserSignupMethod`,\n u.created_at,\n u.maybe_referral_url,\n u.is_temporary as `is_temporary: bool`,\n us.subscription_namespace,\n us.subscription_product_slug,\n us.maybe_stripe_subscription_id,\n us.maybe_stripe_customer_id,\n us.maybe_stripe_product_id,\n us.maybe_stripe_price_id,\n us.maybe_stripe_subscription_status,\n us.maybe_stripe_recurring_interval,\n us.maybe_stripe_invoice_is_paid as `maybe_stripe_invoice_is_paid: bool`,\n us.maybe_cancel_at,\n us.maybe_canceled_at\nFROM users AS u\nINNER JOIN user_subscriptions AS us ON u.token = us.user_token\nWHERE\n u.id < ?\n AND u.user_deleted_at IS NULL\n AND u.mod_deleted_at IS NULL\nORDER BY u.id DESC\nLIMIT ?\n ",
"query":"\nSELECT\n id as `id: u64`,\n token as `token: tokens::tokens::characters::CharacterToken`,\n character_type as `character_type: enums::by_table::characters::character_type::CharacterType`,\n is_active as `is_active: bool`,\n character_name,\n maybe_description,\n maybe_avatar_media_token as `maybe_avatar_media_token: tokens::tokens::media_files::MediaFileToken`,\n maybe_full_image_media_token as `maybe_full_image_media_token: tokens::tokens::media_files::MediaFileToken`,\n maybe_original_upload_media_token as `maybe_original_upload_media_token: tokens::tokens::media_files::MediaFileToken`,\n maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n kinovi_character_id,\n kinovi_character_name,\n maybe_kinovi_asset_id\nFROM characters\nWHERE maybe_creator_user_token = ?\n AND is_active = true\n AND deleted_at IS NULL\n AND id < ?\nORDER BY id DESC\nLIMIT ?\n ",
"query":"\nSELECT\n users.token as `user_token: tokens::tokens::users::UserToken`,\n username,\n display_name,\n username_is_generated as `username_is_generated: bool`,\n is_temporary as `is_temporary: bool`,\n username_is_not_customized as `username_is_not_customized: bool`,\n email_address,\n email_confirmed as `email_confirmed: bool`,\n email_is_synthetic as `email_is_synthetic: bool`,\n is_without_password as `is_without_password: bool`,\n ip_address_creation,\n ip_address_last_login,\n maybe_avatar_media_file_token as `maybe_avatar_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n email_gravatar_hash,\n is_banned as `is_banned: bool`,\n created_at,\n updated_at\nFROM users\nWHERE\n users.username = ?\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\nLIMIT 1\n ",
"query":"\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `kinovi_character_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.job_type = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"query":"\nSELECT\n token as `token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n entry_type as `entry_type: enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType`,\n maybe_entity_ref,\n credits_delta,\n banked_credits_before as `banked_credits_before: u32`,\n banked_credits_after as `banked_credits_after: u32`,\n monthly_credits_before as `monthly_credits_before: u32`,\n monthly_credits_after as `monthly_credits_after: u32`,\n is_refunded as `is_refunded: bool`,\n maybe_linked_refund_ledger_token as `maybe_linked_refund_ledger_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n created_at as `created_at: DateTime<Utc>`\nFROM wallet_ledger_entries\nWHERE token = ?\nLIMIT 1\n ",
"query":"\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `order_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.job_type = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"query":"\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n\n jobs.status as `status: enums::common::job_status_plus::JobStatusPlus`,\n jobs.attempt_count,\n\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n\n jobs.product_category as `product_category: enums::by_table::generic_inference_jobs::inference_job_product_category::InferenceJobProductCategory`,\n jobs.inference_category as `inference_category: enums::by_table::generic_inference_jobs::inference_category::InferenceCategory`,\n jobs.maybe_model_type,\n jobs.maybe_model_token,\n jobs.maybe_raw_inference_text,\n jobs.maybe_inference_args,\n\n jobs.frontend_failure_category as `maybe_frontend_failure_category: enums::by_table::generic_inference_jobs::frontend_failure_category::FrontendFailureCategory`,\n jobs.failure_reason,\n\n jobs.on_success_result_entity_type as maybe_result_entity_type,\n jobs.on_success_result_entity_token as maybe_result_entity_token,\n jobs.on_success_result_batch_token as maybe_result_batch_token,\n\n model_weights.title as maybe_model_weights_title,\n tts_models.title as maybe_tts_model_title,\n voice_conversion_models.title as maybe_voice_conversion_model_title,\n\n tts_results.public_bucket_wav_audio_path as maybe_tts_public_bucket_path,\n voice_conversion_results.public_bucket_hash as maybe_voice_conversion_public_bucket_hash,\n\n media_files.public_bucket_directory_hash as maybe_media_file_public_bucket_directory_hash,\n media_files.maybe_public_bucket_prefix as maybe_media_file_public_bucket_prefix,\n media_files.maybe_public_bucket_extension as maybe_media_file_public_bucket_extension,\n\n jobs.assigned_worker as maybe_assigned_worker,\n jobs.assigned_cluster as maybe_assigned_cluster,\n\n jobs.is_keepalive_required,\n\n jobs.created_at,\n jobs.updated_at,\n\n jobs.first_started_at as maybe_first_started_at,\n jobs.successfully_completed_at as maybe_successfully_completed_at,\n\n NOW() as database_clock\n\nFROM generic_inference_jobs as jobs\n\nLEFT OUTER JOIN model_weights ON jobs.maybe_model_token = model_weights.token\nLEFT OUTER JOIN tts_models ON jobs.maybe_model_token = tts_models.token\nLEFT OUTER JOIN voice_conversion_models ON jobs.maybe_model_token = voice_conversion_models.token\n\nLEFT OUTER JOIN tts_results ON jobs.on_success_result_entity_token = tts_results.token\nLEFT OUTER JOIN voice_conversion_results ON jobs.on_success_result_entity_token = voice_conversion_results.token\nLEFT OUTER JOIN media_files ON jobs.on_success_result_entity_token = media_files.token\n\nWHERE jobs.token = ?\n ",
"query":"\n SELECT\n token as `token: tokens::tokens::wallets::WalletToken`,\n wallet_namespace as `namespace: enums::common::payments_namespace::PaymentsNamespace`,\n banked_credits,\n monthly_credits\n FROM wallets\n WHERE owner_user_token = ?\n AND wallet_namespace = ?\n ORDER BY id ASC\n LIMIT 1\n ",
"query":"\nSELECT\n username,\n display_name,\n username_is_generated as `username_is_generated: bool`,\n username_is_not_customized as `username_is_not_customized: bool`\nFROM users\nWHERE\n token = ?\n AND user_deleted_at IS NULL\n AND mod_deleted_at IS NULL\nLIMIT 1\n ",
"query":"\nSELECT\n j.status as `job_status: enums::common::job_status_plus::JobStatusPlus`,\n j.failure_reason as job_failure_reason,\n wle.credits_delta,\n wle.maybe_linked_refund_ledger_token as `maybe_linked_refund_ledger_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n j.on_success_result_entity_token as `on_success_result_media_token: tokens::tokens::media_files::MediaFileToken`,\n j.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n wle.token as `wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`,\n wle.entry_type as `wallet_ledger_entry_type: enums::by_table::wallet_ledger_entries::wallet_ledger_entry_type::WalletLedgerEntryType`,\n j.maybe_external_third_party as `maybe_external_third_party: enums::by_table::generic_inference_jobs::inference_job_external_third_party::InferenceJobExternalThirdParty`,\n j.maybe_external_third_party_id,\n j.created_at,\n j.updated_at\nFROM generic_inference_jobs as j\nLEFT OUTER JOIN users as u\n ON j.maybe_creator_user_token = u.token\nLEFT OUTER JOIN wallet_ledger_entries as wle\n ON wle.maybe_entity_ref = j.token\nWHERE u.token = ?\nORDER BY j.id DESC\n ",
"query":"\nSELECT\n users.token as `user_token: tokens::tokens::users::UserToken`,\n username,\n display_name,\n username_is_generated as `username_is_generated: bool`,\n is_temporary as `is_temporary: bool`,\n username_is_not_customized as `username_is_not_customized: bool`,\n email_address,\n email_confirmed as `email_confirmed: bool`,\n email_is_synthetic as `email_is_synthetic: bool`,\n is_without_password as `is_without_password: bool`,\n ip_address_creation,\n ip_address_last_login,\n maybe_avatar_media_file_token as `maybe_avatar_media_file_token: tokens::tokens::media_files::MediaFileToken`,\n email_gravatar_hash,\n is_banned as `is_banned: bool`,\n created_at,\n updated_at\nFROM users\nWHERE\n users.email_address = ?\n AND users.user_deleted_at IS NULL\n AND users.mod_deleted_at IS NULL\nLIMIT 1\n ",
"query":"\nSELECT\n jobs.token as `job_token: tokens::tokens::generic_inference_jobs::InferenceJobToken`,\n jobs.maybe_external_third_party_id as `operation_id`,\n jobs.maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n jobs.maybe_creator_anonymous_visitor_token as `maybe_creator_anonymous_visitor_token: tokens::tokens::anonymous_visitor_tracking::AnonymousVisitorTrackingToken`,\n jobs.creator_ip_address,\n jobs.creator_set_visibility as `creator_set_visibility: enums::common::visibility::Visibility`,\n jobs.maybe_prompt_token as `maybe_prompt_token: tokens::tokens::prompts::PromptToken`,\n jobs.maybe_wallet_ledger_entry_token as `maybe_wallet_ledger_entry_token: tokens::tokens::wallet_ledger_entries::WalletLedgerEntryToken`\n\nFROM generic_inference_jobs as jobs\n\nWHERE jobs.maybe_external_third_party = ?\n AND jobs.status NOT IN ('complete_success', 'complete_failure')\n AND jobs.maybe_external_third_party_id IS NOT NULL\n\nLIMIT 25000\n ",
"query":"\nSELECT\n id,\n token as `token: MediaFileToken`,\n created_at as `created_at: DateTime<Utc>`,\n maybe_thumbnail_version as `maybe_thumbnail_version: u8`,\n public_bucket_directory_hash,\n maybe_public_bucket_prefix,\n maybe_public_bucket_extension\nFROM media_files\nWHERE\n id >= (SELECT COALESCE(MIN(id), 0) FROM media_files WHERE created_at >= NOW() - INTERVAL ? HOUR)\n AND id < ?\n AND media_class = ?\n AND maybe_thumbnail_version IS NULL\n AND user_deleted_at IS NULL\n AND mod_deleted_at IS NULL\nORDER BY id DESC\nLIMIT ?\n ",
"query":"\nSELECT\n token as `token: tokens::tokens::user_subscriptions::UserSubscriptionToken`,\n user_token as `user_token: tokens::tokens::users::UserToken`,\n subscription_namespace as `subscription_namespace: enums::common::payments_namespace::PaymentsNamespace`,\n subscription_product_slug,\n maybe_stripe_customer_id,\n maybe_stripe_product_id,\n maybe_stripe_subscription_id,\n maybe_stripe_subscription_status as `maybe_stripe_subscription_status: enums::common::stripe_subscription_status::StripeSubscriptionStatus`,\n maybe_stripe_invoice_is_paid,\n subscription_start_at,\n subscription_expires_at,\n current_billing_period_end_at,\n maybe_cancel_at,\n maybe_canceled_at\n\nFROM user_subscriptions\n\nWHERE\n user_token = ?\n AND subscription_namespace = ?\n AND maybe_stripe_customer_id IS NOT NULL\n AND maybe_stripe_subscription_id IS NOT NULL\n AND maybe_stripe_subscription_status IS NOT NULL\n AND maybe_stripe_subscription_status NOT IN ('canceled', 'incomplete_expired')\n AND subscription_expires_at > CURRENT_TIMESTAMP\n ORDER BY id ASC\n LIMIT 1\n ",
"query":"\nSELECT\n uir.id,\n uir.impersonator_user_token as `impersonator_user_token: UserToken`,\n impersonator.username as impersonator_username,\n impersonator.display_name as impersonator_display_name,\n uir.impersonated_user_token as `impersonated_user_token: UserToken`,\n impersonated.username as impersonated_username,\n impersonated.display_name as impersonated_display_name,\n uir.is_redeemed as `is_redeemed: bool`,\n (uir.expires_at < NOW()) as `is_expired: bool`,\n uir.expires_at,\n uir.created_at,\n uir.updated_at\nFROM user_impersonation_requests uir\nJOIN users impersonator ON impersonator.token = uir.impersonator_user_token\nJOIN users impersonated ON impersonated.token = uir.impersonated_user_token\nORDER BY uir.id DESC\nLIMIT ?\n ",
"query":"\nSELECT\n token as `token: tokens::tokens::characters::CharacterToken`,\n character_type as `character_type: enums::by_table::characters::character_type::CharacterType`,\n is_active as `is_active: bool`,\n character_name,\n maybe_description,\n maybe_avatar_media_token as `maybe_avatar_media_token: tokens::tokens::media_files::MediaFileToken`,\n maybe_full_image_media_token as `maybe_full_image_media_token: tokens::tokens::media_files::MediaFileToken`,\n maybe_original_upload_media_token as `maybe_original_upload_media_token: tokens::tokens::media_files::MediaFileToken`,\n maybe_creator_user_token as `maybe_creator_user_token: tokens::tokens::users::UserToken`,\n kinovi_character_id,\n kinovi_character_name,\n maybe_kinovi_asset_id\nFROM characters\nWHERE token = ?\n AND deleted_at IS NULL\n ",
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.