cozyclay 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -0
- package/README.md +17 -2
- package/THIRD_PARTY_NOTICES.md +8 -0
- package/bin/agent/agent-routes.mjs +219 -38
- package/bin/agent/agent-runner.mjs +98 -4
- package/bin/agent/codex-client.mjs +2 -1
- package/bin/agent/fal-contract.mjs +0 -17
- package/bin/agent/h3-preservation.mjs +6 -8
- package/bin/agent/provider-keys.mjs +1 -1
- package/bin/agent/providers.mjs +124 -3
- package/bin/agent/script-tool.mjs +74 -0
- package/bin/agent/session-store.mjs +5 -5
- package/bin/agent/studio-prompt.mjs +2 -1
- package/bin/agent/studio-rebase.mjs +314 -0
- package/bin/agent/studio-tools.mjs +21 -14
- package/bin/agent/supervisor.mjs +311 -0
- package/bin/agent/video-adapters.mjs +0 -7
- package/bin/codex-auth.mjs +6 -5
- package/bin/cozyclay.mjs +12 -3
- package/bin/live/cli.mjs +2 -3
- package/bin/live/client.mjs +1 -1
- package/bin/live/studio.mjs +5 -2
- package/bin/mcp-runtime.mjs +12 -2
- package/bin/telemetry-state.mjs +3 -3
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-B41FmPYI.js +1 -0
- package/dist/assets/app-Do-Og5Pu.css +1 -0
- package/dist/assets/app-DwP-tl2x.js +4920 -0
- package/dist/assets/{demo-ByeYp-dY.js → demo-Dt0qZfS7.js} +1 -1
- package/dist/assets/{first-shot-handoff-DgT28ZiV.js → first-shot-handoff-BlT33CGf.js} +1 -1
- package/dist/assets/{landing-BaXkpFip.js → landing-BSesrCpL.js} +1 -1
- package/dist/assets/scene-asset-sync-BCWDJB1d.js +19 -0
- package/dist/assets/{shot-prompt-CbqWcWYi.css → scene-asset-sync-DjJ_cJ2R.css} +1 -1
- package/dist/assets/{ticket-BcmsnHn5.js → ticket-jqMhqL88.js} +1 -1
- package/dist/assets/workflow-Br-K7Srs.js +179 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/index.html +328 -222
- package/dist/sitemap.xml +5 -5
- package/dist/workflow/index.html +4 -4
- package/mcp/LIVE-PROTOCOL.md +23 -2
- package/mcp/README.md +37 -2
- package/mcp/ardy-prompts.mjs +1 -1
- package/mcp/image-file.mjs +132 -0
- package/mcp/live-hub.mjs +2 -2
- package/mcp/package.json +1 -0
- package/mcp/runtime/package-lock.json +7 -0
- package/mcp/runtime/package.json +1 -0
- package/mcp/server.mjs +0 -6
- package/mcp/tool-handlers.mjs +289 -87
- package/package.json +11 -2
- package/src/App.jsx +801 -259
- package/src/agent-image-client.js +94 -0
- package/src/analytics.js +47 -3
- package/src/app-stage.jsx +129 -2745
- package/src/ardy/auto-physics.js +6 -16
- package/src/ardy/collision-blockers.js +3 -7
- package/src/ardy/fix-collisions.js +5 -35
- package/src/ardy/ik-drag.js +86 -5
- package/src/ardy/ik-key-json.js +3 -1
- package/src/ardy/ik.js +15 -13
- package/src/ardy/motion-calibration.js +1 -1
- package/src/ardy/npz.js +0 -8
- package/src/ardy/physics-panel.jsx +3 -2
- package/src/ardy/physics-review.js +3 -2
- package/src/ardy/physics-support.js +1 -1
- package/src/ardy/physics-surface.js +1 -1
- package/src/ardy/platform-fit-panel.jsx +2 -1
- package/src/ardy/playback.js +1 -5
- package/src/ardy/range-pin.js +2 -2
- package/src/ardy/root-drop.js +0 -1
- package/src/ardy/timeline.css +196 -3
- package/src/ardy/timeline.jsx +117 -29
- package/src/ardy/to-cskel27.js +0 -2
- package/src/ardy/waypoints.js +1 -4
- package/src/ardy-bridge-contract.js +281 -0
- package/src/asset-pane.jsx +19 -9
- package/src/auto-color.js +1 -1
- package/src/burn-in.js +1 -1
- package/src/camera-block.js +1 -1
- package/src/camera-follow.js +75 -32
- package/src/camera-move.js +0 -6
- package/src/camera-tutorial.jsx +43 -24
- package/src/character-kind.js +79 -0
- package/src/command-bus.js +76 -17
- package/src/commands/cast.js +31 -7
- package/src/commands/elements/object.js +1 -1
- package/src/commands/elements/shot.js +1 -1
- package/src/commands/elements.js +1 -1
- package/src/commands/export.js +3 -3
- package/src/commands/motion.js +2 -2
- package/src/commands/objects.js +105 -10
- package/src/commands/project.js +16 -3
- package/src/commands/sculpt-images.js +53 -0
- package/src/commands/shot.js +88 -10
- package/src/commands/view.js +10 -4
- package/src/coplanar-depth.js +216 -0
- package/src/cuts.js +7 -6
- package/src/domains/cast.js +60 -43
- package/src/domains/motion/motion-intent-codec.js +50 -0
- package/src/domains/motion/motion-range-pin-rules.js +23 -0
- package/src/domains/motion/motion-video-capture.js +664 -0
- package/src/domains/motion.js +253 -742
- package/src/domains/objects.js +106 -5
- package/src/domains/scenes.js +89 -32
- package/src/domains/shots.js +33 -7
- package/src/error-boundary.jsx +1 -1
- package/src/execution-telemetry.js +5 -5
- package/src/facing-marks.js +14 -1
- package/src/first-success-guide.jsx +35 -4
- package/src/gizmo-claim.js +3 -1
- package/src/grid-floor.jsx +1 -4
- package/src/hierarchy-model.js +48 -35
- package/src/hierarchy-panel.css +1 -0
- package/src/hierarchy-panel.jsx +54 -102
- package/src/history.js +0 -2
- package/src/i2v-motion-client.js +1 -4
- package/src/ime.js +6 -0
- package/src/keyframe-pack.js +15 -6
- package/src/line-edit.js +4 -4
- package/src/live-control.js +6 -6
- package/src/matte-editor.js +2 -8
- package/src/matte.js +0 -19
- package/src/mesh-graph-clone.js +16 -2
- package/src/mesh-sniff.js +0 -1
- package/src/monotone-cubic.js +40 -0
- package/src/motion/generation.js +1 -1
- package/src/motion-store.js +4 -4
- package/src/motion-trail.js +5 -11
- package/src/multimodel-ingest.js +1 -3
- package/src/object-catalog.jsx +7 -5
- package/src/object-gizmo.jsx +45 -31
- package/src/object-path.js +310 -67
- package/src/object-travel.js +224 -0
- package/src/otio.js +8 -3
- package/src/panels/CharacterTransformPanel.jsx +24 -6
- package/src/panels/ObjectTransformPanel.jsx +4 -3
- package/src/panels/PoseImagePanel.jsx +205 -0
- package/src/panels/PromptBlocksPanel.jsx +3 -1
- package/src/panels/ProxyFigurePanel.jsx +56 -0
- package/src/panels/ReferenceImageField.jsx +1 -1
- package/src/panels/RigControlPanel.jsx +24 -3
- package/src/panels/RigPanel.jsx +2 -1
- package/src/panels/SculptPanel.jsx +405 -0
- package/src/panels/SubjectBox.jsx +1 -1
- package/src/panels/SubjectsPanel.jsx +0 -2
- package/src/panels/VideoCapturePanel.jsx +2 -1
- package/src/panels/pose-image.css +42 -0
- package/src/panels/pose.css +3 -0
- package/src/panels/sculpt.css +162 -0
- package/src/plan-diagram.js +184 -0
- package/src/planview.jsx +127 -80
- package/src/playground.js +3 -5
- package/src/pose-fade.js +77 -0
- package/src/pose-gizmo.css +23 -0
- package/src/pose-gizmo.js +96 -0
- package/src/posestudio.jsx +134 -83
- package/src/project-browser.css +53 -0
- package/src/project-browser.jsx +52 -3
- package/src/project.js +55 -24
- package/src/props.jsx +177 -36
- package/src/proxy-figure.jsx +69 -0
- package/src/range-pin-panel.jsx +0 -1
- package/src/receipt-findings.js +20 -0
- package/src/render-passes.js +0 -1
- package/src/resource-status.jsx +1 -1
- package/src/root-path.js +48 -0
- package/src/route-owner.js +52 -0
- package/src/scene-asset-cache.js +1 -2
- package/src/scene-assets.js +12 -4
- package/src/scene-fbx.js +1 -1
- package/src/scene-mesh-cache.js +0 -1
- package/src/scene-objects.js +319 -19
- package/src/scenes.js +62 -17
- package/src/sculpt-blueprint.js +65 -0
- package/src/sculpt-compare.js +384 -0
- package/src/sculpt-geometry.js +141 -0
- package/src/sculpt-mode.js +168 -0
- package/src/sculpt-recipe.js +385 -0
- package/src/sculpt-run.js +351 -0
- package/src/sculpt-stage.jsx +221 -0
- package/src/settings-menu.jsx +1 -1
- package/src/shell/BoardSlot.jsx +341 -0
- package/src/shell/BottomDock.jsx +58 -14
- package/src/shell/DetailsSlot.jsx +26 -6
- package/src/shell/MenuBar.jsx +78 -6
- package/src/shell/OutlinerSlot.jsx +3 -1
- package/src/shell/TopBar.jsx +17 -3
- package/src/shell/ViewportToolbar.jsx +18 -3
- package/src/shell/dock.css +239 -0
- package/src/shell/glass-regions.css +3 -2
- package/src/shell/log-store.js +4 -4
- package/src/shell/mode.css +20 -0
- package/src/shell/studio-shell-context.js +4 -1
- package/src/shot-authoring.js +69 -6
- package/src/stable-items.js +1 -1
- package/src/stage-attach-frames.js +169 -0
- package/src/stage-canvas-rigs.jsx +402 -0
- package/src/stage-capture.jsx +216 -0
- package/src/stage-characters.jsx +302 -0
- package/src/stage-editor-gizmos.jsx +1229 -0
- package/src/stage-labels-ko.js +204 -0
- package/src/stage-startup-defaults.js +182 -0
- package/src/starter-scenes.js +1 -1
- package/src/storyboard.js +24 -5
- package/src/studio-actions.js +42 -9
- package/src/studio-agent-commands.js +186 -23
- package/src/studio-agent-context.js +11 -9
- package/src/studio-agent-motion.js +2 -1
- package/src/studio-agent-protocol.js +56 -23
- package/src/studio-app-binding.js +52 -23
- package/src/studio-contact-sheet.js +3 -3
- package/src/studio-elements.js +11 -2
- package/src/studio-geometry-facts.js +114 -0
- package/src/styles.css +111 -10
- package/src/theme.js +1 -1
- package/src/timeline-fps.js +13 -0
- package/src/ui-scrub.js +4 -4
- package/src/ui.jsx +6 -4
- package/src/use-asset-shelf.js +60 -0
- package/src/use-camera-tutorial.js +50 -0
- package/src/use-embed-bridge.js +12 -0
- package/src/use-first-success-prompt.js +7 -0
- package/src/use-i2v-capture-modal.js +15 -0
- package/src/use-sculpt-mode.js +95 -0
- package/src/workflow/AgentPanel.jsx +110 -20
- package/src/workflow/WorkflowBuilder.jsx +1 -1
- package/src/workflow/agent-client.js +113 -45
- package/src/workflow/agent-panel.css +35 -0
- package/src/workflow/attachment-image.js +1 -1
- package/src/workflow/canvas-commands.js +3 -3
- package/src/workflow/cozy-scene-node.js +3 -3
- package/src/workflow/local-workflow.js +2 -2
- package/src/workflow/motion-input.js +3 -3
- package/src/workflow/node-schema.js +1 -1
- package/src/workflow/scene-asset-sync.js +2 -9
- package/src/workflow/shot-prompt-node.js +1 -1
- package/src/workflow/video-contract.js +1 -1
- package/src/workflow/workflow-resources.js +0 -8
- package/src/workflow-send.js +87 -0
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
- package/tools/check-eslint.mjs +57 -0
- package/tools/check-knip.mjs +60 -0
- package/tools/check-loc.mjs +54 -0
- package/tools/demo-worker/api-client.mjs +7 -13
- package/tools/demo-worker/generate.mjs +2 -4
- package/tools/eslint-baseline.json +23 -0
- package/tools/kimodo/effector-constraints.mjs +4 -4
- package/tools/kimodo/generate.mjs +1 -1
- package/tools/knip-baseline.json +331 -0
- package/tools/loc-baseline.json +126 -0
- package/tools/process-supervisor.mjs +1 -1
- package/tools/projflow/replay.mjs +4 -4
- package/tools/projflow/runner.mjs +3 -3
- package/tools/qa/studio-control-count.mjs +2 -0
- package/tools/run-tests.mjs +96 -7
- package/dist/assets/analytics-MVZOEW8O.js +0 -1
- package/dist/assets/app-C1sdVbg4.css +0 -1
- package/dist/assets/app-CA55tcI3.js +0 -4876
- package/dist/assets/shot-prompt-Yrt99wZx.js +0 -17
- package/dist/assets/workflow-dNPGY9xz.js +0 -179
package/src/ui.jsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import { createScrubGesture } from "./ui-scrub.js";
|
|
4
|
+
import { isImeComposing } from "./ime.js";
|
|
4
5
|
|
|
5
6
|
// Custom dropdown, structurally matched to the reference (`.dropdown`,
|
|
6
7
|
// `dd-caret`, `dropdown-menu`, `dropdown-item`, `dd-check`) with the keyboard
|
|
@@ -279,7 +280,7 @@ export function Field({ label, children }) {
|
|
|
279
280
|
</div>
|
|
280
281
|
);
|
|
281
282
|
}
|
|
282
|
-
export function NumberField({ label, value, step, precision = 2, scrubRange, onChange, onScrubStart, onScrubEnd, title }) {
|
|
283
|
+
export function NumberField({ label, value, step, precision = 2, scrubRange, onChange, onScrubStart, onScrubEnd, title, disabled = false }) {
|
|
283
284
|
const [focused, setFocused] = useState(false);
|
|
284
285
|
const [draft, setDraft] = useState(null);
|
|
285
286
|
// The draft must be readable synchronously: blur() fires the commit
|
|
@@ -335,7 +336,7 @@ export function NumberField({ label, value, step, precision = 2, scrubRange, onC
|
|
|
335
336
|
};
|
|
336
337
|
|
|
337
338
|
const onKeyDown = (e) => {
|
|
338
|
-
if (e.key === "Enter") {
|
|
339
|
+
if (e.key === "Enter" && !isImeComposing(e)) {
|
|
339
340
|
e.preventDefault();
|
|
340
341
|
commitDraft();
|
|
341
342
|
// Keep focus and select the committed value so the next keystroke
|
|
@@ -357,7 +358,7 @@ export function NumberField({ label, value, step, precision = 2, scrubRange, onC
|
|
|
357
358
|
// alone was a 15 px hotspot nobody found). Sensitivity is a fixed-travel
|
|
358
359
|
// rate (src/ui-scrub.js), not pixels * step, so every field feels the same.
|
|
359
360
|
const onFieldPointerDown = (e) => {
|
|
360
|
-
if (e.button !== 0) return;
|
|
361
|
+
if (e.button !== 0 || disabled) return;
|
|
361
362
|
// A press inside an already-focused input is text editing (caret
|
|
362
363
|
// placement, drag-select), not a scrub — leave it alone.
|
|
363
364
|
if (focused && e.target === inputRef.current) return;
|
|
@@ -460,7 +461,7 @@ export function NumberField({ label, value, step, precision = 2, scrubRange, onC
|
|
|
460
461
|
|
|
461
462
|
return (
|
|
462
463
|
<span
|
|
463
|
-
className={`number-field v2-details-number-field${focused ? " editing" : ""}`}
|
|
464
|
+
className={`number-field v2-details-number-field${focused ? " editing" : ""}${disabled ? " disabled" : ""}`}
|
|
464
465
|
title={title}
|
|
465
466
|
onPointerDown={onFieldPointerDown}
|
|
466
467
|
onPointerUp={endFieldDrag}
|
|
@@ -472,6 +473,7 @@ export function NumberField({ label, value, step, precision = 2, scrubRange, onC
|
|
|
472
473
|
type="text"
|
|
473
474
|
inputMode="decimal"
|
|
474
475
|
value={display}
|
|
476
|
+
disabled={disabled}
|
|
475
477
|
onFocus={onFocus}
|
|
476
478
|
onBlur={onBlur}
|
|
477
479
|
onChange={(e) => setDraftValue(e.target.value)}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { ASSET_DELETE_UNDO_MS } from "./app-stage.jsx";
|
|
3
|
+
import { CUTOUT_KIND, MESH_KIND } from "./scene-objects.js";
|
|
4
|
+
import { assetGraphSignature } from "./scene-assets.js";
|
|
5
|
+
|
|
6
|
+
export function useAssetShelf({ scenes, activeSceneId, sceneObjects }) {
|
|
7
|
+
// The imported-pictures region of the Assets shelf. null = the scan has
|
|
8
|
+
// never resolved (the pane shows skeletons, NEVER the empty message); an
|
|
9
|
+
// array = the SOURCE ids to show, mattes and cut renders already filtered
|
|
10
|
+
// out (asset-shelf.js). Scans run only while the shelf is visible, and
|
|
11
|
+
// re-run when a cutout's lineage changes — not on every transform tick, so
|
|
12
|
+
// a gizmo drag never hammers IndexedDB.
|
|
13
|
+
const [shelfImageIds, setShelfImageIds] = useState(null);
|
|
14
|
+
const [shelfMeshIds, setShelfMeshIds] = useState(null);
|
|
15
|
+
const [manageAssetStorage, setManageAssetStorage] = useState(false);
|
|
16
|
+
// A separate scan preserves the source-only placement shelf while the
|
|
17
|
+
// manager exposes every unreachable stored record, including matte and cut
|
|
18
|
+
// derivatives orphaned with a deleted card.
|
|
19
|
+
const [unusedAssetIds, setUnusedAssetIds] = useState(null);
|
|
20
|
+
const [usedAssetIds, setUsedAssetIds] = useState(null);
|
|
21
|
+
const [usageCounts, setUsageCounts] = useState(new Map());
|
|
22
|
+
const assetShelfScanTokenRef = useRef(0);
|
|
23
|
+
const legacyDerivedIdsRef = useRef(new Set());
|
|
24
|
+
// This is deliberately session-only. Each entry is a complete IndexedDB
|
|
25
|
+
// record, so Undo can put it back byte-for-byte until the page is reloaded.
|
|
26
|
+
const [assetTrash, setAssetTrash] = useState([]);
|
|
27
|
+
// Same rule as the object deletion offer: the toast is a window, not a
|
|
28
|
+
// banner. Only the OFFER expires — the trashed record itself is the restore
|
|
29
|
+
// data, so it is deliberately kept for the session and never timed out.
|
|
30
|
+
const [assetUndoOffered, setAssetUndoOffered] = useState(false);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (!assetUndoOffered) return undefined;
|
|
33
|
+
const timer = setTimeout(() => setAssetUndoOffered(false), ASSET_DELETE_UNDO_MS);
|
|
34
|
+
return () => clearTimeout(timer);
|
|
35
|
+
}, [assetUndoOffered]);
|
|
36
|
+
const [deletingAssetId, setDeletingAssetId] = useState(null);
|
|
37
|
+
const cutoutLineage = useMemo(
|
|
38
|
+
() => JSON.stringify(sceneObjects.flatMap((object) => (object.renderer === CUTOUT_KIND ? [[object.assetId, object.sourceAssetId, object.matteAssetId]] : []))),
|
|
39
|
+
[sceneObjects],
|
|
40
|
+
);
|
|
41
|
+
const meshLineage = useMemo(
|
|
42
|
+
() => JSON.stringify(sceneObjects.flatMap((object) => (object.renderer === MESH_KIND ? [object.assetId] : []))),
|
|
43
|
+
[sceneObjects],
|
|
44
|
+
);
|
|
45
|
+
const projectCutoutLineage = useMemo(() => {
|
|
46
|
+
const allScenes = scenes.map((scene) => (scene.id === activeSceneId ? { ...scene, objects: sceneObjects } : scene));
|
|
47
|
+
return JSON.stringify(
|
|
48
|
+
allScenes.flatMap((scene) =>
|
|
49
|
+
(Array.isArray(scene.objects) ? scene.objects : []).flatMap((object) =>
|
|
50
|
+
object.renderer === CUTOUT_KIND ? [[object.assetId, object.sourceAssetId, object.matteAssetId]] : [],
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
}, [scenes, activeSceneId, sceneObjects]);
|
|
55
|
+
const projectAssetGraphSignature = useMemo(() => {
|
|
56
|
+
const allScenes = scenes.map((scene) => (scene.id === activeSceneId ? { ...scene, objects: sceneObjects } : scene));
|
|
57
|
+
return assetGraphSignature(allScenes);
|
|
58
|
+
}, [scenes, activeSceneId, sceneObjects]);
|
|
59
|
+
return { shelfImageIds, setShelfImageIds, shelfMeshIds, setShelfMeshIds, manageAssetStorage, setManageAssetStorage, unusedAssetIds, setUnusedAssetIds, usedAssetIds, setUsedAssetIds, usageCounts, setUsageCounts, assetShelfScanTokenRef, legacyDerivedIdsRef, assetTrash, setAssetTrash, assetUndoOffered, setAssetUndoOffered, deletingAssetId, setDeletingAssetId, cutoutLineage, meshLineage, projectCutoutLineage, projectAssetGraphSignature };
|
|
60
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import { cameraTutorialSuppressed } from "./first-shot-handoff.js";
|
|
3
|
+
import { loadProjectSession } from "./project.js";
|
|
4
|
+
import { SCENES_STORAGE_KEY } from "./scenes.js";
|
|
5
|
+
|
|
6
|
+
export function useCameraTutorial({ embedMode, playgroundMode }) {
|
|
7
|
+
// The camera tutorial (#206): the landing page's seven steps, run against
|
|
8
|
+
// the real studio instead of the playground iframe. It opens from
|
|
9
|
+
// /app/?tutorial=camera or from Settings ▾, and it is not offered inside an
|
|
10
|
+
// embed.
|
|
11
|
+
|
|
12
|
+
// Both doors go through startCameraTutorial (#209), which puts the studio in
|
|
13
|
+
// the state the landing page teaches these steps in — the city-block starter
|
|
14
|
+
// scene with the walk take on its character — so Shot / Rail / Play always
|
|
15
|
+
// have something to frame. That function is declared with the project
|
|
16
|
+
// actions further down; the listeners here reach it through a ref so they
|
|
17
|
+
// always call the current render's closure (the confirm reads projectDirty).
|
|
18
|
+
const [cameraTutorialEntry] = useState(() => {
|
|
19
|
+
const params = new URLSearchParams(globalThis.location?.search || "");
|
|
20
|
+
const query = params.get("tutorial") === "camera";
|
|
21
|
+
let firstRun = false;
|
|
22
|
+
try {
|
|
23
|
+
firstRun = !query && !params.get("scene") && !cameraTutorialSuppressed()
|
|
24
|
+
&& !loadProjectSession()?.name && !globalThis.localStorage?.getItem(SCENES_STORAGE_KEY);
|
|
25
|
+
} catch { firstRun = false; }
|
|
26
|
+
return !embedMode && !playgroundMode && !cameraTutorialSuppressed() && (query ? "query" : firstRun ? "first-run" : null);
|
|
27
|
+
});
|
|
28
|
+
const cameraTutorialQuery = Boolean(cameraTutorialEntry);
|
|
29
|
+
const [cameraTutorial, setCameraTutorial] = useState(false);
|
|
30
|
+
const cameraTutorialAnalytics = useRef(null);
|
|
31
|
+
const [cameraTutorialAttempt, setCameraTutorialAttempt] = useState(0);
|
|
32
|
+
const [cameraTutorialHandoff, setCameraTutorialHandoff] = useState(null);
|
|
33
|
+
const cameraTutorialCompletedRef = useRef(false);
|
|
34
|
+
const exportMenuTriggerRef = useRef(null);
|
|
35
|
+
const exportShotIdRef = useRef(null);
|
|
36
|
+
// The step the tutorial is on, published on the .app root so styles.css can
|
|
37
|
+
// spotlight the one control that step needs (#211).
|
|
38
|
+
const [cameraTutorialStep, setCameraTutorialStep] = useState(null);
|
|
39
|
+
const startCameraTutorialRef = useRef(null);
|
|
40
|
+
const cameraTutorialStarted = useRef(false);
|
|
41
|
+
// A tutorial restart never reloads the starter over the user's edits.
|
|
42
|
+
const tutorialStarterRef = useRef(false);
|
|
43
|
+
const tutorialLoadingRef = useRef(false);
|
|
44
|
+
const tutorialProjectEpochRef = useRef(0);
|
|
45
|
+
const tutorialSeedEpochRef = useRef(null);
|
|
46
|
+
// Armed once the starter is applied, consumed by the seed effect next to the
|
|
47
|
+
// hosted-demo seed as soon as the new character's rig exists.
|
|
48
|
+
const [tutorialSeedPending, setTutorialSeedPending] = useState(false);
|
|
49
|
+
return { cameraTutorialEntry, cameraTutorialQuery, cameraTutorial, setCameraTutorial, cameraTutorialAnalytics, cameraTutorialAttempt, setCameraTutorialAttempt, cameraTutorialHandoff, setCameraTutorialHandoff, cameraTutorialCompletedRef, exportMenuTriggerRef, exportShotIdRef, cameraTutorialStep, setCameraTutorialStep, startCameraTutorialRef, cameraTutorialStarted, tutorialStarterRef, tutorialLoadingRef, tutorialProjectEpochRef, tutorialSeedEpochRef, tutorialSeedPending, setTutorialSeedPending };
|
|
50
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { useRef, useState } from "react";
|
|
2
|
+
import { isPlaygroundEmbed } from "./playground.js";
|
|
3
|
+
|
|
4
|
+
export function useEmbedBridge() {
|
|
5
|
+
const embedMode = ["scene", "playview"].includes(new URLSearchParams(globalThis.location?.search || "").get("embed"));
|
|
6
|
+
// The landing page's try-it iframe: full studio interaction on a preset
|
|
7
|
+
// scene with the project chrome hidden and saving off (see playground.js).
|
|
8
|
+
const playgroundMode = isPlaygroundEmbed(globalThis.location?.search);
|
|
9
|
+
const [playgroundHint, setPlaygroundHint] = useState(null);
|
|
10
|
+
const playgroundExportRef = useRef(null);
|
|
11
|
+
return { embedMode, playgroundMode, playgroundHint, setPlaygroundHint, playgroundExportRef };
|
|
12
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
export function useFirstSuccessPrompt() {
|
|
4
|
+
const [firstSuccessGuideOpen, setFirstSuccessGuideOpen] = useState(false);
|
|
5
|
+
const [useCaseAskOpen, setUseCaseAskOpen] = useState(false);
|
|
6
|
+
return { firstSuccessGuideOpen, setFirstSuccessGuideOpen, useCaseAskOpen, setUseCaseAskOpen };
|
|
7
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
export function useI2vCaptureModal() {
|
|
4
|
+
const [i2vMotionEnabled, setI2vMotionEnabled] = useState(false);
|
|
5
|
+
const [i2vMotionMode, setI2vMotionMode] = useState("interpolate");
|
|
6
|
+
const [i2vMotionCameraUnlocked, setI2vMotionCameraUnlocked] = useState(false);
|
|
7
|
+
return {
|
|
8
|
+
i2vMotionEnabled,
|
|
9
|
+
setI2vMotionEnabled,
|
|
10
|
+
i2vMotionMode,
|
|
11
|
+
setI2vMotionMode,
|
|
12
|
+
i2vMotionCameraUnlocked,
|
|
13
|
+
setI2vMotionCameraUnlocked,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { createAgentTransport } from "./workflow/agent-client.js";
|
|
3
|
+
import { requestAgentImage, stylizeErrorMessage } from "./agent-image-client.js";
|
|
4
|
+
import { blueprintErrorMessage, blueprintRequest } from "./sculpt-blueprint.js";
|
|
5
|
+
import { agentSessionReady, createCommandTap, sculptModelChoice, sculptTargetFromSelection, storeSculptModel } from "./sculpt-mode.js";
|
|
6
|
+
import { SCULPT_KIND } from "./scene-objects.js";
|
|
7
|
+
|
|
8
|
+
export function useSculptMode({ workflowMode, sceneObjects, selectedSceneObjectId }) {
|
|
9
|
+
// Sculpt mode (#739): one sculpt on an isolated turntable, the Sculpt panel
|
|
10
|
+
// and its agent run. The target is the sculpt selected on entry, or a new one
|
|
11
|
+
// the first run creates; picking another sculpt while here retargets.
|
|
12
|
+
const sculptMode = workflowMode === "sculpt";
|
|
13
|
+
const sculptCamRef = useRef(null);
|
|
14
|
+
const [sculptTargetId, setSculptTargetId] = useState(null);
|
|
15
|
+
const [sculptView, setSculptView] = useState({ view: "front", nonce: 0 });
|
|
16
|
+
const [sculptReference, setSculptReference] = useState(null);
|
|
17
|
+
const [sculptRun, setSculptRun] = useState(null);
|
|
18
|
+
const sculptRunRef = useRef(null);
|
|
19
|
+
const [sculptAgentStatus, setSculptAgentStatus] = useState("unknown");
|
|
20
|
+
// The model Sculpt runs on (its own pick, remembered apart from the chat model) and the list it comes from.
|
|
21
|
+
const [sculptModels, setSculptModels] = useState({ state: "loading", providers: [], models: [] });
|
|
22
|
+
const [sculptModelPick, setSculptModelPick] = useState({ model: "", effort: null });
|
|
23
|
+
const chooseSculptModel = (next) => setSculptModelPick((current) => {
|
|
24
|
+
const choice = { ...current, ...next };
|
|
25
|
+
storeSculptModel(choice);
|
|
26
|
+
return choice;
|
|
27
|
+
});
|
|
28
|
+
const [sculptTransport] = useState(() => createAgentTransport({ surface: "studio" }));
|
|
29
|
+
const [sculptTap] = useState(createCommandTap);
|
|
30
|
+
const sculptTarget = sceneObjects.find((object) => object.id === sculptTargetId && object.renderer === SCULPT_KIND) ?? null;
|
|
31
|
+
const requestSculptView = (view) => setSculptView((current) => ({ view, nonce: current.nonce + 1 }));
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!sculptMode) return;
|
|
34
|
+
setSculptTargetId(sculptTargetFromSelection(sceneObjects, selectedSceneObjectId));
|
|
35
|
+
requestSculptView("front");
|
|
36
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
37
|
+
}, [sculptMode]);
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
const selected = sculptMode ? sculptTargetFromSelection(sceneObjects, selectedSceneObjectId) : null;
|
|
40
|
+
if (selected) setSculptTargetId(selected);
|
|
41
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
42
|
+
}, [selectedSceneObjectId]);
|
|
43
|
+
// Start needs the Agent panel's readiness rule (signed in, or a provider
|
|
44
|
+
// key in the sidecar); a sign-in finished in another window lands on focus.
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
if (!sculptMode) return undefined;
|
|
47
|
+
let live = true;
|
|
48
|
+
const read = () => {
|
|
49
|
+
sculptTransport.status().then(
|
|
50
|
+
(status) => { if (live) setSculptAgentStatus(agentSessionReady(status) ? "ready" : "signed-out"); },
|
|
51
|
+
() => { if (live) setSculptAgentStatus("signed-out"); },
|
|
52
|
+
);
|
|
53
|
+
// The Agent panel's catalogue: a key added or removed elsewhere changes what can run.
|
|
54
|
+
sculptTransport.models().then(
|
|
55
|
+
(advertised) => {
|
|
56
|
+
if (!live) return;
|
|
57
|
+
const list = Array.isArray(advertised?.models) ? advertised.models : [];
|
|
58
|
+
setSculptModels({ state: list.length ? "ready" : "failed", providers: advertised?.providers ?? [], models: list });
|
|
59
|
+
setSculptModelPick((current) => sculptModelChoice(advertised, current));
|
|
60
|
+
},
|
|
61
|
+
() => { if (live) setSculptModels((current) => ({ ...current, state: current.models.length ? current.state : "failed" })); },
|
|
62
|
+
);
|
|
63
|
+
};
|
|
64
|
+
read();
|
|
65
|
+
window.addEventListener("focus", read);
|
|
66
|
+
return () => { live = false; window.removeEventListener("focus", read); };
|
|
67
|
+
}, [sculptMode, sculptTransport]);
|
|
68
|
+
// The blueprint step: the reference turned into a four-view model sheet by one image request.
|
|
69
|
+
const [sculptBlueprint, setSculptBlueprint] = useState({ state: "idle", pose: "keep", notes: "", sheet: null, error: null, startedAt: null });
|
|
70
|
+
const blueprintAbortRef = useRef(null);
|
|
71
|
+
async function generateSculptBlueprint() {
|
|
72
|
+
if (!sculptReference) return;
|
|
73
|
+
blueprintAbortRef.current?.abort();
|
|
74
|
+
const controller = new AbortController();
|
|
75
|
+
blueprintAbortRef.current = controller;
|
|
76
|
+
const startedAt = Date.now();
|
|
77
|
+
setSculptBlueprint((current) => ({ ...current, state: "generating", error: null, startedAt }));
|
|
78
|
+
try {
|
|
79
|
+
const result = await requestAgentImage(blueprintRequest({ reference: sculptReference, pose: sculptBlueprint.pose, notes: sculptBlueprint.notes }), { signal: controller.signal });
|
|
80
|
+
if (blueprintAbortRef.current !== controller) return;
|
|
81
|
+
setSculptBlueprint((current) => ({ ...current, state: "ready", sheet: { dataUrl: result.dataUrl, width: result.width, height: result.height, ms: Date.now() - startedAt } }));
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (blueprintAbortRef.current !== controller) return;
|
|
84
|
+
const cancelled = controller.signal.aborted;
|
|
85
|
+
setSculptBlueprint((current) => ({ ...current, state: current.sheet ? "ready" : "idle", error: cancelled ? null : blueprintErrorMessage(error, stylizeErrorMessage(error).replace(/^Stylize failed — |^스타일화 실패 — /, "")) }));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function cancelSculptBlueprint() {
|
|
89
|
+
const controller = blueprintAbortRef.current;
|
|
90
|
+
blueprintAbortRef.current = null;
|
|
91
|
+
controller?.abort();
|
|
92
|
+
setSculptBlueprint((current) => ({ ...current, state: current.sheet ? "ready" : "idle" }));
|
|
93
|
+
}
|
|
94
|
+
return { sculptMode, sculptCamRef, sculptView, sculptReference, setSculptReference, sculptRun, setSculptRun, sculptRunRef, sculptAgentStatus, sculptModels, sculptModelPick, chooseSculptModel, sculptTransport, sculptTap, sculptTarget, setSculptTargetId, requestSculptView, sculptBlueprint, setSculptBlueprint, generateSculptBlueprint, cancelSculptBlueprint };
|
|
95
|
+
}
|
|
@@ -8,6 +8,9 @@ import {
|
|
|
8
8
|
attachmentFromFile,
|
|
9
9
|
transferCarriesFiles,
|
|
10
10
|
} from "./attachment-image.js";
|
|
11
|
+
import { isImeComposing } from "../ime.js";
|
|
12
|
+
import { useStudioShell } from "../shell/studio-shell-context.js";
|
|
13
|
+
import { ko } from "../locale.js";
|
|
11
14
|
import {
|
|
12
15
|
AGENT_PANEL_OVERLAY_BREAKPOINT,
|
|
13
16
|
AGENT_PANEL_RAIL_WIDTH,
|
|
@@ -34,9 +37,14 @@ import {
|
|
|
34
37
|
formatElapsed,
|
|
35
38
|
formatResetIn,
|
|
36
39
|
readStoredPanelWidth,
|
|
40
|
+
readStoredSupervisor,
|
|
37
41
|
storeModel,
|
|
38
42
|
storePanelWidth,
|
|
43
|
+
storeSupervisor,
|
|
39
44
|
STUDIO_SESSION_STORAGE_KEY,
|
|
45
|
+
selectedModelRoles,
|
|
46
|
+
shortModelId,
|
|
47
|
+
supervisorFollowUp,
|
|
40
48
|
} from "./agent-client.js";
|
|
41
49
|
import "./agent-panel.css";
|
|
42
50
|
|
|
@@ -122,6 +130,18 @@ function PausedCard({ resetAt, onRetry, onSwitchModel }) {
|
|
|
122
130
|
</div>;
|
|
123
131
|
}
|
|
124
132
|
|
|
133
|
+
// The lineup the sidecar derived for the selected model (#717), read-only:
|
|
134
|
+
// what a turn may lean on beside it. Vision is named only when it is not the
|
|
135
|
+
// selected model itself.
|
|
136
|
+
function ModelRolesLine({ model, roles }) {
|
|
137
|
+
if (!roles) return null;
|
|
138
|
+
const parts = [[ko("helper", "보조"), roles.helper], [ko("advisor", "자문"), roles.advisor]];
|
|
139
|
+
if (roles.vision !== model) parts.push([ko("vision", "비전"), roles.vision]);
|
|
140
|
+
return <p className="agent-model-roles" data-agent-model-roles="true">
|
|
141
|
+
{parts.map(([label, key]) => `${label} · ${shortModelId(key)}`).join(" · ")}
|
|
142
|
+
</p>;
|
|
143
|
+
}
|
|
144
|
+
|
|
125
145
|
// A generation is a server-owned job. The card shows the state the runtime
|
|
126
146
|
// reported, the phase it named and only a progress value it actually sent.
|
|
127
147
|
function JobCard({ job, onStop, onAccept }) {
|
|
@@ -182,6 +202,47 @@ function ReceiptCard({ item }) {
|
|
|
182
202
|
</div>;
|
|
183
203
|
}
|
|
184
204
|
|
|
205
|
+
const SUPERVISOR_SEVERITY_COPY = { blocker: ko("blocker", "차단"), concern: ko("concern", "우려"), note: ko("note", "참고") };
|
|
206
|
+
const SUPERVISOR_STATUS_COPY = { done: ko("done", "완료"), partial: ko("partial", "일부"), refused: ko("refused", "거부됨"), missing: ko("missing", "누락") };
|
|
207
|
+
|
|
208
|
+
// The supervisor's note on a finished Studio turn (#728): muted, collapsed to
|
|
209
|
+
// its summary and counts, and never acted on by itself. Its one action puts
|
|
210
|
+
// the blockers and concerns into the composer as the author's next turn.
|
|
211
|
+
function SupervisorCard({ note, onSend, defaultOpen = false }) {
|
|
212
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
213
|
+
if (note.verdict !== "reviewed") return <div className="agent-card agent-supervisor-card" data-supervisor-verdict="unavailable">
|
|
214
|
+
<p className="agent-supervisor-summary">{ko("Supervisor review unavailable", "감독 검토를 할 수 없었어요")}{note.reason ? ` — ${note.reason}` : ""}</p>
|
|
215
|
+
</div>;
|
|
216
|
+
const followUp = supervisorFollowUp(note);
|
|
217
|
+
// Validated run_action drafts (#743), shown under the issue each one fixes.
|
|
218
|
+
const fixes = Array.isArray(note.fixes) ? note.fixes : [];
|
|
219
|
+
return <div className="agent-card agent-supervisor-card" data-supervisor-verdict="reviewed" data-open={open ? "true" : "false"}>
|
|
220
|
+
<div className="agent-supervisor-head">
|
|
221
|
+
<span className="agent-job-label">{ko("Supervisor", "감독")}</span>
|
|
222
|
+
{["blocker", "concern", "note"].filter((severity) => note.counts?.[severity] > 0).map((severity) => <span key={severity} className={`agent-job-badge agent-supervisor-badge ${severity}`} data-severity={severity}>{note.counts[severity]} {SUPERVISOR_SEVERITY_COPY[severity]}</span>)}
|
|
223
|
+
<button type="button" className="agent-ghost-button agent-supervisor-toggle" aria-expanded={open} onClick={() => setOpen((value) => !value)}>{open ? ko("Less", "접기") : ko("Details", "자세히")}</button>
|
|
224
|
+
</div>
|
|
225
|
+
<p className="agent-supervisor-summary">{note.summary}</p>
|
|
226
|
+
{open && note.items?.length > 0 && <ul className="agent-receipt-notes agent-supervisor-items">
|
|
227
|
+
{note.items.map((item, index) => <li key={index} data-status={item.status}>{SUPERVISOR_STATUS_COPY[item.status] ?? item.status} · {item.text}{item.evidence ? ` — ${item.evidence}` : ""}</li>)}
|
|
228
|
+
</ul>}
|
|
229
|
+
{open && note.issues?.length > 0 && <ul className="agent-receipt-notes agent-supervisor-issues">
|
|
230
|
+
{note.issues.map((issue, index) => {
|
|
231
|
+
const fix = fixes.find((entry) => entry.issueIndex === index);
|
|
232
|
+
return <li key={index} data-severity={issue.severity}>{SUPERVISOR_SEVERITY_COPY[issue.severity] ?? issue.severity} · {issue.text}{issue.evidence ? ` — ${issue.evidence}` : ""}
|
|
233
|
+
{fix && <code className="agent-supervisor-fix" data-fix-action={fix.action}>{`run_action ${JSON.stringify({ action: fix.action, args: fix.args })}`}</code>}
|
|
234
|
+
</li>;
|
|
235
|
+
})}
|
|
236
|
+
</ul>}
|
|
237
|
+
{followUp && <div className="agent-job-actions">
|
|
238
|
+
<button type="button" className="agent-ghost-button agent-supervisor-send" onClick={() => onSend(followUp)}
|
|
239
|
+
title={note.autofix?.eligible ? ko("Autofix is on: the blocker fixes would be applied as written", "자동 수정 켜짐: 차단 수정안을 적힌 그대로 적용합니다") : undefined}>
|
|
240
|
+
{fixes.length ? ko("Apply fixes as next prompt", "수정안 다음 턴으로 보내기") : ko("Send as next prompt", "다음 턴으로 보내기")}
|
|
241
|
+
</button>
|
|
242
|
+
</div>}
|
|
243
|
+
</div>;
|
|
244
|
+
}
|
|
245
|
+
|
|
185
246
|
const RECOVERY_COPY = {
|
|
186
247
|
none: "No recovery is available for this command.",
|
|
187
248
|
inspect: "Ask the agent to inspect the target before trying again.",
|
|
@@ -286,6 +347,11 @@ export default function AgentPanel({
|
|
|
286
347
|
const transport = useMemo(() => injectedTransport || createAgentTransport({ surface }), [injectedTransport, surface]);
|
|
287
348
|
const mockState = transport.mock ? transport.state : null;
|
|
288
349
|
|
|
350
|
+
// Inside the studio the title names the project's previs mode (#650).
|
|
351
|
+
const previsMode = useStudioShell()?.previsMode;
|
|
352
|
+
const titleMode = surface === "studio" && previsMode
|
|
353
|
+
? (previsMode === "storyboard" ? ko("Storyboard", "스토리보드") : ko("Animation", "애니메이션"))
|
|
354
|
+
: null;
|
|
289
355
|
const [collapsed, setCollapsed] = useState(defaultCollapsed);
|
|
290
356
|
// Only the dock owns a width; an embedded host sizes the panel itself and
|
|
291
357
|
// never reads the dock's stored key.
|
|
@@ -302,6 +368,8 @@ export default function AgentPanel({
|
|
|
302
368
|
const [modelProviders, setModelProviders] = useState([]);
|
|
303
369
|
const [model, setModel] = useState("");
|
|
304
370
|
const [modelsState, setModelsState] = useState("loading");
|
|
371
|
+
// The roles derived per main model (#717); shown, never sent or stored.
|
|
372
|
+
const [modelRoles, setModelRoles] = useState(null);
|
|
305
373
|
// null = the model's backend default; picking a model resets it.
|
|
306
374
|
const [effort, setEffort] = useState(null);
|
|
307
375
|
const efforts = useMemo(() => effortOptions(models.find((entry) => entry.id === model)), [models, model]);
|
|
@@ -309,6 +377,10 @@ export default function AgentPanel({
|
|
|
309
377
|
// remembered so a reload does not quietly move the next turn to another
|
|
310
378
|
// provider's model.
|
|
311
379
|
const chooseModel = useCallback((id) => { setModel(id); setEffort(null); storeModel(id); }, []);
|
|
380
|
+
// The supervisor switch (#728): a preference like the model pick. Off skips
|
|
381
|
+
// the post-turn review for every turn sent from this panel.
|
|
382
|
+
const [supervisorOn, setSupervisorOn] = useState(() => readStoredSupervisor());
|
|
383
|
+
const toggleSupervisor = useCallback(() => setSupervisorOn((current) => { storeSupervisor(!current); return !current; }), []);
|
|
312
384
|
// One place takes `{ providers, models }` from the sidecar: the grouped list
|
|
313
385
|
// the dropdown draws, the flat key-addressed list every lookup uses, and the
|
|
314
386
|
// model to open with. Returns false when nothing was advertised.
|
|
@@ -318,6 +390,7 @@ export default function AgentPanel({
|
|
|
318
390
|
const providers = Array.isArray(advertised?.providers) ? advertised.providers : [];
|
|
319
391
|
setModelProviders(providers);
|
|
320
392
|
setModels(list);
|
|
393
|
+
setModelRoles(advertised?.roles?.byMain ?? null);
|
|
321
394
|
// A saved or removed key arrives through this same path, so a selection the
|
|
322
395
|
// panel opened with is dropped here the moment its provider cannot run it.
|
|
323
396
|
setModel((current) => nextSelectedModel(providers, list, current));
|
|
@@ -345,6 +418,7 @@ export default function AgentPanel({
|
|
|
345
418
|
const [restoreReady, setRestoreReady] = useState(surface !== "studio");
|
|
346
419
|
|
|
347
420
|
const composerRef = useRef(null);
|
|
421
|
+
const focusPendingRef = useRef(false);
|
|
348
422
|
const transcriptRef = useRef(null);
|
|
349
423
|
|
|
350
424
|
// The host's context builder and image-action handler are read through refs
|
|
@@ -382,10 +456,12 @@ export default function AgentPanel({
|
|
|
382
456
|
const payload = await transport.loadSession(stored);
|
|
383
457
|
if (!Array.isArray(payload?.transcript)) throw new Error("missing transcript");
|
|
384
458
|
if (!cancelled) store.restore(payload.transcript, payload.sessionId || stored);
|
|
385
|
-
} catch {
|
|
459
|
+
} catch (error) {
|
|
386
460
|
if (!cancelled) {
|
|
387
461
|
store.newSession();
|
|
388
|
-
|
|
462
|
+
// A session id with no record behind it (the pane opened, nothing
|
|
463
|
+
// was ever sent) is nothing to restore, not a failure.
|
|
464
|
+
if (error?.status !== 404) setRestoreNotice("Previous conversation could not be restored");
|
|
389
465
|
}
|
|
390
466
|
} finally {
|
|
391
467
|
if (!cancelled) setRestoreReady(true);
|
|
@@ -399,7 +475,6 @@ export default function AgentPanel({
|
|
|
399
475
|
try { globalThis.localStorage?.setItem(STUDIO_SESSION_STORAGE_KEY, chat.sessionId); } catch { /* storage may be unavailable */ }
|
|
400
476
|
}, [chat.sessionId, restoreReady, surface]);
|
|
401
477
|
|
|
402
|
-
// --- activity line -----------------------------------------------------
|
|
403
478
|
// One line that always states what the panel is doing, ticking while a turn
|
|
404
479
|
// is live and holding the turn's outcome for a few seconds after it ends.
|
|
405
480
|
const [activityNow, setActivityNow] = useState(() => Date.now());
|
|
@@ -414,7 +489,6 @@ export default function AgentPanel({
|
|
|
414
489
|
return () => clearInterval(timer);
|
|
415
490
|
}, [activity.ticking]);
|
|
416
491
|
|
|
417
|
-
// --- session bootstrap -------------------------------------------------
|
|
418
492
|
// Two ways in (#379): the ChatGPT sign-in, or a provider key the sidecar
|
|
419
493
|
// already holds. `providersConfigured` counts the second, so a session that
|
|
420
494
|
// never signed in to ChatGPT still gets a composer instead of a sign-in wall
|
|
@@ -518,7 +592,6 @@ export default function AgentPanel({
|
|
|
518
592
|
store.send("Give me a wide two-shot of this scene", { attachFrame: false, model });
|
|
519
593
|
}, [authState, items.length, mockState, model, store]);
|
|
520
594
|
|
|
521
|
-
// --- layout ------------------------------------------------------------
|
|
522
595
|
useEffect(() => {
|
|
523
596
|
if (embedded) return;
|
|
524
597
|
const onResize = () => setOverlay((globalThis.innerWidth || 1440) < AGENT_PANEL_OVERLAY_BREAKPOINT);
|
|
@@ -555,7 +628,10 @@ export default function AgentPanel({
|
|
|
555
628
|
// Focus moves to the composer whenever the panel opens. The composer only
|
|
556
629
|
// mounts once the session resolves, so authState is a dependency too:
|
|
557
630
|
// otherwise this fires against a composer that does not exist yet. A hidden
|
|
558
|
-
// embedded panel never pulls focus out of the host's own surface.
|
|
631
|
+
// embedded panel never pulls focus out of the host's own surface. An
|
|
632
|
+
// embedded panel never collapses itself (its host hides it), so shown means
|
|
633
|
+
// open there: without that the Studio composer opened unfocused and the
|
|
634
|
+
// next Cmd+V landed on the stage.
|
|
559
635
|
useEffect(() => {
|
|
560
636
|
if (hidden) {
|
|
561
637
|
// Chrome blurs a hidden focused field only on its next lifecycle step;
|
|
@@ -563,8 +639,12 @@ export default function AgentPanel({
|
|
|
563
639
|
if (composerRef.current && composerRef.current === document.activeElement) composerRef.current.blur();
|
|
564
640
|
return;
|
|
565
641
|
}
|
|
566
|
-
if (!collapsed)
|
|
567
|
-
|
|
642
|
+
if (!(embedded || !collapsed)) return;
|
|
643
|
+
composerRef.current?.focus();
|
|
644
|
+
// A composer still waiting on the model list is disabled and refuses
|
|
645
|
+
// focus; the open is remembered so it lands once the composer enables.
|
|
646
|
+
focusPendingRef.current = document.activeElement !== composerRef.current;
|
|
647
|
+
}, [authState, collapsed, embedded, hidden]);
|
|
568
648
|
|
|
569
649
|
useEffect(() => {
|
|
570
650
|
const node = transcriptRef.current;
|
|
@@ -595,12 +675,13 @@ export default function AgentPanel({
|
|
|
595
675
|
});
|
|
596
676
|
}, []);
|
|
597
677
|
|
|
598
|
-
// --- pasted and dropped pictures ---------------------------------------
|
|
599
678
|
// A screenshot is the fastest thing an author can show the agent, so the
|
|
600
679
|
// composer takes one from the clipboard or the desktop. Only a transfer that
|
|
601
680
|
// actually carries a picture is intercepted: a text paste still lands in the
|
|
602
681
|
// caret, which is the whole reason the studio's document handler steps aside
|
|
603
|
-
// for a textarea in the first place.
|
|
682
|
+
// for a textarea in the first place. The paste is taken on the composer
|
|
683
|
+
// block, not the textarea, so the Send and model buttons a click just left
|
|
684
|
+
// focus on do not hand the picture to the stage.
|
|
604
685
|
const attachFiles = useCallback(async (files) => {
|
|
605
686
|
const prepared = [];
|
|
606
687
|
let failed = 0;
|
|
@@ -610,7 +691,6 @@ export default function AgentPanel({
|
|
|
610
691
|
const rejected = prepared.length ? store.addAttachments(prepared).rejected : 0;
|
|
611
692
|
setAttachNotice(rejected ? ATTACHMENT_LIMIT_NOTICE : failed ? ATTACHMENT_FAILED_NOTICE : null);
|
|
612
693
|
}, [store]);
|
|
613
|
-
/** Whether this transfer belongs to the composer. */
|
|
614
694
|
const takeTransfer = useCallback((transfer) => {
|
|
615
695
|
const { images, unsupported } = attachmentFilesFromTransfer(transfer);
|
|
616
696
|
if (!images.length) {
|
|
@@ -641,12 +721,11 @@ export default function AgentPanel({
|
|
|
641
721
|
if (takeTransfer(event.dataTransfer)) event.preventDefault();
|
|
642
722
|
}, [takeTransfer]);
|
|
643
723
|
|
|
644
|
-
// --- turn --------------------------------------------------------------
|
|
645
724
|
const runTurn = useCallback((text) => {
|
|
646
725
|
setAttachNotice(null);
|
|
647
726
|
setSteerNotice("");
|
|
648
|
-
return store.send(text, { attachFrame, model, effort: effort ?? undefined });
|
|
649
|
-
}, [attachFrame, effort, model, store]);
|
|
727
|
+
return store.send(text, { attachFrame, model, effort: effort ?? undefined, supervisor: supervisorOn });
|
|
728
|
+
}, [attachFrame, effort, model, store, supervisorOn]);
|
|
650
729
|
const stopTurn = useCallback(() => store.stop(), [store]);
|
|
651
730
|
// Steering the turn that is already running: the composer's text joins THAT
|
|
652
731
|
// turn instead of starting a second one. A refusal (the turn ended while the
|
|
@@ -715,7 +794,6 @@ export default function AgentPanel({
|
|
|
715
794
|
}
|
|
716
795
|
}, [store, transport]);
|
|
717
796
|
|
|
718
|
-
// --- provider keys -----------------------------------------------------
|
|
719
797
|
// The section is read when it opens; every write goes back through the
|
|
720
798
|
// refresh above, because a key changes the session, not just a row.
|
|
721
799
|
const toggleProviderKeys = useCallback(() => {
|
|
@@ -759,7 +837,7 @@ export default function AgentPanel({
|
|
|
759
837
|
stopTurn();
|
|
760
838
|
return;
|
|
761
839
|
}
|
|
762
|
-
if (event.key === "Enter" && !event.shiftKey) {
|
|
840
|
+
if (event.key === "Enter" && !isImeComposing(event) && !event.shiftKey) {
|
|
763
841
|
event.preventDefault();
|
|
764
842
|
// Enter does what the button under it says: Send, or Steer while a turn
|
|
765
843
|
// on a steerable surface is still running.
|
|
@@ -782,6 +860,13 @@ export default function AgentPanel({
|
|
|
782
860
|
// can send to: the composer stays shut instead of submitting a turn the
|
|
783
861
|
// provider would refuse.
|
|
784
862
|
const composerDisabled = panelState === "rate-limited" || !model || !modelIsSelectable(modelProviders, model);
|
|
863
|
+
useEffect(() => {
|
|
864
|
+
if (hidden) focusPendingRef.current = false;
|
|
865
|
+
if (hidden || composerDisabled || !focusPendingRef.current) return;
|
|
866
|
+
focusPendingRef.current = false;
|
|
867
|
+
// Only claim focus nobody else has taken in the meantime.
|
|
868
|
+
if (!document.activeElement || document.activeElement === document.body) composerRef.current?.focus();
|
|
869
|
+
}, [composerDisabled, hidden]);
|
|
785
870
|
|
|
786
871
|
if (collapsed && !embedded) {
|
|
787
872
|
return <aside className="agent-panel collapsed" data-agent-state={panelState} data-agent-collapsed="true" aria-label="Agent panel, collapsed">
|
|
@@ -819,7 +904,7 @@ export default function AgentPanel({
|
|
|
819
904
|
|
|
820
905
|
<header className="agent-header">
|
|
821
906
|
<StatusDot tone={statusTone} title={panelState} />
|
|
822
|
-
<h2 className="agent-title">Agent</h2>
|
|
907
|
+
<h2 className="agent-title" data-previs-mode={titleMode ? previsMode : undefined}>{titleMode ? `Agent · ${titleMode}` : "Agent"}</h2>
|
|
823
908
|
<span className="agent-header-spacer" />
|
|
824
909
|
<button type="button" className="agent-icon-button agent-new" aria-label="New conversation" title="New conversation" onClick={newSession}><FiPlus size={14} aria-hidden="true" /></button>
|
|
825
910
|
{presentation.history && <span className="agent-history-wrap">
|
|
@@ -904,6 +989,7 @@ export default function AgentPanel({
|
|
|
904
989
|
if (item.kind === "job") return <div className="agent-row" key={item.id}><JobCard job={item} onStop={stopTurn} onAccept={(job) => store.acceptJob(job.jobId)} /></div>;
|
|
905
990
|
if (item.kind === "receipt") return <div className="agent-row" key={item.id}><ReceiptCard item={item} /></div>;
|
|
906
991
|
if (item.kind === "failure") return <div className="agent-row" key={item.id}><FailureCard failure={item.failure} onRetry={() => runTurn(chat.lastPrompt)} /></div>;
|
|
992
|
+
if (item.kind === "supervisor") return <div className="agent-row" key={item.id}><SupervisorCard note={item} onSend={(text) => { store.setDraft(text); runTurn(text); }} /></div>;
|
|
907
993
|
return <div className="agent-row" key={item.id}><ImageResultCard image={item} onUse={(image) => store.applyImage(image.id)} onUndo={(image) => store.undoImage(image.id)} onRegenerate={() => runTurn(chat.lastPrompt)} onOpen={setLightbox} /></div>;
|
|
908
994
|
})}
|
|
909
995
|
|
|
@@ -923,6 +1009,7 @@ export default function AgentPanel({
|
|
|
923
1009
|
onDragOver={composerDisabled ? undefined : onComposerDragOver}
|
|
924
1010
|
onDragLeave={composerDisabled ? undefined : onComposerDragLeave}
|
|
925
1011
|
onDrop={composerDisabled ? undefined : onComposerDrop}
|
|
1012
|
+
onPaste={composerDisabled ? undefined : onComposerPaste}
|
|
926
1013
|
>
|
|
927
1014
|
{pendingAttachments.length > 0 && <ul className="agent-attachments" aria-label="Attached images">
|
|
928
1015
|
{pendingAttachments.map((attachment) => <li key={attachment.id} className="agent-attachment">
|
|
@@ -931,8 +1018,6 @@ export default function AgentPanel({
|
|
|
931
1018
|
</li>)}
|
|
932
1019
|
</ul>}
|
|
933
1020
|
{attachNotice && <p className="agent-attachment-notice" role="status">{attachNotice}</p>}
|
|
934
|
-
{/* One field: the message, and under it on the same surface the
|
|
935
|
-
frame toggle, the model and effort picks, and Send. */}
|
|
936
1021
|
<div className="agent-field">
|
|
937
1022
|
<textarea
|
|
938
1023
|
ref={composerRef}
|
|
@@ -943,7 +1028,6 @@ export default function AgentPanel({
|
|
|
943
1028
|
disabled={composerDisabled}
|
|
944
1029
|
onChange={(event) => store.setDraft(event.target.value)}
|
|
945
1030
|
onKeyDown={onComposerKeyDown}
|
|
946
|
-
onPaste={onComposerPaste}
|
|
947
1031
|
onDragOver={onComposerDragOver}
|
|
948
1032
|
onDrop={onComposerDrop}
|
|
949
1033
|
/>
|
|
@@ -972,6 +1056,11 @@ export default function AgentPanel({
|
|
|
972
1056
|
{efforts.map((value, index) => <option key={value} value={value} title={index === 0 ? "Default for this model" : undefined}>{value}</option>)}
|
|
973
1057
|
</select>
|
|
974
1058
|
)}
|
|
1059
|
+
{surface === "studio" && (
|
|
1060
|
+
<button type="button" className={`agent-supervisor-toggle${supervisorOn ? " is-on" : ""}`} role="switch" aria-checked={supervisorOn} aria-label={ko("Supervisor review", "감독 검토")} title={ko(supervisorOn ? "Supervisor review is on: a note arrives after each editing turn" : "Supervisor review is off", supervisorOn ? "감독 검토 켜짐: 편집 턴이 끝나면 메모가 옵니다" : "감독 검토 꺼짐")} onClick={toggleSupervisor}>
|
|
1061
|
+
{ko("Supervisor", "감독")} {supervisorOn ? "ON" : "OFF"}
|
|
1062
|
+
</button>
|
|
1063
|
+
)}
|
|
975
1064
|
<span className="agent-composer-spacer" aria-hidden="true" />
|
|
976
1065
|
{surface === "studio" && i2vActionRef.current && <button
|
|
977
1066
|
type="button"
|
|
@@ -993,6 +1082,7 @@ export default function AgentPanel({
|
|
|
993
1082
|
</>
|
|
994
1083
|
: <button type="button" className="agent-send" aria-label="Send" title="Send (Enter)" disabled={composerDisabled || !draft.trim()} onClick={() => runTurn(draft)}><FiArrowUp size={14} aria-hidden="true" /></button>}
|
|
995
1084
|
</div>
|
|
1085
|
+
<ModelRolesLine model={model} roles={selectedModelRoles(modelRoles, model)} />
|
|
996
1086
|
</div>
|
|
997
1087
|
</div>}
|
|
998
1088
|
{/* The hint exists to warn about image spend; a surface that cannot
|
|
@@ -14,7 +14,7 @@ import { Toaster, toast } from "react-hot-toast";
|
|
|
14
14
|
import { loadWorkflowGraph, normalizeWorkflowGraph, storeWorkflowGraph, WORKFLOW_STORAGE_KEY } from "../project.js";
|
|
15
15
|
import AgentPanel from "./AgentPanel.jsx";
|
|
16
16
|
import CozySceneNode from "./CozySceneNode.jsx";
|
|
17
|
-
import { applyCozyScenePatch, normalizeCozySceneData, sceneConnectionAllowed
|
|
17
|
+
import { applyCozyScenePatch, normalizeCozySceneData, sceneConnectionAllowed } from "./cozy-scene-node.js";
|
|
18
18
|
import { activeSceneCharacters, characterHandleId, characterIdFromHandle, normalizeMotionInputData, motionInputOutput } from "./motion-input.js";
|
|
19
19
|
import { DEFAULT_NODE_SCHEMAS, defaultFormValues, schemaCategoryForType, schemaModelEntries, schemaProperties } from "./node-schema.js";
|
|
20
20
|
import { executeLocalWorkflowGraph } from "./local-workflow.js";
|