cozyclay 1.10.0 → 2.0.2
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 +193 -0
- package/bin/agent/agent-routes.mjs +82 -72
- package/bin/agent/agent-runner.mjs +10 -3
- package/bin/agent/motion-runtime.mjs +32 -260
- package/bin/agent/providers.mjs +46 -3
- package/bin/agent/session-store.mjs +41 -5
- package/bin/agent/studio-prompt.mjs +1 -1
- package/bin/agent/studio-tools.mjs +48 -11
- package/bin/codex-auth.mjs +2 -1
- package/bin/cozyclay.mjs +38 -11
- package/bin/live/cli.mjs +22 -3
- package/bin/mcp-runtime.mjs +45 -20
- package/bin/telemetry-state.mjs +5 -2
- package/bin/update-check.mjs +15 -0
- package/dist/app/index.html +6 -6
- package/dist/assets/analytics-MVZOEW8O.js +1 -0
- package/dist/assets/app-BxBLC6GT.js +4876 -0
- package/dist/assets/app-CJdxvnrl.css +1 -0
- package/dist/assets/{demo-B-eg1CT8.js → demo-ByeYp-dY.js} +1 -1
- package/dist/assets/{first-shot-handoff-CGgnvNu8.js → first-shot-handoff-DgT28ZiV.js} +1 -1
- package/dist/assets/{landing-IbRp3FnQ.js → landing-BaXkpFip.js} +1 -1
- package/dist/assets/shot-prompt-CbqWcWYi.css +1 -0
- package/dist/assets/shot-prompt-hN0j_O-p.js +17 -0
- package/dist/assets/{ticket-BgIzOBOf.js → ticket-BcmsnHn5.js} +1 -1
- package/dist/assets/workflow-B5XZwVfe.js +179 -0
- package/dist/assets/workflow-DRdwoDBT.css +1 -0
- package/dist/cozyclay-package.json +1 -1
- package/dist/fonts/IBM-Plex-Sans-OFL.txt +92 -0
- package/dist/fonts/JetBrains-Mono-OFL.txt +93 -0
- package/dist/fonts/README.md +13 -9
- package/dist/fonts/ibm-plex-sans-400-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-500-latin.woff2 +0 -0
- package/dist/fonts/ibm-plex-sans-600-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-400-latin.woff2 +0 -0
- package/dist/fonts/jetbrains-mono-500-latin.woff2 +0 -0
- package/dist/index.html +7 -7
- package/dist/privacy/index.html +13 -8
- package/dist/sitemap.xml +2 -2
- package/dist/workflow/index.html +5 -5
- package/mcp/live-hub.mjs +35 -93
- package/mcp/mesh-file.mjs +48 -0
- package/mcp/server.mjs +6 -77
- package/mcp/tool-handlers.mjs +219 -376
- package/package.json +2 -1
- package/src/App.jsx +1275 -9086
- package/src/analytics.js +393 -9
- package/src/app-context.js +211 -0
- package/src/app-stage.jsx +25 -24
- package/src/ardy/auto-fix-panel.css +108 -0
- package/src/ardy/fix-collisions.js +10 -66
- package/src/ardy/ik-drag.js +101 -0
- package/src/ardy/ik-key-json.js +43 -0
- package/src/ardy/ik.js +97 -67
- package/src/ardy/physics-panel.css +2 -0
- package/src/ardy/physics-panel.jsx +17 -5
- package/src/ardy/platform-fit-panel.css +64 -0
- package/src/ardy/platform-fit-panel.jsx +40 -0
- package/src/ardy/platform-fit.js +326 -0
- package/src/ardy/playback.js +166 -20
- package/src/ardy/range-pin.js +299 -0
- package/src/ardy/timeline-coordinates.js +26 -0
- package/src/ardy/timeline.css +1214 -0
- package/src/ardy/timeline.jsx +152 -41
- package/src/ardy/waypoints.js +3 -3
- package/src/asset-pane.css +462 -0
- package/src/asset-pane.jsx +461 -232
- package/src/camera-tutorial.jsx +38 -18
- package/src/command-bus.js +349 -0
- package/src/commands/ai.js +31 -0
- package/src/commands/cast.js +140 -0
- package/src/commands/elements/character.js +13 -0
- package/src/commands/elements/motion.js +14 -0
- package/src/commands/elements/object.js +22 -0
- package/src/commands/elements/scene.js +16 -0
- package/src/commands/elements/shot.js +9 -0
- package/src/commands/elements/stage.js +11 -0
- package/src/commands/elements.js +155 -0
- package/src/commands/export.js +20 -0
- package/src/commands/index.js +40 -0
- package/src/commands/motion.js +231 -0
- package/src/commands/objects.js +174 -0
- package/src/commands/project.js +53 -0
- package/src/commands/scene.js +81 -0
- package/src/commands/shared.js +19 -0
- package/src/commands/shot.js +103 -0
- package/src/commands/stage.js +17 -0
- package/src/commands/view.js +42 -0
- package/src/document-store.js +144 -0
- package/src/domains/cast.js +1007 -0
- package/src/domains/motion.js +3219 -0
- package/src/domains/objects.js +932 -0
- package/src/domains/scenes.js +823 -0
- package/src/domains/shots.js +404 -0
- package/src/domains/stage.js +93 -0
- package/src/dualview.jsx +113 -57
- package/src/facing-marks.js +3 -0
- package/src/first-success-guide.jsx +14 -14
- package/src/grid-view.js +18 -5
- package/src/hierarchy-panel.css +386 -0
- package/src/hierarchy-panel.jsx +88 -19
- package/src/{fal-motion-client.js → i2v-motion-client.js} +13 -13
- package/src/i2v-motion-studio.jsx +180 -0
- package/src/ik-camera.js +3 -0
- package/src/main.jsx +6 -0
- package/src/motion/generation.js +105 -0
- package/src/motion-readiness-ui.jsx +4 -4
- package/src/motion-readiness.js +11 -0
- package/src/motion-trail.js +366 -9
- package/src/object-gizmo.jsx +7 -1
- package/src/panels/CameraPanel.jsx +60 -0
- package/src/panels/CharacterTransformPanel.jsx +48 -0
- package/src/panels/EnvironmentPanel.jsx +40 -0
- package/src/panels/Foldout.jsx +32 -0
- package/src/panels/LightPanel.jsx +24 -0
- package/src/panels/ObjectTransformPanel.jsx +554 -0
- package/src/panels/PosePanel.jsx +97 -0
- package/src/panels/ProjectPanel.jsx +33 -0
- package/src/panels/PromptBlocksPanel.jsx +442 -0
- package/src/panels/PropsPanel.jsx +70 -0
- package/src/panels/ReferenceImageField.jsx +91 -0
- package/src/panels/RigControlPanel.jsx +78 -0
- package/src/panels/RigPanel.jsx +37 -0
- package/src/panels/SubjectBox.jsx +47 -0
- package/src/panels/SubjectsPanel.jsx +39 -0
- package/src/panels/VideoCapturePanel.jsx +180 -0
- package/src/panels/details.css +1171 -0
- package/src/panels/motion.css +197 -0
- package/src/panels/pose.css +39 -0
- package/src/planview.jsx +60 -31
- package/src/posestudio.jsx +59 -4
- package/src/project-browser.css +1073 -0
- package/src/project-browser.jsx +288 -123
- package/src/range-pin-object-transform.js +19 -0
- package/src/range-pin-panel.css +518 -0
- package/src/range-pin-panel.jsx +344 -0
- package/src/result-modal.jsx +5 -5
- package/src/room.jsx +79 -51
- package/src/scenes.js +9 -8
- package/src/semantic-edit.js +2 -0
- package/src/settings-menu.jsx +39 -105
- package/src/shell/BottomDock.jsx +450 -0
- package/src/shell/DetailsSlot.jsx +513 -0
- package/src/shell/LibrarySlot.jsx +86 -0
- package/src/shell/MenuBar.jsx +601 -0
- package/src/shell/OutlinerSlot.jsx +50 -0
- package/src/shell/PreferencesDialog.jsx +538 -0
- package/src/shell/PreferencesSlot.jsx +40 -0
- package/src/shell/StatusBar.jsx +125 -0
- package/src/shell/StudioShell.jsx +92 -0
- package/src/shell/TopBar.jsx +129 -0
- package/src/shell/ViewportToolbar.jsx +656 -0
- package/src/shell/agent-glass.css +296 -0
- package/src/shell/dock.css +93 -0
- package/src/shell/glass-regions.css +985 -0
- package/src/shell/glass.css +687 -0
- package/src/shell/log-store.js +52 -0
- package/src/shell/mode.css +9 -0
- package/src/shell/preferences.css +651 -0
- package/src/shell/shell.css +282 -0
- package/src/shell/studio-shell-context.js +19 -0
- package/src/shell/topbar.css +435 -0
- package/src/shell/viewport.css +451 -0
- package/src/store/authored-intent.js +22 -0
- package/src/store/runtime-adapters.js +92 -0
- package/src/store/scene-stage.js +21 -0
- package/src/store/use-document-store.js +14 -0
- package/src/studio-actions.js +210 -0
- package/src/studio-agent-commands.js +38 -268
- package/src/studio-agent-context.js +38 -9
- package/src/studio-agent-motion.js +122 -378
- package/src/studio-agent-protocol.js +66 -30
- package/src/studio-app-binding.js +379 -0
- package/src/studio-contact-sheet.js +73 -0
- package/src/studio-elements.js +58 -51
- package/src/styles/themes.css +200 -0
- package/src/styles/tokens.css +69 -0
- package/src/styles.css +456 -1748
- package/src/theme.js +44 -0
- package/src/timeline-extent.js +16 -0
- package/src/timeline-fps.js +13 -0
- package/src/trail-key-conflicts.js +39 -0
- package/src/trail-pick.js +59 -0
- package/src/ui.jsx +8 -8
- package/src/use-case-question.jsx +66 -0
- package/src/workflow/AgentPanel.jsx +86 -71
- package/src/workflow/agent-client.js +12 -2
- package/src/workflow/agent-panel.css +34 -32
- package/src/workflow/cozy-scene-node.css +2 -0
- package/src/workflow/workflow.css +2 -0
- package/tools/ardy/bridge.mjs +164 -146
- package/tools/ardy/visual-qa.mjs +5 -5
- package/tools/bench/EXP3.md +61 -0
- package/tools/bench/cclay_bench_extract_incam.py +125 -0
- package/tools/bench/cclay_bench_extract_obs.py +204 -0
- package/tools/bench/cclay_bench_runner.py +42 -0
- package/tools/bench/cube-contact.mjs +162 -0
- package/tools/bench/exp3.mjs +152 -0
- package/tools/bench/extract-bench-lib.mjs +216 -0
- package/tools/bench/extract-bench.mjs +302 -0
- package/tools/bench/fal-generate.mjs +88 -0
- package/tools/bench/fit/README.md +189 -0
- package/tools/bench/fit/camera.mjs +59 -0
- package/tools/bench/fit/contact.mjs +419 -0
- package/tools/bench/fit/footlock.mjs +234 -0
- package/tools/bench/fit/motion.mjs +85 -0
- package/tools/bench/fit/pin.mjs +27 -0
- package/tools/bench/fit/remote.mjs +105 -0
- package/tools/bench/fit-bench.mjs +126 -0
- package/tools/bench/fit-sanity.mjs +45 -0
- package/tools/bench/metrics.mjs +313 -0
- package/tools/bench/obs/depth.mjs +258 -0
- package/tools/bench/obs/extrinsics.mjs +175 -0
- package/tools/bench/obs/fit_mannequin_betas.py +340 -0
- package/tools/bench/obs/ground.mjs +294 -0
- package/tools/bench/obs/heading.mjs +151 -0
- package/tools/bench/obs/ladder.mjs +555 -0
- package/tools/bench/obs/mannequin-betas.json +124 -0
- package/tools/bench/obs/remote.mjs +83 -0
- package/tools/bench/obs/rest_joints.py +52 -0
- package/tools/bench/obs/ybot-targets.mjs +49 -0
- package/tools/bench/obs-bench.mjs +520 -0
- package/tools/bench/score.mjs +343 -0
- package/tools/bench/summarize.mjs +86 -0
- package/tools/dev/pages/privacy.html +13 -8
- package/tools/gt-render/browser.mjs +159 -0
- package/tools/gt-render/camera-math.mjs +276 -0
- package/tools/gt-render/page.mjs +267 -0
- package/tools/gt-render/render.mjs +514 -0
- package/tools/gt-render/scene-box.mjs +31 -0
- package/tools/gt-render/take-transform.mjs +87 -0
- package/tools/qa/css-rule-usage.mjs +283 -0
- package/tools/qa/studio-control-count.mjs +70 -32
- package/tools/run-tests.mjs +60 -3
- package/tools/track/DESIGN.md +513 -0
- package/tools/track/backfill-provenance.mjs +116 -0
- package/tools/track/budget.mjs +8 -0
- package/tools/track/check-rig.mjs +209 -0
- package/tools/track/diagnostics.schema.json +60 -0
- package/tools/track/export-rig.mjs +256 -0
- package/tools/track/fallback.mjs +7 -0
- package/tools/track/fk-parity-fixture.mjs +161 -0
- package/tools/track/gate.mjs +349 -0
- package/tools/track/masks.mjs +318 -0
- package/tools/track/metrics.mjs +201 -0
- package/tools/track/publish-obs.mjs +108 -0
- package/tools/track/py/check_env.py +61 -0
- package/tools/track/py/eval_lr.py +229 -0
- package/tools/track/py/lr_viterbi.py +294 -0
- package/tools/track/py/masks.py +405 -0
- package/tools/track/py/objective.py +382 -0
- package/tools/track/py/rig.py +508 -0
- package/tools/track/py/scene.py +326 -0
- package/tools/track/py/test_joint_indices.py +123 -0
- package/tools/track/py/test_lr_viterbi.py +195 -0
- package/tools/track/py/test_masks.py +229 -0
- package/tools/track/py/test_rig.py +288 -0
- package/tools/track/py/test_scene.py +354 -0
- package/tools/track/py/test_track.py +328 -0
- package/tools/track/py/track.py +411 -0
- package/tools/track/remote.mjs +140 -0
- package/tools/track/rig-dump.mjs +227 -0
- package/tools/track/run-box-tests.mjs +14 -0
- package/tools/track/run-box.mjs +168 -0
- package/tools/track/setup-box.sh +59 -0
- package/tools/track/study-2d.mjs +728 -0
- package/dist/assets/analytics-B1hnH66c.js +0 -1
- package/dist/assets/app-Cu1cexZR.css +0 -1
- package/dist/assets/app-Dxdh8kds.js +0 -4861
- package/dist/assets/shot-prompt-C_g2BHVc.css +0 -1
- package/dist/assets/shot-prompt-HVJRP4Gr.js +0 -17
- package/dist/assets/workflow-CJd-PDtV.js +0 -179
- package/dist/assets/workflow-DsudxKHF.css +0 -1
- package/src/fal-motion-studio.jsx +0 -180
- package/src/scene-history.js +0 -129
- package/tools/ardy/__pycache__/cclay_gvhmr_worker.cpython-313.pyc +0 -0
package/src/theme.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Studio colour theme (#570). The choice lives on <html data-theme>, so the
|
|
2
|
+
// shell, its regions and body-level popovers all read the same token set.
|
|
3
|
+
export const THEMES = ["dark", "light"];
|
|
4
|
+
export const THEME_KEY = "cozyclay.theme.v1";
|
|
5
|
+
export const DEFAULT_THEME = "light";
|
|
6
|
+
|
|
7
|
+
import { useEffect, useState } from "react";
|
|
8
|
+
|
|
9
|
+
/** The theme on <html data-theme>, following Preferences' live switch. */
|
|
10
|
+
export function useUiTheme() {
|
|
11
|
+
const [theme, setTheme] = useState(() => globalThis.document?.documentElement.dataset.theme ?? readTheme());
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const onChange = (event) => setTheme(event.detail);
|
|
14
|
+
globalThis.addEventListener("cozyclay:theme-change", onChange);
|
|
15
|
+
return () => globalThis.removeEventListener("cozyclay:theme-change", onChange);
|
|
16
|
+
}, []);
|
|
17
|
+
return theme;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function readTheme() {
|
|
21
|
+
try {
|
|
22
|
+
const stored = globalThis.localStorage?.getItem(THEME_KEY);
|
|
23
|
+
return THEMES.includes(stored) ? stored : DEFAULT_THEME;
|
|
24
|
+
} catch {
|
|
25
|
+
return DEFAULT_THEME;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function applyTheme(theme) {
|
|
30
|
+
const next = THEMES.includes(theme) ? theme : DEFAULT_THEME;
|
|
31
|
+
document.documentElement.dataset.theme = next;
|
|
32
|
+
return next;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function saveTheme(theme) {
|
|
36
|
+
const next = applyTheme(theme);
|
|
37
|
+
try {
|
|
38
|
+
globalThis.localStorage?.setItem(THEME_KEY, next);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.warn("[cozyclay] theme not saved:", error?.name ?? error);
|
|
41
|
+
}
|
|
42
|
+
globalThis.dispatchEvent?.(new CustomEvent("cozyclay:theme-change", { detail: next }));
|
|
43
|
+
return next;
|
|
44
|
+
}
|
package/src/timeline-extent.js
CHANGED
|
@@ -37,3 +37,19 @@ export function timelineContentExtent(
|
|
|
37
37
|
? authoredExtent
|
|
38
38
|
: Number.isFinite(extraFrames) ? extraFrames : 0;
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Return the frame count the shared timeline shows for a content `extent`.
|
|
43
|
+
*
|
|
44
|
+
* It never ends under an authored shot: a shot outside the timeline is an
|
|
45
|
+
* invalid scene for the Studio agent and for playback, so the count is at
|
|
46
|
+
* least every shot's inclusive `endFrame + 1`. A zero extent keeps
|
|
47
|
+
* `currentCount` (the existing authored duration), raised only by a shot.
|
|
48
|
+
*/
|
|
49
|
+
export function timelineSpan(extent, shots = [], currentCount = 0) {
|
|
50
|
+
const shotEnd = shots.reduce(
|
|
51
|
+
(max, shot) => Math.max(max, Number.isFinite(shot?.endFrame) ? shot.endFrame + 1 : 0),
|
|
52
|
+
0,
|
|
53
|
+
);
|
|
54
|
+
return Math.max(extent > 0 ? extent : currentCount, shotEnd);
|
|
55
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// The timeline clock, on its own so the agent sidecar (bin/agent) can read it
|
|
2
|
+
// without importing scenes.js, whose scene-object and motion-trail imports pull
|
|
3
|
+
// in `three` — a devDependency the npm package does not ship
|
|
4
|
+
// (test/verify-package-agent-imports.mjs).
|
|
5
|
+
|
|
6
|
+
/** v3 and older authored every frame number on ARDY's 20 fps clock; v4 reads
|
|
7
|
+
* them on the 24 fps production clock. The numbers are MULTIPLIED, never
|
|
8
|
+
* reinterpreted: a waypoint at frame 40 meant 2.0 s and must still mean 2.0 s,
|
|
9
|
+
* which is frame 48 — reinterpreting it would silently speed the scene up. */
|
|
10
|
+
export const LEGACY_FRAME_FPS = 20;
|
|
11
|
+
export const TIMELINE_FRAME_FPS = 24;
|
|
12
|
+
export const toTimelineFrame = (frame) =>
|
|
13
|
+
Math.round((frame * TIMELINE_FRAME_FPS) / LEGACY_FRAME_FPS);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const CHAIN_BY_EFFECTOR = Object.freeze({
|
|
2
|
+
LeftHand: "leftHand",
|
|
3
|
+
RightHand: "rightHand",
|
|
4
|
+
LeftFoot: "leftFoot",
|
|
5
|
+
RightFoot: "rightFoot",
|
|
6
|
+
});
|
|
7
|
+
const EFFECTOR_BY_CHAIN = Object.freeze({
|
|
8
|
+
leftHand: "LeftHand",
|
|
9
|
+
rightHand: "RightHand",
|
|
10
|
+
leftFoot: "LeftFoot",
|
|
11
|
+
rightFoot: "RightFoot",
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
function trackIds(track) {
|
|
15
|
+
const id = typeof track === "string" ? track : track?.id ?? track?.joint ?? track?.effector;
|
|
16
|
+
const chain = typeof track === "object" ? track?.chain : null;
|
|
17
|
+
return new Set([id, chain, CHAIN_BY_EFFECTOR[id], EFFECTOR_BY_CHAIN[id]].filter(Boolean));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function entryFor(entries, id) {
|
|
21
|
+
return typeof entries?.get === "function" ? entries.get(id) : entries?.[id];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Return absolute IK-key frames that can override a trail edit. */
|
|
25
|
+
export function findAbsoluteIkKeyConflicts({ keys, track, startFrame, endFrame }) {
|
|
26
|
+
const ids = trackIds(track);
|
|
27
|
+
const frames = [];
|
|
28
|
+
for (const [frame, entries] of keys ?? []) {
|
|
29
|
+
if (frame < startFrame || frame >= endFrame) continue;
|
|
30
|
+
for (const id of ids) {
|
|
31
|
+
const key = entryFor(entries, id);
|
|
32
|
+
if (key && key.baseQ == null) {
|
|
33
|
+
frames.push(frame);
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return [...new Set(frames)].sort((a, b) => a - b);
|
|
39
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Pure picking for motion-trail pointer rays. */
|
|
2
|
+
|
|
3
|
+
const FRAME_PENALTY = 0.001;
|
|
4
|
+
const TIE_DISTANCE = 0.01;
|
|
5
|
+
const MIN_FRAME_WINDOW = 12;
|
|
6
|
+
|
|
7
|
+
/** Pick the trail track/frame intended by a ray near the current playhead. */
|
|
8
|
+
export function pickTrailPoint({
|
|
9
|
+
tracks,
|
|
10
|
+
playheadFrame,
|
|
11
|
+
falloffFrames,
|
|
12
|
+
rayOrigin,
|
|
13
|
+
rayDirection,
|
|
14
|
+
maxDistance,
|
|
15
|
+
}) {
|
|
16
|
+
if (!Array.isArray(tracks) || !rayOrigin || !rayDirection) return null;
|
|
17
|
+
const directionLength = Math.hypot(rayDirection.x, rayDirection.y, rayDirection.z);
|
|
18
|
+
if (!Number.isFinite(directionLength) || directionLength === 0) return null;
|
|
19
|
+
const dx = rayDirection.x / directionLength;
|
|
20
|
+
const dy = rayDirection.y / directionLength;
|
|
21
|
+
const dz = rayDirection.z / directionLength;
|
|
22
|
+
const center = Number.isFinite(playheadFrame) ? playheadFrame : 0;
|
|
23
|
+
const radius = Math.max(2 * (Number.isFinite(falloffFrames) ? falloffFrames : 0), MIN_FRAME_WINDOW);
|
|
24
|
+
const limit = Number.isFinite(maxDistance) ? maxDistance : 0;
|
|
25
|
+
let best = null;
|
|
26
|
+
|
|
27
|
+
for (const track of tracks) {
|
|
28
|
+
const points = track?.flat;
|
|
29
|
+
if (!track?.id || !points || points.length < 3) continue;
|
|
30
|
+
const firstFrame = Math.max(0, Math.ceil(center - radius));
|
|
31
|
+
const lastFrame = Math.min(Math.floor(points.length / 3) - 1, Math.floor(center + radius));
|
|
32
|
+
for (let frame = firstFrame; frame <= lastFrame; frame += 1) {
|
|
33
|
+
const offset = frame * 3;
|
|
34
|
+
const px = points[offset] - rayOrigin.x;
|
|
35
|
+
const py = points[offset + 1] - rayOrigin.y;
|
|
36
|
+
const pz = points[offset + 2] - rayOrigin.z;
|
|
37
|
+
const depth = px * dx + py * dy + pz * dz;
|
|
38
|
+
if (depth < 0) continue;
|
|
39
|
+
const ox = px - depth * dx;
|
|
40
|
+
const oy = py - depth * dy;
|
|
41
|
+
const oz = pz - depth * dz;
|
|
42
|
+
const distance = Math.hypot(ox, oy, oz);
|
|
43
|
+
if (distance > limit) continue;
|
|
44
|
+
const score = distance + Math.abs(frame - center) * FRAME_PENALTY;
|
|
45
|
+
const scoreDelta = score - (best?.score ?? Infinity);
|
|
46
|
+
if (
|
|
47
|
+
!best
|
|
48
|
+
|| scoreDelta < -TIE_DISTANCE
|
|
49
|
+
|| (Math.abs(scoreDelta) <= TIE_DISTANCE && (
|
|
50
|
+
depth < best.depth
|
|
51
|
+
|| (depth === best.depth && score < best.score)
|
|
52
|
+
))
|
|
53
|
+
) {
|
|
54
|
+
best = { track: track.id, grabFrame: frame, score, depth };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return best ? { track: best.track, grabFrame: best.grabFrame } : null;
|
|
59
|
+
}
|
package/src/ui.jsx
CHANGED
|
@@ -247,7 +247,7 @@ export function Slider({ label, min, max, step, value, unit = "", onChange, comp
|
|
|
247
247
|
);
|
|
248
248
|
if (compact) {
|
|
249
249
|
return (
|
|
250
|
-
<div className="cslider">
|
|
250
|
+
<div className="cslider v2-details-compact-slider">
|
|
251
251
|
<div className={"cslider-head" + (softMax ? " scrub" : "")} {...scrubProps}>
|
|
252
252
|
<span>{label}</span>
|
|
253
253
|
<span className="val">
|
|
@@ -260,8 +260,8 @@ export function Slider({ label, min, max, step, value, unit = "", onChange, comp
|
|
|
260
260
|
);
|
|
261
261
|
}
|
|
262
262
|
return (
|
|
263
|
-
<div className="row">
|
|
264
|
-
<span>{label}</span>
|
|
263
|
+
<div className="row v2-details-row">
|
|
264
|
+
<span className="v2-details-row-label">{label}</span>
|
|
265
265
|
{input}
|
|
266
266
|
<span className="val">
|
|
267
267
|
{text}
|
|
@@ -273,7 +273,7 @@ export function Slider({ label, min, max, step, value, unit = "", onChange, comp
|
|
|
273
273
|
|
|
274
274
|
export function Field({ label, children }) {
|
|
275
275
|
return (
|
|
276
|
-
<div className="field">
|
|
276
|
+
<div className="field v2-details-field">
|
|
277
277
|
<label>{label}</label>
|
|
278
278
|
{children}
|
|
279
279
|
</div>
|
|
@@ -460,13 +460,13 @@ export function NumberField({ label, value, step, precision = 2, scrubRange, onC
|
|
|
460
460
|
|
|
461
461
|
return (
|
|
462
462
|
<span
|
|
463
|
-
className={`number-field${focused ? " editing" : ""}`}
|
|
463
|
+
className={`number-field v2-details-number-field${focused ? " editing" : ""}`}
|
|
464
464
|
title={title}
|
|
465
465
|
onPointerDown={onFieldPointerDown}
|
|
466
466
|
onPointerUp={endFieldDrag}
|
|
467
467
|
onPointerCancel={endFieldDrag}
|
|
468
468
|
>
|
|
469
|
-
<span className="axis">{label}</span>
|
|
469
|
+
<span className="axis" data-axis={label}>{label}</span>
|
|
470
470
|
<input
|
|
471
471
|
ref={inputRef}
|
|
472
472
|
type="text"
|
|
@@ -485,8 +485,8 @@ export function NumberField({ label, value, step, precision = 2, scrubRange, onC
|
|
|
485
485
|
// per axis. `fields` entries are `{ axis, value, step, precision, onChange }`.
|
|
486
486
|
export function Vector3Row({ label, fields }) {
|
|
487
487
|
return (
|
|
488
|
-
<div className="vec3-row">
|
|
489
|
-
<span className="vec3-label">{label}</span>
|
|
488
|
+
<div className="vec3-row v2-details-transform-row">
|
|
489
|
+
<span className="vec3-label v2-details-transform-label">{label}</span>
|
|
490
490
|
{fields.map((field) => (
|
|
491
491
|
<NumberField
|
|
492
492
|
key={field.axis}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
|
|
3
|
+
// Closed choices only (analytics USE_CASE_VALUES / TEAM_VALUES): no free text
|
|
4
|
+
// ever leaves the browser from this card.
|
|
5
|
+
const USE_CASES = Object.freeze([
|
|
6
|
+
["animation", "Animation", "애니메이션"],
|
|
7
|
+
["film", "Film / drama", "영화·드라마"],
|
|
8
|
+
["game", "Game", "게임"],
|
|
9
|
+
["ad_mv", "Ad / music video", "광고·뮤비"],
|
|
10
|
+
["personal", "Personal project", "개인 작업"],
|
|
11
|
+
["other", "Something else", "기타"],
|
|
12
|
+
]);
|
|
13
|
+
const TEAMS = Object.freeze([
|
|
14
|
+
["team", "With a team or company", "팀·회사에서"],
|
|
15
|
+
["solo", "On my own", "혼자"],
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* One optional question after the first successful export (#466). Two taps:
|
|
20
|
+
* what the export is for, then team or solo. Closing it at any point counts as
|
|
21
|
+
* a skip, and the card never comes back on this browser profile.
|
|
22
|
+
*/
|
|
23
|
+
export default function UseCaseQuestion({ open, isKo, onAnswer }) {
|
|
24
|
+
const [useCase, setUseCase] = useState(null);
|
|
25
|
+
const label = (en, ko) => (isKo ? ko : en);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!open) setUseCase(null);
|
|
28
|
+
}, [open]);
|
|
29
|
+
if (!open) return null;
|
|
30
|
+
const close = () => onAnswer(useCase ?? "skip", "skip");
|
|
31
|
+
return (
|
|
32
|
+
<section
|
|
33
|
+
className="use-case-ask"
|
|
34
|
+
role="dialog"
|
|
35
|
+
aria-modal="false"
|
|
36
|
+
aria-labelledby="use-case-ask-title"
|
|
37
|
+
onKeyDown={(event) => {
|
|
38
|
+
if (event.key === "Escape") close();
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
<div className="use-case-ask-head">
|
|
42
|
+
<strong id="use-case-ask-title">{label("Quick question (optional)", "짧은 질문 (선택)")}</strong>
|
|
43
|
+
<button type="button" className="use-case-ask-close" aria-label={label("Close", "닫기")} onClick={close}>
|
|
44
|
+
×
|
|
45
|
+
</button>
|
|
46
|
+
</div>
|
|
47
|
+
<p className="use-case-ask-prompt">
|
|
48
|
+
{useCase
|
|
49
|
+
? label("Who are you making it with?", "누구와 작업하나요?")
|
|
50
|
+
: label("What are you making with CozyClay?", "CozyClay로 뭘 만들고 있나요?")}
|
|
51
|
+
</p>
|
|
52
|
+
<div className="use-case-ask-choices" role="group" aria-label={useCase ? label("Team or solo", "팀 또는 혼자") : label("Use case", "용도")}>
|
|
53
|
+
{(useCase ? TEAMS : USE_CASES).map(([value, en, ko]) => (
|
|
54
|
+
<button
|
|
55
|
+
key={value}
|
|
56
|
+
type="button"
|
|
57
|
+
onClick={() => (useCase ? onAnswer(useCase, value) : setUseCase(value))}
|
|
58
|
+
>
|
|
59
|
+
{label(en, ko)}
|
|
60
|
+
</button>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
63
|
+
<p className="use-case-ask-note">{label("Anonymous. Only the choice is sent.", "익명이에요. 고른 항목만 전송돼요.")}</p>
|
|
64
|
+
</section>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
2
|
-
import { FiAlertTriangle, FiCheck, FiChevronRight, FiClock, FiDownload, FiImage, FiMoreHorizontal, FiPaperclip, FiPlus, FiRotateCw, FiX } from "react-icons/fi";
|
|
2
|
+
import { FiAlertTriangle, FiArrowUp, FiCheck, FiChevronRight, FiClock, FiDownload, FiFilm, FiImage, FiMoreHorizontal, FiPaperclip, FiPlus, FiRotateCw, FiX } from "react-icons/fi";
|
|
3
3
|
import {
|
|
4
4
|
ATTACHMENT_FAILED_NOTICE,
|
|
5
5
|
ATTACHMENT_LIMIT_NOTICE,
|
|
@@ -132,10 +132,12 @@ function JobCard({ job, onStop, onAccept }) {
|
|
|
132
132
|
const phase = job.state === "installed" ? null : job.phase;
|
|
133
133
|
const unverified = job.state === "review_required" || job.verification?.status === "unverified";
|
|
134
134
|
const limitations = job.verification?.limitations ?? [];
|
|
135
|
+
// An unverified take is installed all the same; the label says both.
|
|
136
|
+
const label = job.state === "installed" && unverified ? "Installed with warnings" : JOB_STATE_COPY[job.state] || job.state;
|
|
135
137
|
return <div className="agent-card agent-job-card" data-job-id={job.jobId} data-job-state={job.state}>
|
|
136
138
|
<div className="agent-job-head">
|
|
137
139
|
<StatusDot tone={jobStateTone(job.state)} title={job.state} />
|
|
138
|
-
<span className="agent-job-label">{
|
|
140
|
+
<span className="agent-job-label">{label}</span>
|
|
139
141
|
{unverified && <span className="agent-job-badge">Unverified</span>}
|
|
140
142
|
{percent !== null && <span className="agent-job-progress">{formatJobProgress(job.progress)}</span>}
|
|
141
143
|
</div>
|
|
@@ -163,15 +165,20 @@ function ReceiptCard({ item }) {
|
|
|
163
165
|
const unverified = receipt.verification?.status === "unverified";
|
|
164
166
|
const limitations = receipt.verification?.limitations ?? [];
|
|
165
167
|
const warnings = receipt.warnings ?? [];
|
|
168
|
+
const installedWithWarnings = receipt.status === "installed" && unverified;
|
|
166
169
|
return <div className="agent-card agent-receipt-card" data-receipt-id={item.receiptId} data-receipt-status={receipt.status}>
|
|
167
170
|
<div className="agent-receipt-head">
|
|
168
171
|
<StatusDot tone={unverified ? "warn" : "ok"} title={receipt.status} />
|
|
172
|
+
{installedWithWarnings && <span className="agent-job-label">Installed with warnings</span>}
|
|
169
173
|
<span className="agent-receipt-summary">{summary}</span>
|
|
170
174
|
</div>
|
|
171
175
|
{(warnings.length > 0 || limitations.length > 0) && <ul className="agent-receipt-notes">
|
|
172
176
|
{warnings.map((warning) => <li key={warning.code}>{warning.message || warning.code}</li>)}
|
|
173
177
|
{limitations.map((limit) => <li key={limit}>{limit}</li>)}
|
|
174
178
|
</ul>}
|
|
179
|
+
{/* The pane has no direct Undo into the Studio editor; it names the two
|
|
180
|
+
paths that do exist rather than drawing a button it cannot honour. */}
|
|
181
|
+
{installedWithWarnings && <p className="agent-job-note">Undo with Cmd+Z or ask the agent.</p>}
|
|
175
182
|
</div>;
|
|
176
183
|
}
|
|
177
184
|
|
|
@@ -268,7 +275,7 @@ export default function AgentPanel({
|
|
|
268
275
|
surface = "workflow",
|
|
269
276
|
buildContext = null,
|
|
270
277
|
onImageAction = null,
|
|
271
|
-
|
|
278
|
+
onI2vAction = null,
|
|
272
279
|
onReceipt = null,
|
|
273
280
|
}) {
|
|
274
281
|
// What this host shows around the shared conversation: labels, chips, the
|
|
@@ -346,8 +353,8 @@ export default function AgentPanel({
|
|
|
346
353
|
buildContextRef.current = buildContext;
|
|
347
354
|
const imageActionRef = useRef(onImageAction);
|
|
348
355
|
imageActionRef.current = onImageAction;
|
|
349
|
-
const
|
|
350
|
-
|
|
356
|
+
const i2vActionRef = useRef(onI2vAction);
|
|
357
|
+
i2vActionRef.current = onI2vAction;
|
|
351
358
|
const receiptRef = useRef(onReceipt);
|
|
352
359
|
receiptRef.current = onReceipt;
|
|
353
360
|
|
|
@@ -375,10 +382,12 @@ export default function AgentPanel({
|
|
|
375
382
|
const payload = await transport.loadSession(stored);
|
|
376
383
|
if (!Array.isArray(payload?.transcript)) throw new Error("missing transcript");
|
|
377
384
|
if (!cancelled) store.restore(payload.transcript, payload.sessionId || stored);
|
|
378
|
-
} catch {
|
|
385
|
+
} catch (error) {
|
|
379
386
|
if (!cancelled) {
|
|
380
387
|
store.newSession();
|
|
381
|
-
|
|
388
|
+
// A session id with no record behind it (the pane opened, nothing
|
|
389
|
+
// was ever sent) is nothing to restore, not a failure.
|
|
390
|
+
if (error?.status !== 404) setRestoreNotice("Previous conversation could not be restored");
|
|
382
391
|
}
|
|
383
392
|
} finally {
|
|
384
393
|
if (!cancelled) setRestoreReady(true);
|
|
@@ -814,9 +823,9 @@ export default function AgentPanel({
|
|
|
814
823
|
<StatusDot tone={statusTone} title={panelState} />
|
|
815
824
|
<h2 className="agent-title">Agent</h2>
|
|
816
825
|
<span className="agent-header-spacer" />
|
|
817
|
-
<button type="button" className="agent-
|
|
826
|
+
<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>
|
|
818
827
|
{presentation.history && <span className="agent-history-wrap">
|
|
819
|
-
<button type="button" className="agent-
|
|
828
|
+
<button type="button" className="agent-icon-button agent-history" aria-label="History" title="History" aria-haspopup="listbox" aria-expanded={surface === "studio" ? historyOpen : undefined} onClick={surface === "studio" ? openHistory : undefined} disabled={surface !== "studio"}><FiClock size={13} aria-hidden="true" /></button>
|
|
820
829
|
{surface === "studio" && historyOpen && <div className="agent-history-popover" role="listbox" aria-label="Agent history">
|
|
821
830
|
{historySessions.length ? historySessions.map((entry) => <button type="button" role="option" className="agent-history-item" key={entry.sessionId} onClick={() => restoreHistorySession(entry.sessionId)}>
|
|
822
831
|
<span className="agent-history-time">{relativeTime(entry.updatedAt)}</span>
|
|
@@ -827,6 +836,13 @@ export default function AgentPanel({
|
|
|
827
836
|
<span className="agent-overflow">
|
|
828
837
|
<button type="button" className="agent-icon-button agent-overflow-toggle" aria-haspopup="menu" aria-expanded={menuOpen} aria-label="More agent actions" onClick={() => setMenuOpen((value) => !value)}><FiMoreHorizontal size={13} /></button>
|
|
829
838
|
{menuOpen && <div className="agent-menu" role="menu">
|
|
839
|
+
{/* Who the session is lives with the actions that change it, not
|
|
840
|
+
as a standing strip over the conversation. */}
|
|
841
|
+
{account?.signedIn && <div className="agent-account" role="presentation">
|
|
842
|
+
<span className="agent-account-email">{account.email}</span>
|
|
843
|
+
<span className="agent-plan-badge">{quota?.plan || account.plan || "Free"}</span>
|
|
844
|
+
{resetLabel && <span className="agent-account-reset">resets in {resetLabel}</span>}
|
|
845
|
+
</div>}
|
|
830
846
|
<button type="button" role="menuitem" onClick={clearContext}>Clear context</button>
|
|
831
847
|
<button type="button" role="menuitem" className="agent-menu-keys" aria-expanded={keysOpen} onClick={toggleProviderKeys}>Provider keys…</button>
|
|
832
848
|
<button type="button" role="menuitem" onClick={signOut}>Sign out</button>
|
|
@@ -838,12 +854,6 @@ export default function AgentPanel({
|
|
|
838
854
|
{restoreNotice && <div className="agent-toast" role="status">{restoreNotice}</div>}
|
|
839
855
|
{steerNotice && <div className="agent-toast alert agent-steer-notice" role="alert">{steerNotice}</div>}
|
|
840
856
|
|
|
841
|
-
{account?.signedIn && <div className="agent-account">
|
|
842
|
-
<span className="agent-account-email">{account.email}</span>
|
|
843
|
-
<span className="agent-plan-badge">{quota?.plan || account.plan || "Free"}</span>
|
|
844
|
-
{resetLabel && <span className="agent-account-reset">resets in {resetLabel}</span>}
|
|
845
|
-
</div>}
|
|
846
|
-
|
|
847
857
|
{/* An inline section, not a modal: the panel stays the one place the
|
|
848
858
|
conversation and the credentials it runs on are managed. */}
|
|
849
859
|
{keysOpen && <section className="agent-keys" aria-label="Provider keys" data-agent-keys="true">
|
|
@@ -923,63 +933,68 @@ export default function AgentPanel({
|
|
|
923
933
|
</li>)}
|
|
924
934
|
</ul>}
|
|
925
935
|
{attachNotice && <p className="agent-attachment-notice" role="status">{attachNotice}</p>}
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
?
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
936
|
+
{/* One field: the message, and under it on the same surface the
|
|
937
|
+
frame toggle, the model and effort picks, and Send. */}
|
|
938
|
+
<div className="agent-field">
|
|
939
|
+
<textarea
|
|
940
|
+
ref={composerRef}
|
|
941
|
+
className="agent-input"
|
|
942
|
+
aria-label="Message the agent"
|
|
943
|
+
placeholder={panelState === "rate-limited" ? "Composer is paused" : model ? presentation.composerPlaceholder : "Waiting for the model list…"}
|
|
944
|
+
value={draft}
|
|
945
|
+
disabled={composerDisabled}
|
|
946
|
+
onChange={(event) => store.setDraft(event.target.value)}
|
|
947
|
+
onKeyDown={onComposerKeyDown}
|
|
948
|
+
onPaste={onComposerPaste}
|
|
949
|
+
onDragOver={onComposerDragOver}
|
|
950
|
+
onDrop={onComposerDrop}
|
|
951
|
+
/>
|
|
952
|
+
<div className="agent-composer-controls">
|
|
953
|
+
<button type="button" className="agent-attach-chip" aria-pressed={attachFrame} aria-label="Attach current frame" title="Attach current frame" onClick={() => setAttachFrame((value) => !value)}>
|
|
954
|
+
{attachFrame ? <span className="agent-attach-thumb" aria-hidden="true" /> : <FiPaperclip size={13} aria-hidden="true" />}
|
|
955
|
+
</button>
|
|
956
|
+
{/* Five providers in one dropdown: grouped by the provider that serves
|
|
957
|
+
them, and a provider without a credential still lists its models —
|
|
958
|
+
unpickable, and saying what they are waiting for. */}
|
|
959
|
+
<select className="agent-model-select" aria-label="Model" title="Model" value={model} disabled={!models.length} onChange={(event) => chooseModel(event.target.value)}>
|
|
960
|
+
{modelProviders.length
|
|
961
|
+
? modelProviders.map((provider) => <optgroup key={provider.id} label={provider.label}>
|
|
962
|
+
{provider.models.length
|
|
963
|
+
? provider.models.map((entry) => <option key={entry.key} value={entry.key} disabled={!provider.signedIn}>{provider.signedIn ? entry.label : `${entry.label} — add key`}</option>)
|
|
964
|
+
: <option value={`${provider.id}/`} disabled>{provider.signedIn ? "No models" : "No models — add key"}</option>}
|
|
965
|
+
</optgroup>)
|
|
966
|
+
: models.length
|
|
967
|
+
? models.map((entry) => <option key={entry.id} value={entry.id}>{entry.label}</option>)
|
|
968
|
+
: <option value="">{modelsState === "failed" ? "No model available" : "Loading models…"}</option>}
|
|
957
969
|
</select>
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
970
|
+
{efforts.length > 0 && (
|
|
971
|
+
<select className="agent-model-select agent-effort-select" aria-label="Reasoning effort" title="Reasoning effort" value={effort ?? efforts[0]} onChange={(event) => setEffort(event.target.value)}>
|
|
972
|
+
{/* The closed select shows only the word; "default" rides in the
|
|
973
|
+
option's tooltip so the composer row stays one line. */}
|
|
974
|
+
{efforts.map((value, index) => <option key={value} value={value} title={index === 0 ? "Default for this model" : undefined}>{value}</option>)}
|
|
975
|
+
</select>
|
|
976
|
+
)}
|
|
977
|
+
<span className="agent-composer-spacer" aria-hidden="true" />
|
|
978
|
+
{surface === "studio" && i2vActionRef.current && <button
|
|
979
|
+
type="button"
|
|
980
|
+
className="agent-attach-chip agent-i2v-action"
|
|
981
|
+
data-testid="i2v-agent-action"
|
|
982
|
+
aria-label="Generate motion"
|
|
983
|
+
title="Generate motion from this message"
|
|
984
|
+
disabled={composerDisabled || !draft.trim() || streaming}
|
|
985
|
+
onClick={() => { const instruction = draft.trim(); store.setDraft(""); i2vActionRef.current?.(instruction); }}
|
|
986
|
+
>
|
|
987
|
+
<FiFilm size={13} aria-hidden="true" />
|
|
988
|
+
</button>}
|
|
989
|
+
{/* While a turn runs, Stop is still the way out of it; on a surface
|
|
990
|
+
that can be steered, Send becomes the way INTO it. */}
|
|
991
|
+
{streaming
|
|
992
|
+
? <>
|
|
993
|
+
<button type="button" className="agent-send stop agent-stop" onClick={stopTurn}>Stop</button>
|
|
994
|
+
{presentation.steer && <button type="button" className="agent-send agent-steer" disabled={!draft.trim()} onClick={() => steerTurn(draft)}>Steer</button>}
|
|
995
|
+
</>
|
|
996
|
+
: <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>}
|
|
997
|
+
</div>
|
|
983
998
|
</div>
|
|
984
999
|
</div>}
|
|
985
1000
|
{/* The hint exists to warn about image spend; a surface that cannot
|
|
@@ -148,7 +148,7 @@ const CANVAS_TOOL_LABELS = {
|
|
|
148
148
|
add_reference_node: "Add reference image",
|
|
149
149
|
};
|
|
150
150
|
|
|
151
|
-
/** The
|
|
151
|
+
/** The Studio families, named for what they do to the scene. The Studio
|
|
152
152
|
* is bilingual, so these go through ko() like every other Studio label. */
|
|
153
153
|
const STUDIO_TOOL_LABELS = {
|
|
154
154
|
inspect_studio: ko("Read the scene", "\uC7A5\uBA74 \uC77D\uAE30"),
|
|
@@ -160,6 +160,7 @@ const STUDIO_TOOL_LABELS = {
|
|
|
160
160
|
generate_motion: ko("Generate motion", "\uBAA8\uC158 \uC0DD\uC131"),
|
|
161
161
|
verify_result: ko("Verify the result", "\uACB0\uACFC \uAC80\uC99D"),
|
|
162
162
|
undo_edit: ko("Undo an edit", "\uD3B8\uC9D1 \uB418\uB3CC\uB9AC\uAE30"),
|
|
163
|
+
run_action: ko("Run an editor action", "\uD3B8\uC9D1\uAE30 \uB3D9\uC791 \uC2E4\uD589"),
|
|
163
164
|
};
|
|
164
165
|
|
|
165
166
|
/**
|
|
@@ -1349,13 +1350,22 @@ export function createAgentChatStore({
|
|
|
1349
1350
|
const restore = (transcript, sessionId = state.sessionId) => {
|
|
1350
1351
|
seenReceipts.clear();
|
|
1351
1352
|
settledActions.clear();
|
|
1353
|
+
const restoredReceiptIds = new Set();
|
|
1352
1354
|
const restored = (Array.isArray(transcript) ? transcript : []).flatMap((item) => {
|
|
1353
1355
|
if (item?.kind === "user" || item?.kind === "assistant") {
|
|
1354
1356
|
const attachments = Array.isArray(item.attachments) ? item.attachments.filter((entry) => typeof entry?.dataUrl === "string") : [];
|
|
1355
1357
|
return [{ kind: item.kind, id: newId(), text: String(item.text ?? ""), ...(attachments.length ? { attachments } : {}) }];
|
|
1356
1358
|
}
|
|
1357
1359
|
if (item?.kind === "tool") return [{ kind: "tool", id: newId(), callId: newId(), name: item.name || "tool", label: item.label, ok: item.ok, elapsedMs: item.elapsedMs, status: item.ok === false ? "failed" : "done" }];
|
|
1358
|
-
if (item?.kind === "receipt")
|
|
1360
|
+
if (item?.kind === "receipt") {
|
|
1361
|
+
if (item.receiptId && restoredReceiptIds.has(item.receiptId)) return [];
|
|
1362
|
+
if (item.receiptId) {
|
|
1363
|
+
restoredReceiptIds.add(item.receiptId);
|
|
1364
|
+
seenReceipts.add(item.receiptId);
|
|
1365
|
+
}
|
|
1366
|
+
return [{ kind: "receipt", id: `receipt:${item.receiptId || newId()}`, receiptId: item.receiptId, summary: item.summary || "Receipt", receipt: { status: "applied", warnings: [], verification: { status: "verified" } } }];
|
|
1367
|
+
}
|
|
1368
|
+
if (item?.kind === "failure") return [{ kind: "failure", id: newId(), failure: { code: item.code || "upstream", message: String(item.message || "The turn failed."), recovery: { action: "retry", retryAllowed: true } } }];
|
|
1359
1369
|
return [];
|
|
1360
1370
|
});
|
|
1361
1371
|
const lastPrompt = [...restored].reverse().find((item) => item.kind === "user")?.text || "";
|