Closes the last UI-layer direct ColorAction construction. Same shape
as the toggleObjectLock / toggleObjectVisibility / transformObject
helpers — the view passes editor + uuid + new value; the helper
reads the before-state from the live engine, applies the change, and
records the undo entry.
Bonus correctness: before-state now comes from obj.userData.color
instead of the panel's local React `color` state. The two diverge if
the user undoes a color change (engine reverts but the local React
ref doesn't), so the engine-read shape produces the right diff
entries even when the panel state is stale.
Outliner.tsx imported LockAction and VisibilityAction directly and
mixed engine + Zustand + history calls in its click handlers,
breaking the layering convention the rest of actions/ follows
(addObject, transformObject, etc. — UI calls one helper; the helper
owns engine + store + history orchestration).
actions/toggleObjectLock and actions/toggleObjectVisibility now wrap
the engine action class + store toggle + history record in one
function. Each reads the before-state from the live engine so the
view doesn't have to plumb it.
Outliner.tsx drops the engine-action imports; handlers shrink to a
single call.
Color picker recording in ControlPanelSceneObject still constructs
ColorAction directly. Same pattern applies (actions/setObjectColor)
— easy follow-up if full layering symmetry is wanted.
The .tsx file shouldn't construct UndoableAction instances or call
HistoryManager directly — those are engine-level concerns that other
action helpers (addObject, addCharacter, addShape) already keep behind
the actions/ layer.
actions/transformObject.ts exposes beginTransformSession(editor, uuid)
which returns a session handle with one method, commit(). Open the
session on first edit, call commit() on selection change / panel
unmount. The session:
- snapshots the before-state in the constructor (via TransformAction)
- on commit, captures after-state and pushes a single undo entry only
if the transform actually changed
- always re-syncs the Zustand objectPanel slice from the engine on
commit so currentSceneObject reflects what the user just committed
ControlPanelSceneObject swaps its TransformAction import + manual
record/commit logic for the session helper. The view only owns the
"when does a session begin/end" boundary now; the action layer owns
the recording + store sync.
Closes the last undo/redo feature-parity gap vs. main. Main captured
panel-input transforms via the mouseup-diff in check_scene_for_updates
(only worked when the user happened to fire a subsequent mouseup);
this branch had no equivalent until now.
Implementation:
- A useRef holds a pending TransformAction. beginPanelTransform()
constructs one on the first detected change for the currently-shown
object — the constructor captures the before-state of the live
scene object before the inputsUpdated useEffect dispatches the
engine write.
- All three change handlers (position / rotation / scale, including
the uniform-scale path) call beginPanelTransform() when an actual
diff is detected.
- The selection-change useEffect's cleanup function runs
commitPanelTransform() — captures the after-state and pushes the
entry only if the transform actually changed. This fires both on
selection switch (panel reloads for a new object) and on unmount.
The TransformAction stashes the uuid at construction, so commit
correctly targets the original object even if the panel has already
reloaded for a new selection by the time cleanup runs.
Resolves the longstanding `// TODO Refactor remove editor.` Drops
the Editor import; constructor takes a SaveManagerDeps surface
inline-built by Editor (Phase 2 idiom — same shape as
CameraControllerDeps, GizmoControllerDeps, etc.).
Deps surface (10 fields, single-purpose):
- getVersion / setVersion — engine version (load may upgrade)
- getActiveScene — proxy serialization target
- getRenderer — for save thumbnail
- removeTransformControls — pre-save cleanup
- getCamera / refreshCamObj / changeRenderCameraAspectRatio — camera
- setPositivePrompt — restore prompt on load
- saveSceneState / loadSceneState — backend API
current_scene_media_token moves into SaveManager as private state
(only the load path used it; nothing outside SaveManager read it).
The field on Editor is dropped along with the two `= null` writes
in the constructor and initialize.
CameraController gains refreshCamObj(scene) — re-anchors cam_obj to
the named entity in the scene + sets layers. Replaces the inline
post-load setup that SaveManager used to do directly. Editor.initialize
also routes through the same method now (was inline at line ~410).
Pure removals, no behavior change. Sets up the SaveManager Deps
refactor in the next commit.
SaveManager:
- Drop unused saveData (the live caller is getSceneJson + saveScene
which goes through saveSceneState directly).
- Drop unused computeSceneChecksum + checkSumData + the
computeHashForBrowser helper. Zero callers anywhere.
- Drop the editor.generating_preview = true/false bracket inside
saveScene. The field has no readers — all the noise about "stop
control panel from flipping out" is dead with VICCCCCCCCCCCCCCCTORRRRRRRR.
Editor:
- Drop generating_preview field (no readers anywhere).
- Drop current_scene_glb_media_token field + the two writes (no
readers anywhere; suspected leftover of an older save format).
Shapes redo'd to broken / invisible objects because the snapshot
captured obj.name = "Cube" (the display label addShape sets after
creation), but scene.instantiate's geometry switch only matches the
geometry key ("Box", "Sphere", "PointLight", ...). Calling
instantiate("Cube", pos) fell through with no matching geometry.
Fix: addShape stashes the geometry key in userData.shapeKey before
overriding obj.name. recreateFromSnapshot picks shapeKey when
present (falls back to snap.name for non-shape paths), then restores
obj.name = snap.name after the create returns so the display label
sticks. Non-shape recreations are unaffected — their userData has no
shapeKey, so the fallback to snap.name keeps the existing flow.
After the migration to LockAction, recording is the call site's
responsibility (Outliner.tsx pushes a LockAction directly). Drop
the recordLockChange dep callback and the bridge's auto-record
inside lockUnlockObject — the bridge stays history-agnostic.
Every recording site now constructs an action class and passes it to
record(). HistoryManager loses all per-kind methods, ctx/HistoryContext,
HistoryEntry, the wrap helper, the legacy entry factories, and the
pendingTransform field — down to a generic stack with one external
method (record) plus undo/redo.
editor.ts shrinks dramatically: the ~70-line inline HistoryContext
deps blob is gone, replaced by `new HistoryManager({ capacity: 64 })`
on its own. Gizmo onDraggingChanged now stores a TransformAction
between dragstart and dragend; commits and records via record(...) at
dragend. SelectionBridgeDeps drops recordLockChange — lock recording
moves to the Outliner call site via new LockAction(...).
Migration map:
- actions/addObject, addCharacter, addShape:
history.recordCreate(obj) → history.record(new CreateAction(editor, obj))
- editor.deleteObject:
history.recordDelete(obj) → history.record(new DeleteAction(editor, obj))
- editor.ts gizmo onDraggingChanged:
begin/endTransform → activeTransform: TransformAction with commit()
- keymap.ts paste:
history.recordCreate(obj) → history.record(new CreateAction(editor, obj))
- comps/Outliner/Outliner.tsx:
lock — new LockAction(selection, uuid, before, after)
visibility — new VisibilityAction(editor, uuid, before, after)
- ControlPanelSceneObject color picker:
history.recordSetColor(...) → history.record(new ColorAction(editor, uuid, before, after))
LockAction and VisibilityAction self-refresh the outliner after
apply/revert, matching the pattern CreateAction/DeleteAction already
use. HistoryManager has no global onReplay refresh hook — actions
that need UI sync handle it themselves.
The "adding a new action" test: a future "duplicate object" action is
just a new file under engine/editor/actions/DuplicateAction.ts
implementing UndoableAction. Zero edits to HistoryManager, zero edits
to editor.ts.
Six self-contained classes under engine/editor/actions/, one per
action kind. Each implements UndoableAction, captures its own state
in the constructor, and runs apply/revert against subsystem refs
held privately. Adding a new kind = a new file here, no edits
elsewhere.
- CreateAction: snapshots an obj, recreates on apply (redo),
removes via utils.deleteObject on revert (undo). Uses the
non-recording delete path so undo of create can't recurse.
- DeleteAction: mirror — removes on apply, recreates on revert.
- TransformAction: encapsulates begin/end transaction state. Caller
constructs at dragstart (captures before), calls commit() at
dragend to capture after + check for a no-op move. records only
if commit() returns true.
- ColorAction: simple before/after pair, routes through
activeScene.setColor.
- LockAction: routes through SelectionBridge.setLockState so the
gizmo attach/detach side effects fire on undo.
- VisibilityAction: writes obj.visible + userData.visible directly.
No call sites use these yet — that's the next commit. HistoryManager
is unchanged; it already accepts UndoableAction via record().
The user critique on the original HistoryManager design was sharp:
adding a new action kind required edits to four places (HistoryContext
interface, deps wiring in editor.ts, entry factory, recorder method).
Anything shared between kinds becomes a chokepoint that grows.
Introduce the proper Command pattern:
- UndoableAction interface — single shape every action class
implements. HistoryManager's external API becomes record(action) +
undo/redo. Future kinds add a new class file under
engine/editor/actions/; HistoryManager itself never grows.
- isReplaying flag — record() no-ops while undo/redo is mid-flight,
so any side-effect chain inside an action's apply/revert (e.g.
delete-undo reaching editor.deleteObject which calls
history.recordDelete) can't poison the stack. Belt-and-suspenders
with the per-kind silent-setter routes the action classes will use.
- engine/editor/actions/snapshots.ts — shared ObjectSnap /
TransformSnap types, snapshot helpers, transformsEqual,
writeTransform, and recreateFromSnapshot. Used by the action
classes added in the next commit.
Legacy recordCreate / recordDelete / begin+endTransform /
recordSetColor / recordSetLocked / recordSetVisible methods stay as
thin wrappers that bind the current HistoryContext into an
UndoableAction via wrap(). Call sites compile unchanged. They're
removed once all sites migrate to record(new XAction(...)).
SelectionBridge.lockUnlockObject toggled userData.locked AND
attached/detached the gizmo, but HistoryContext.setLocked only did
the userData write. After Ctrl+Z the lock flag reverted but the
gizmo state stayed wrong.
Extract the side-effect chain (setObjectLocked + gizmo attach/detach
+ updateSelectedUI) into a private SelectionBridge.applyLockState
helper. Both lockUnlockObject (toggle path) and the new
setLockState (history-replay path) call it. Add a SceneUtils
direct-set helper (setObjectLocked) so applyLockState can target a
specific state instead of toggling.
Wire HistoryContext.setLocked through selection.setLockState so undo
of a lock toggle re-runs the gizmo attach/detach. The
recordLockChange dep stays for now; it moves to the call site in a
later commit when the action layer migrates to LockAction.
Four loader paths in scene.ts passed `position` to the placeholder
manager but never copied it onto the actual loaded object — so a
GLB / MMD / Splat / image / video drop showed the placeholder at the
cursor and then jumped to (0, 0, 0) when the asset finished loading.
Shapes were unaffected because Scene.instantiate already does
`obj.position.copy(pos)`.
Mirror the existing loadObjectFromUrl template (line 553-555):
- loadObject image/video branch — copy position onto the result of
instantiate("Image::"+url)
- loadMMDWithPlaceholder — copy onto mmd before scene.add
- loadSplatWithPlaceholder — copy onto splat before scene.add
- loadGlbWithPlaceholder — copy onto child_result before scene.add
Now that every mutation site records explicitly through HistoryManager,
SceneManager no longer needs to own an undo stack or reverse-engineer
edits from per-mouseup scene-state diffs.
Removed from SceneManager:
- undoStack, undoIndex, lastSceneState, copiedObject is kept
- undo, redo, dropRedoHistory methods
- getSceneState, is_creation, is_deletion, is_userdata, is_transformation
- add_creation_undostack, check_scene_for_updates
- onMouseMove/onMouseClick/onKeyDown/onMouseDown/onMouseUp wrappers
(no callers — pointer events now go canvas → useViewportPointer →
mouse_controls directly)
- attachEventListeners/detachEventListeners (already empty stubs)
- Constructor params: devMode, updateOutliner, is_character (unused
after the gut)
- updateOutliner field — paste() refresh moved to keymap's paste handler
Also:
- engine/Commands.ts deleted — its CreationSceneItem / DeletionSceneItem
/ TransformSceneItem / UserDataSceneItem and supporting types are
obsolete (HistoryManager owns its own entry factories).
- useViewportPointer.onPointerUp reverts to mouse_controls.onMouseUp
directly (the sceneManager indirection only existed to call
check_scene_for_updates).
- Editor.initialize drops attachEventListeners/detachEventListeners
calls and the three trailing constructor args.
scene_manager_api.ts: 477 → 265 LOC. SceneManagerAPI interface trims
to what it actually does — object lifecycle (create/retrieve/delete/
update), selection, outliner rendering. Object Panel transform-input
recording is the one remaining gap from the old diff path; tracked
as a follow-up.
Color picker is the third interactive surface (after gizmo drag and
outliner toggles) that mutates an object. Wire it the same way:
record in the onChange handler (input type="color" fires once per
committed pick, so one undo entry per user choice).
Object Panel transform inputs (position/rotation/scale text fields)
are still on the legacy diff path. Wiring them is one more pass —
they need a coalescer because each keystroke flushes to the engine.
Tracking that as a follow-up.
The gizmo's onDraggingChanged fires once with dragging=true at
dragstart and once with dragging=false at dragend. Wire those
boundaries to history.beginTransform / endTransform so each drag
becomes one coalesced undo entry. No-op moves (mouse-down + drag-up
without movement) are dropped automatically by HistoryManager.
Object Panel inputs and color edits still ride the legacy
check_scene_for_updates diff path; those are migrated separately
before the diff machinery is removed.
Wires creation, deletion, lock, visibility, and paste through the new
HistoryManager. Mutator-recorded — every external call site keeps
the same signature it had before; recording happens inside the mutator
or its dep callback, so callers don't grow new responsibilities.
Recordings added:
- actions/addObject, addCharacter, addShape: editor.history.recordCreate(obj)
replaces the legacy add_creation_undostack call.
- editor.deleteObject: snapshots the target before delegating to
utils.deleteObject so undo can fully reconstruct it.
- SelectionBridge.lockUnlockObject: takes a recordLockChange dep
callback, records before/after locked state inline. No coupling
back to HistoryManager from the bridge.
- Outliner.handleToggleVisibility: records visible before/after at the
call site (hideObject lives on SceneManager which has no history dep).
- SceneManager.paste: now returns the new Object3D so keymap's paste
handler can record it via editor.history.recordCreate. The legacy
add_creation_undostack call inside paste is removed.
keymap.ts: undo/redo target editor.history; deleteSelected routes
through editor.deleteObject (so history records); paste captures the
returned object and records.
Transform recording (gizmo drag, panel inputs) and color stay on the
old check_scene_for_updates diff path until follow-up commits. Old
SceneManager undo machinery (undoStack, undo/redo methods,
add_creation_undostack, the diff helpers) is still in place but only
the diff path drives transforms now.
New peer subsystem under engine/editor/ that owns the undo/redo stack.
Lands the foundation only — old SceneManager-based undo machinery
still drives undo/redo for now. A follow-up commit migrates every
mutation site over and removes the legacy diffing.
Design:
- Pure-data HistoryEntry: { label, apply(ctx), revert(ctx) }. Entries
receive a HistoryContext when replayed; they don't hold subsystem
references. Easy to test in isolation, immune to subsystem
re-creation.
- The external API is the recordX methods (recordCreate, recordDelete,
begin/endTransform, recordSetColor/Locked/Visible). Mutation sites
call ONE method — entry construction stays in the manager. Adding
a future action is "add an entry factory + a recorder method"; no
churn at action sites.
- Async-serialized replay via a Promise chain: a Ctrl+Z mash during
an in-flight asset reload can't interleave entry replays.
- onChange callback hook for a future store-bridge to publish
canUndo/canRedo into PageSceneStore.
- Capacity 64 (vs. main's 16); transform/userdata no-ops are dropped.
Editor wires the HistoryContext inline in its constructor (Phase 2
deps idiom — same shape as CameraControllerDeps etc.). Closures over
`this` resolve sceneManager/activeScene lazily so construction order
inside the editor constructor doesn't matter.
Pre-refactor, the queue handler (timeline.addShape/addObject/addCharacter)
called sceneManager.add_creation_undostack(obj) after creating the
THREE.Object3D. The actions/ extraction collapsed the queue path but
forgot to carry the undo push along, so dragging a new asset onto the
3D canvas left the undo stack untouched.
Push a CreationSceneItem from each of addObject, addCharacter, and
addShape after the engine call succeeds. check_scene_for_updates only
detects transforms / deletions / userdata diffs on mouseup; it does
not record creations, which is why this has to be an explicit call.
useViewportPointer.onPointerUp called editor.mouse_controls.onMouseUp
directly. That ran the interaction state machine (gizmo end, FK toggle,
selection click) but skipped editor.sceneManager.onMouseUp, which is
the only thing that calls check_scene_for_updates. Result: transform,
deletion, and userdata edits (color, lock, visibility) never made it
into the undo stack, so Ctrl+Z found nothing to undo.
Pre-refactor, SceneManager.attachEventListeners attached a window-level
mouseup that went through SceneManager.onMouseUp; the canvas-scoped
hook lost the same pass-through. Calling sceneManager.onMouseUp here
preserves all the mouse_controls behavior (it forwards internally) and
restores the post-mouseup undo capture.
Phase 1 moved camera, freeCamState, and focalLengthToFov off Editor and
onto CameraController, but two camera-update blocks in PageEditor
(handleSelectCamera and handleAddCamera) still read editorEngine.camera,
editorEngine.focalLengthToFov, and editorEngine.freeCamState. Those
fields are undefined now, so the cam-update guards short-circuited and
the reset/random-camera buttons silently did nothing.
Sweep both blocks to read from editorEngine.cameraController.* and
collapse the chain into a local `cam` const.
DndAsset.onPointerUp captured event.pageX/Y but threw them away; both
add* actions then defaulted position to origin. PageEditor's gallery
onImageDrop tried an NDC unproject but gated on editorEngine.camera,
which Phase 1's CameraController extraction left undefined — the guard
short-circuited and worldPosition stayed undefined.
Add pickDropPosition: a deps-pattern helper that mirrors the
pre-refactor timeline.getPos() — raycasts from the camera through the
cursor against scene children and returns the first hit (or origin if
the ray misses or the camera/canvas isn't ready). Same idiom as
CameraControllerDeps / GizmoControllerDeps from Phase 2; callers build
the deps inline so the helper stays decoupled from Editor and any
subsystem's import surface.
Both drop sites — DndAsset.onPointerUp and PageEditor's 3D gallery
onImageDrop — now call the helper and forward worldPosition to
addCharacter / addObject / addShape.
create_parim was a one-line passthrough to activeScene.instantiate
that the pre-refactor queue path used. After actions/addShape was
rewired to call sceneManager.create with the "Parim" sentinel,
create_parim has zero callers anywhere in the artcraft frontend.
actions/addShape passed item.media_id ("Box", "Sphere", ...) as the
routing token to SceneManager.create. That hits the
loadObject(media_token, ...) branch and tries to fetch a media URL
for "Box", which fails — the shape never lands in the scene.
Pass "Parim" as the routing sentinel and item.media_id as the
geometry name so create() forwards to scene.instantiate(name, pos),
which has the geometry switch ("Box" -> BoxGeometry, etc.). Then
overwrite obj.name with the display label so the outliner reads
"Cube" instead of "Box".
Restores the pre-Phase-4 behavior of editorEngine.create_parim(media_id, pos)
that the queue path used.
- Move sound catalog registration off PageScene mount and into a single
SoundManager.install() call in GlobalSettingsManager so sounds exist at
app boot (previously silent until PageScene mounted).
- Honor the play_sounds master toggle in all five event playback sites
via SoundManager.playEvent helpers. Settings preview keeps firing
regardless (handled by SoundManager.playPreview).
- Replace per-callsite GetAppPreferences + SoundRegistry boilerplate in
the four generation event hooks and the file-deleted handler with
one-line SoundManager.* calls.
- Dedupe SOUND_OPTIONS: AudioSettingsPane now reads
SoundManager.OPTIONS, derived from a single declarative SOUNDS array
(label-bearing entries are user-selectable; others are still registered
for code-driven use).
- Narrow the soundboard public barrel to SoundManager + SoundOption
only; SoundRegistry and SoundEffect are now lib-internal.
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.