@volter/editor-game 0.5.65

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.
Files changed (302) hide show
  1. package/LICENSE +661 -0
  2. package/NOTICE +23 -0
  3. package/contributions/asset-budget-asset.menu.ts +26 -0
  4. package/contributions/asset-budget.action.ts +18 -0
  5. package/contributions/asset-budget.document.tsx +21 -0
  6. package/contributions/asset-budget.menu.ts +22 -0
  7. package/contributions/audio-unlock.service.ts +17 -0
  8. package/contributions/audio.utility.tsx +15 -0
  9. package/contributions/autoplay.service.ts +48 -0
  10. package/contributions/bridge.command.ts +172 -0
  11. package/contributions/build-profiles.document.tsx +21 -0
  12. package/contributions/build-progress.status.tsx +45 -0
  13. package/contributions/build.action.ts +26 -0
  14. package/contributions/build.command.ts +27 -0
  15. package/contributions/build.header.tsx +37 -0
  16. package/contributions/build.menu.ts +31 -0
  17. package/contributions/build.service.ts +31 -0
  18. package/contributions/connection.status.tsx +43 -0
  19. package/contributions/coverage.service.ts +118 -0
  20. package/contributions/edit-mode-audio.service.ts +23 -0
  21. package/contributions/edit-mode-networking.service.ts +30 -0
  22. package/contributions/game-document.service.ts +25 -0
  23. package/contributions/game-eval.command.ts +210 -0
  24. package/contributions/game.layout.ts +19 -0
  25. package/contributions/gameplay.command.ts +255 -0
  26. package/contributions/generation.service.ts +96 -0
  27. package/contributions/generations.status.tsx +53 -0
  28. package/contributions/ingest.service.ts +80 -0
  29. package/contributions/instances.command.ts +75 -0
  30. package/contributions/navmesh.menu.ts +39 -0
  31. package/contributions/navmesh.service.ts +19 -0
  32. package/contributions/network.utility.tsx +17 -0
  33. package/contributions/play.command.ts +355 -0
  34. package/contributions/profiler.action.ts +16 -0
  35. package/contributions/profiler.menu.ts +22 -0
  36. package/contributions/profiler.utility.tsx +17 -0
  37. package/contributions/react/component-board.service.ts +21 -0
  38. package/contributions/react/design-time-mount.service.ts +60 -0
  39. package/contributions/react/pasteboard.action.ts +41 -0
  40. package/contributions/react/react-inspector.service.ts +85 -0
  41. package/contributions/react/story-documents.service.ts +44 -0
  42. package/contributions/scene-document.service.ts +32 -0
  43. package/contributions/state-watch.action.ts +17 -0
  44. package/contributions/state-watch.menu.ts +23 -0
  45. package/contributions/state-watch.utility.tsx +20 -0
  46. package/contributions/team-playtest.service.ts +124 -0
  47. package/contributions/three/camera-runtime.inspector.tsx +34 -0
  48. package/contributions/three/component-board.service.ts +24 -0
  49. package/contributions/three/component-verbs.command.ts +110 -0
  50. package/contributions/three/component-verbs.service.ts +92 -0
  51. package/contributions/three/constraints.inspector.tsx +34 -0
  52. package/contributions/three/model-asset-sections.service.ts +26 -0
  53. package/contributions/three/reflection-probe-capture.inspector.tsx +32 -0
  54. package/contributions/three/story-documents.service.ts +31 -0
  55. package/contributions/three/three-authoring.service.ts +66 -0
  56. package/contributions/transport.header.tsx +19 -0
  57. package/contributions/xstate-behavior.action.ts +42 -0
  58. package/contributions/xstate-behavior.document.tsx +73 -0
  59. package/contributions/xstate-behavior.inspector.tsx +28 -0
  60. package/contributions/xstate-behavior.menu.ts +23 -0
  61. package/package.json +144 -0
  62. package/src/asset-budget/AssetBudgetPanel.tsx +1172 -0
  63. package/src/asset-budget/asset-budget-model.ts +799 -0
  64. package/src/asset-budget/basis-encoder.ts +165 -0
  65. package/src/asset-budget/gltf-io.ts +154 -0
  66. package/src/asset-budget/gltf-optimize.ts +384 -0
  67. package/src/asset-budget/image-dims.ts +78 -0
  68. package/src/asset-budget/optimize-apply.ts +221 -0
  69. package/src/audio/AudioDebuggerPanel.tsx +457 -0
  70. package/src/audio/audio-debugger-model.ts +87 -0
  71. package/src/bridge/call.ts +60 -0
  72. package/src/bridge/dispatch.ts +459 -0
  73. package/src/bridge/live-frames.ts +25 -0
  74. package/src/bridge/screenshot.ts +316 -0
  75. package/src/build/BuildProfilesPanel.tsx +476 -0
  76. package/src/build/build-session.ts +247 -0
  77. package/src/build/format-bytes.ts +14 -0
  78. package/src/command-results.ts +36 -0
  79. package/src/coverage/live-authoring-surface.ts +30 -0
  80. package/src/coverage/live-project-verbs.ts +162 -0
  81. package/src/coverage/native-system-coverage.ts +143 -0
  82. package/src/coverage/root-coverage.ts +79 -0
  83. package/src/coverage/session-coverage.ts +193 -0
  84. package/src/design-system-stories/ApplicationChrome.stories.tsx +100 -0
  85. package/src/design-system-stories/InspectorNarrowBodies.stories.tsx +406 -0
  86. package/src/edit-mode/edit-mode-audio.ts +56 -0
  87. package/src/edit-mode/edit-mode-networking.ts +112 -0
  88. package/src/game-document/DevicePresetPicker.tsx +84 -0
  89. package/src/game-document/GameCaptureFrameButton.tsx +45 -0
  90. package/src/game-document/GameDocument.tsx +215 -0
  91. package/src/game-document/GamePanel.tsx +662 -0
  92. package/src/game-document/InstanceInspectorPicker.tsx +183 -0
  93. package/src/game-document/crowd-debug.ts +183 -0
  94. package/src/game-document/device-preview.ts +336 -0
  95. package/src/game-document/game-view-store.ts +107 -0
  96. package/src/game-document/physics-debug.ts +187 -0
  97. package/src/generation/GenerationActivity.tsx +421 -0
  98. package/src/generation/GenerationGallery.css +231 -0
  99. package/src/generation/generation-documents.tsx +338 -0
  100. package/src/generation/generation-jobs.ts +128 -0
  101. package/src/generation/generation-presentation.ts +257 -0
  102. package/src/host/adapter-reach.ts +445 -0
  103. package/src/host/adapter-runtime-bindings.ts +303 -0
  104. package/src/host/after-paint.ts +112 -0
  105. package/src/host/api/configurations.ts +66 -0
  106. package/src/host/authoring/babylon-authoring-adapter.ts +703 -0
  107. package/src/host/authoring/canvas-runtime-recognition.ts +50 -0
  108. package/src/host/authoring/contract-hierarchy-authoring.ts +144 -0
  109. package/src/host/authoring/contract-scenes-stories.ts +204 -0
  110. package/src/host/authoring/creation-site-related.ts +55 -0
  111. package/src/host/authoring/ephemeral-persistence.ts +29 -0
  112. package/src/host/authoring/gesture-persist.ts +84 -0
  113. package/src/host/authoring/ingest-data-writer.ts +232 -0
  114. package/src/host/authoring/ingest-source-persistence.ts +826 -0
  115. package/src/host/authoring/mount-isolated-pixi-screen.ts +250 -0
  116. package/src/host/authoring/mounted-authoring.ts +41 -0
  117. package/src/host/authoring/owned-pixi-ticker-listeners.ts +96 -0
  118. package/src/host/authoring/phaser-live-authoring-adapter.ts +265 -0
  119. package/src/host/authoring/pixi-authoring-adapter.ts +1554 -0
  120. package/src/host/authoring/pixi-creation-site-write-target.ts +60 -0
  121. package/src/host/authoring/pixi-isolation-assets.ts +25 -0
  122. package/src/host/authoring/pixi-live-write-target.ts +979 -0
  123. package/src/host/authoring/pixi-source-identity.ts +141 -0
  124. package/src/host/authoring/pixi-still-presentation.ts +71 -0
  125. package/src/host/authoring/pixi-structure-history.ts +237 -0
  126. package/src/host/authoring/pixi-transform-channels.ts +205 -0
  127. package/src/host/authoring/selection-remount-handoff.ts +23 -0
  128. package/src/host/authoring/source-persistence-backend.ts +373 -0
  129. package/src/host/authoring/source-refresh-revisions.ts +81 -0
  130. package/src/host/authoring/struct-write-pipe.ts +143 -0
  131. package/src/host/auto-frame-window.ts +89 -0
  132. package/src/host/binding-resolver.ts +393 -0
  133. package/src/host/browser-transpile.ts +631 -0
  134. package/src/host/canvas-entry-runtime.ts +95 -0
  135. package/src/host/components/CameraAuthoringOverlay.tsx +216 -0
  136. package/src/host/components/HeaderTelemetry.tsx +341 -0
  137. package/src/host/components/PixiIsolationSceneContent.tsx +280 -0
  138. package/src/host/components/ResolutionPicker.tsx +89 -0
  139. package/src/host/components/ThreeIsolationSceneContent.tsx +180 -0
  140. package/src/host/components/frame-debugger-model.ts +579 -0
  141. package/src/host/components/header-telemetry-model.ts +74 -0
  142. package/src/host/components/scene-document.tsx +447 -0
  143. package/src/host/components/utility-view-state.ts +87 -0
  144. package/src/host/components/world-root-stage-binding.tsx +133 -0
  145. package/src/host/components/world-root-stage.ts +1046 -0
  146. package/src/host/coverage/authoring-read-probe.ts +583 -0
  147. package/src/host/coverage/capability-coverage.ts +1587 -0
  148. package/src/host/coverage/coverage-accounting.ts +261 -0
  149. package/src/host/coverage/game-contract-seam-evidence.ts +82 -0
  150. package/src/host/coverage/project-verb-coverage.ts +148 -0
  151. package/src/host/coverage/system-adapter-coverage.ts +373 -0
  152. package/src/host/design-system-stories/StoryLayout.tsx +104 -0
  153. package/src/host/design-system-stories/fixtures/authoring.ts +247 -0
  154. package/src/host/design-system-stories/fixtures/editor-runtime.tsx +115 -0
  155. package/src/host/document-preview-three.ts +110 -0
  156. package/src/host/entry-adjudication.ts +89 -0
  157. package/src/host/game-location-guard.ts +150 -0
  158. package/src/host/game-module-access.ts +196 -0
  159. package/src/host/game-realm-page.ts +358 -0
  160. package/src/host/game-realm-reclaim.ts +55 -0
  161. package/src/host/game-realm-storage.ts +104 -0
  162. package/src/host/gameplay-export.ts +288 -0
  163. package/src/host/gameplay-recording.ts +717 -0
  164. package/src/host/gated-globals.ts +1511 -0
  165. package/src/host/history/json-history-resource.ts +254 -0
  166. package/src/host/ingest/registry.ts +261 -0
  167. package/src/host/instance-extract-actions.ts +120 -0
  168. package/src/host/instance-fork-actions.ts +120 -0
  169. package/src/host/play-control-hook.ts +26 -0
  170. package/src/host/playwright-shim.ts +479 -0
  171. package/src/host/projection/dom.ts +295 -0
  172. package/src/host/projection/pixi.ts +288 -0
  173. package/src/host/r3f-entry-runtime.ts +77 -0
  174. package/src/host/react-mount-runtime.ts +162 -0
  175. package/src/host/realm-services.ts +148 -0
  176. package/src/host/recording-preview.ts +106 -0
  177. package/src/host/roots/module-root.ts +203 -0
  178. package/src/host/roots/react-root.ts +181 -0
  179. package/src/host/same-realm-loop-gate.ts +544 -0
  180. package/src/host/scene-view-drawability.ts +69 -0
  181. package/src/host/sdk/tools.ts +31 -0
  182. package/src/host/served-bundle-runtime-modules.ts +331 -0
  183. package/src/host/server-log-bridge.ts +60 -0
  184. package/src/host/staged-projects.ts +24 -0
  185. package/src/host/stories/mounted-story-viewport-source.ts +64 -0
  186. package/src/host/stories/story-arg-descriptors.ts +50 -0
  187. package/src/host/stories/story-media-presence.ts +152 -0
  188. package/src/host/surface-content.ts +87 -0
  189. package/src/host/take-named-export.ts +23 -0
  190. package/src/host/three-ingest-runtime.ts +76 -0
  191. package/src/host/types-fastnoise-lite.d.ts +7 -0
  192. package/src/host/types-mikktspace.d.ts +20 -0
  193. package/src/host/types-troika-three-text.d.ts +7 -0
  194. package/src/host/use-active-performance-source.ts +53 -0
  195. package/src/host/viewport-pose-memory.ts +48 -0
  196. package/src/host/viewport-root-presentation.ts +40 -0
  197. package/src/ingest/active-ingest.ts +251 -0
  198. package/src/ingest/active-scene-navigation.ts +43 -0
  199. package/src/ingest/authoring/ingest-dom-surface-authoring.ts +191 -0
  200. package/src/ingest/authoring/ingest-root-adapter.ts +897 -0
  201. package/src/ingest/capture-wait-report.ts +122 -0
  202. package/src/ingest/deferred-ingest-play.ts +216 -0
  203. package/src/ingest/deferred-ingest-session.ts +23 -0
  204. package/src/ingest/discovery-public-ingest.ts +242 -0
  205. package/src/ingest/dom-stub-mark.ts +7 -0
  206. package/src/ingest/entry-load.ts +56 -0
  207. package/src/ingest/game-contract-realm.ts +34 -0
  208. package/src/ingest/game-pointer-lock.ts +89 -0
  209. package/src/ingest/held-scene-repaint.ts +73 -0
  210. package/src/ingest/host-surface-box.ts +174 -0
  211. package/src/ingest/ingest-boot-viewport.ts +54 -0
  212. package/src/ingest/ingest-canvas-scene-document.tsx +177 -0
  213. package/src/ingest/ingest-canvas-scene.ts +50 -0
  214. package/src/ingest/ingest-evidence-hook.ts +131 -0
  215. package/src/ingest/ingest-frame-snapshot.ts +183 -0
  216. package/src/ingest/ingest-play-commands.ts +134 -0
  217. package/src/ingest/ingest-play-control.ts +299 -0
  218. package/src/ingest/ingest-render-debug.ts +320 -0
  219. package/src/ingest/ingest-siblings.ts +487 -0
  220. package/src/ingest/ingest-status.ts +62 -0
  221. package/src/ingest/live-ingest-facet.ts +46 -0
  222. package/src/ingest/module-mode.ts +229 -0
  223. package/src/ingest/mount-canvas-ingest-root.ts +899 -0
  224. package/src/ingest/mount-coverage.ts +296 -0
  225. package/src/ingest/mount-dom-ingest-root.ts +282 -0
  226. package/src/ingest/mount-ingest-root.ts +744 -0
  227. package/src/ingest/mount-three-ingest-root.ts +366 -0
  228. package/src/ingest/resolve-canvas.ts +47 -0
  229. package/src/ingest/resolve-three.ts +123 -0
  230. package/src/ingest/served-bundle.ts +109 -0
  231. package/src/ingest/served-html-boot.ts +292 -0
  232. package/src/ingest/surface-canvas.ts +84 -0
  233. package/src/ingest/surface-dom.ts +84 -0
  234. package/src/ingest/surface-three.ts +128 -0
  235. package/src/ingest/types.ts +76 -0
  236. package/src/ingest/unmount-ingest-root.ts +224 -0
  237. package/src/navmesh/navmesh-actions.ts +27 -0
  238. package/src/navmesh/navmesh-handler.ts +237 -0
  239. package/src/navmesh/navmesh-workflow-store.ts +78 -0
  240. package/src/network/NetworkInspectorPanel.tsx +644 -0
  241. package/src/network/network-inspector-model.ts +225 -0
  242. package/src/play/play-boot-stall.ts +118 -0
  243. package/src/play/play-log-events.ts +26 -0
  244. package/src/play/play-mode.ts +2553 -0
  245. package/src/play/play-recording.ts +335 -0
  246. package/src/play/react-play-live-authoring.ts +165 -0
  247. package/src/play-bar/PlayBar.tsx +488 -0
  248. package/src/play-bar/PlayerCountPicker.tsx +100 -0
  249. package/src/profiler/FrameDebuggerPanel.tsx +458 -0
  250. package/src/profiler/PerformancePanel.tsx +1068 -0
  251. package/src/profiler/ProfilerPanel.tsx +53 -0
  252. package/src/profiler/frame-debugger-store.ts +97 -0
  253. package/src/profiler/main-thread-busy.ts +90 -0
  254. package/src/react/design-time-react-mount.ts +782 -0
  255. package/src/react/dom-authoring-adapter.ts +764 -0
  256. package/src/react/pasteboard-materialize.ts +154 -0
  257. package/src/react/react-inspector-section.tsx +2496 -0
  258. package/src/react/react-world-authoring-adapter.ts +3795 -0
  259. package/src/react/story-documents/story-args-section.ts +19 -0
  260. package/src/react/story-documents/story-documents.tsx +1380 -0
  261. package/src/react/story-paint-bounds.ts +94 -0
  262. package/src/react/ui-board-document.tsx +101 -0
  263. package/src/react/ui-board-title.ts +9 -0
  264. package/src/react/ui-component-board.ts +72 -0
  265. package/src/services/audio-pose-guard.ts +81 -0
  266. package/src/services/game-audio-unlock.ts +48 -0
  267. package/src/state-watch/StateWatchPanel.tsx +535 -0
  268. package/src/three/authoring/camera-runtime-inspector-section.tsx +135 -0
  269. package/src/three/authoring/constraint-inspector-section.tsx +189 -0
  270. package/src/three/authoring/design-time-renderer.ts +189 -0
  271. package/src/three/authoring/model-asset-inspector-section.css +41 -0
  272. package/src/three/authoring/model-asset-inspector-section.tsx +869 -0
  273. package/src/three/authoring/oid-source-persistence.ts +557 -0
  274. package/src/three/authoring/r3f-design-session.ts +1174 -0
  275. package/src/three/authoring/r3f-source-authoring-adapter.ts +5949 -0
  276. package/src/three/authoring/reflection-probe-inspector-section.tsx +76 -0
  277. package/src/three/authoring/spatial-audio-handles.ts +215 -0
  278. package/src/three/authoring/spatial-collider-handles.ts +229 -0
  279. package/src/three/authoring/spatial-joint-handles.ts +114 -0
  280. package/src/three/authoring/spatial-light-handles.ts +178 -0
  281. package/src/three/authoring/spatial-lod-handles.ts +93 -0
  282. package/src/three/authoring/spatial-particle-handles.ts +368 -0
  283. package/src/three/authoring/three-authoring-adapter.ts +1953 -0
  284. package/src/three/authoring/three-scene-identity.ts +19 -0
  285. package/src/three/authoring/three-spatial-handles.ts +161 -0
  286. package/src/three/authoring/typed-three-inspector.ts +528 -0
  287. package/src/three/component-verbs/extract-menu.ts +71 -0
  288. package/src/three/component-verbs/fork-menu.ts +78 -0
  289. package/src/three/component-verbs/internals-menu.ts +91 -0
  290. package/src/three/story-documents/three-story-documents.tsx +607 -0
  291. package/src/three/three-board/ThreeBoardDocument.tsx +888 -0
  292. package/src/three/three-board/board-framing.ts +412 -0
  293. package/src/three/three-board/board-layout.ts +401 -0
  294. package/src/three/three-board/board-scene.ts +901 -0
  295. package/src/three/three-board/three-component-board.ts +62 -0
  296. package/src/xstate/XStateBehaviorSection.tsx +130 -0
  297. package/src/xstate/XStateMachineInspector.tsx +566 -0
  298. package/src/xstate/character-animation-machine.fixture.ts +74 -0
  299. package/src/xstate/live-behaviors.ts +106 -0
  300. package/src/xstate/use-live-actor-state.ts +44 -0
  301. package/src/xstate/xstate-graph.ts +235 -0
  302. package/src/xstate/xstate-layout.ts +76 -0
@@ -0,0 +1,421 @@
1
+ /**
2
+ * Generations — the project's generated ASSETS, shown as the assets they are.
3
+ *
4
+ * Every card's subject is the file the job produced: the image, the video with
5
+ * its own controls, the model's rendered thumbnail. Job vocabulary (state,
6
+ * model, charge, prompt) frames that preview instead of replacing it, and the
7
+ * preview is never fabricated — a file the browser cannot show honestly gets a
8
+ * named file row, not a decorative stand-in.
9
+ */
10
+
11
+ import { faFile, faFileLines, faTriangleExclamation } from '@fortawesome/free-solid-svg-icons';
12
+ import type { GenerationJob } from '@volter/editor-sdk/generations';
13
+ import { useState, useSyncExternalStore } from 'react';
14
+ import './GenerationGallery.css';
15
+ import { assetCapabilities, assetDocumentKind } from '@volter/editor-core/asset-workflow/asset-capabilities';
16
+ import { openAssetDocument } from '@volter/editor-core/components/asset-documents';
17
+ import { AudioAssetThumb, ModelThumbnail } from '@volter/editor-core/components/asset-thumbnails';
18
+ import { editorConsole } from '@volter/editor-core/editor-console';
19
+ import { useEditorStore } from '@volter/editor-core/editor-runtime';
20
+ import { modelThumbnailFormat } from '@volter/editor-core/model-thumbnail';
21
+ import {
22
+ Button,
23
+ EditorBadge,
24
+ EditorBanner,
25
+ EditorIcon,
26
+ EditorToolbar,
27
+ Spacer,
28
+ StateSurface,
29
+ type StateSurfaceTone,
30
+ Text,
31
+ } from '@volter/editor-sdk/widgets';
32
+ import { openGenerationCreateDocument, openGenerationDocument } from './generation-documents';
33
+ import {
34
+ acceptGenerationJob,
35
+ forgetGenerationJob,
36
+ generationJobIsActive,
37
+ generationJobIsUnread,
38
+ generationJobsError,
39
+ generationJobsSnapshot,
40
+ markGenerationJobRead,
41
+ pollActiveGenerationJobs,
42
+ refreshGenerationJobs,
43
+ subscribeGenerationJobs,
44
+ } from './generation-jobs';
45
+ import {
46
+ GENERATION_STATE_LABEL,
47
+ type GenerationCardState,
48
+ type GenerationOutput,
49
+ generationAge,
50
+ generationBillingLabel,
51
+ generationBillingSettled,
52
+ generationCardState,
53
+ generationModelLabel,
54
+ generationOutputs,
55
+ generationPrompt,
56
+ generationRemovable,
57
+ generationRouteLabel,
58
+ generationSaveRetryable,
59
+ generationTitle,
60
+ } from './generation-presentation';
61
+
62
+ /** One produced file, painted the way that file can honestly be shown. */
63
+ function OutputPreview({
64
+ output,
65
+ onOpen,
66
+ }: {
67
+ output: GenerationOutput;
68
+ onOpen?: (() => void) | undefined;
69
+ }) {
70
+ const src = output.url;
71
+
72
+ if (src && output.kind === 'image') {
73
+ const preview = (
74
+ <img className="vgai-generation-output-media" src={src} alt={output.name} loading="lazy" />
75
+ );
76
+ return onOpen ? (
77
+ // The preview itself opens the asset — the gesture a gallery implies.
78
+ <Button
79
+ variant="ghost"
80
+ className="vgai-generation-output-open"
81
+ onClick={onOpen}
82
+ aria-label={`Open ${output.name}`}
83
+ >
84
+ {preview}
85
+ </Button>
86
+ ) : (
87
+ preview
88
+ );
89
+ }
90
+ if (src && output.kind === 'video') {
91
+ return (
92
+ // Deliberately no autoplay and no `muted` autoplay trick: a gallery that
93
+ // starts sound is the failure this panel is replacing.
94
+ <video
95
+ className="vgai-generation-output-media"
96
+ src={src}
97
+ controls
98
+ preload="metadata"
99
+ playsInline
100
+ title={output.name}
101
+ />
102
+ );
103
+ }
104
+ if (src && output.kind === 'audio') {
105
+ return (
106
+ <div className="vgai-generation-output-audio">
107
+ <AudioAssetThumb url={src} name={output.name} />
108
+ <audio src={src} controls preload="metadata" title={output.name} />
109
+ </div>
110
+ );
111
+ }
112
+ if (output.url && output.kind === 'model' && modelThumbnailFormat(output.url)) {
113
+ return <ModelThumbnail url={output.url} />;
114
+ }
115
+ return (
116
+ <div className="vgai-generation-output-file">
117
+ <EditorIcon icon={output.kind === 'model' ? faFile : faFileLines} />
118
+ <Text variant="caption" tone="muted">
119
+ {output.name}
120
+ </Text>
121
+ {onOpen && (
122
+ <Button size="compact" variant="ghost" onClick={onOpen}>
123
+ Open file
124
+ </Button>
125
+ )}
126
+ </div>
127
+ );
128
+ }
129
+
130
+ const STATE_TONE: Record<GenerationCardState, StateSurfaceTone> = {
131
+ queued: 'loading',
132
+ running: 'loading',
133
+ saving: 'loading',
134
+ 'save-failed': 'error',
135
+ ready: 'success',
136
+ 'no-asset': 'neutral',
137
+ failed: 'error',
138
+ cancelled: 'neutral',
139
+ };
140
+
141
+ /** What each state means, in a game developer's terms. */
142
+ const STATE_DESCRIPTION: Record<GenerationCardState, (job: GenerationJob) => string> = {
143
+ queued: (job) =>
144
+ job.queuePosition === undefined
145
+ ? 'Waiting for the provider to start.'
146
+ : `Position ${job.queuePosition} in the provider queue.`,
147
+ running: (job) => job.message ?? 'The provider is working on it.',
148
+ saving: () => 'Downloading the finished output into this project.',
149
+ // The provider's own result is fine; the DOWNLOAD into the project is what
150
+ // failed, and the server has stopped trying. Nothing here promises another
151
+ // automatic attempt — the retry is the button.
152
+ 'save-failed': (job) => job.pollError ?? 'The finished output could not be saved.',
153
+ ready: () => 'Saved into this project.',
154
+ 'no-asset': () => 'The operation finished without a file this project can hold.',
155
+ failed: (job) => job.message ?? 'The provider reported a failure.',
156
+ cancelled: () => 'This generation was cancelled.',
157
+ };
158
+
159
+ /** The media region while a job has produced nothing to show yet. */
160
+ function StateMedia({ job, state }: { job: GenerationJob; state: GenerationCardState }) {
161
+ const pending = state === 'queued' || state === 'running' || state === 'saving';
162
+ const failing = state === 'failed' || state === 'save-failed';
163
+ return (
164
+ <div className="vgai-generation-state-media" data-state={state}>
165
+ <StateSurface
166
+ compact
167
+ tone={STATE_TONE[state]}
168
+ {...(failing ? { icon: <EditorIcon icon={faTriangleExclamation} /> } : {})}
169
+ title={GENERATION_STATE_LABEL[state]}
170
+ description={STATE_DESCRIPTION[state](job)}
171
+ />
172
+ {pending && (
173
+ // Determinate ONLY when the provider reported progress; otherwise the
174
+ // native indeterminate bar, which claims nothing.
175
+ <progress
176
+ className="vgai-generation-progress"
177
+ aria-label={`${job.label} progress`}
178
+ {...(state === 'running' && job.progress !== undefined
179
+ ? { value: job.progress, max: 1 }
180
+ : {})}
181
+ />
182
+ )}
183
+ </div>
184
+ );
185
+ }
186
+
187
+ function CardActions({
188
+ job,
189
+ busy,
190
+ error,
191
+ run,
192
+ }: {
193
+ job: GenerationJob;
194
+ busy: boolean;
195
+ error: string | null;
196
+ run: (action: () => Promise<void>) => void;
197
+ }) {
198
+ return (
199
+ <div className="vgai-generation-card-actions">
200
+ {generationSaveRetryable(job, error) && (
201
+ // There is NO add-to-project gate: the server accepts a finished
202
+ // output on its own. This appears only once a save has actually
203
+ // failed, and runs that same accept operation again by hand.
204
+ <Button
205
+ size="compact"
206
+ variant="primary"
207
+ disabled={busy}
208
+ onClick={() => run(() => acceptGenerationJob(job))}
209
+ >
210
+ {busy ? 'Saving…' : 'Retry save'}
211
+ </Button>
212
+ )}
213
+ <Button size="compact" variant="ghost" onClick={() => openGenerationDocument(job)}>
214
+ Details
215
+ </Button>
216
+ {generationRemovable(job) && (
217
+ <Button
218
+ size="compact"
219
+ variant="ghost"
220
+ disabled={busy}
221
+ onClick={() => run(() => forgetGenerationJob(job.id))}
222
+ >
223
+ Remove
224
+ </Button>
225
+ )}
226
+ </div>
227
+ );
228
+ }
229
+
230
+ function GenerationCard({
231
+ job,
232
+ busy,
233
+ error,
234
+ run,
235
+ }: {
236
+ job: GenerationJob;
237
+ busy: boolean;
238
+ error: string | null;
239
+ run: (action: () => Promise<void>) => void;
240
+ }) {
241
+ const store = useEditorStore();
242
+ const state = generationCardState(job);
243
+ const outputs = generationOutputs(job);
244
+ const prompt = generationPrompt(job);
245
+ const unread = generationJobIsUnread(job);
246
+ const openOutput = (output: GenerationOutput): (() => void) | undefined => {
247
+ if (!output.url) return undefined;
248
+ const kind = assetDocumentKind(assetCapabilities(output.name));
249
+ if (!kind) return undefined;
250
+ return () => openAssetDocument(store, output.url as string, kind);
251
+ };
252
+
253
+ return (
254
+ <article
255
+ className="vgai-generation-card"
256
+ data-state={state}
257
+ aria-busy={generationJobIsActive(job)}
258
+ aria-label={generationTitle(job)}
259
+ onClickCapture={() => {
260
+ if (unread) {
261
+ void markGenerationJobRead(job.id).catch((cause) => {
262
+ editorConsole.warn(
263
+ `Could not mark generation as seen: ${cause instanceof Error ? cause.message : String(cause)}`,
264
+ 'generations',
265
+ );
266
+ });
267
+ }
268
+ }}
269
+ >
270
+ <div className="vgai-generation-card-media" data-outputs={Math.min(outputs.length, 4)}>
271
+ {outputs.length === 0 ? (
272
+ <StateMedia job={job} state={state} />
273
+ ) : (
274
+ outputs.map((output) => (
275
+ <div className="vgai-generation-output" key={output.path} title={output.path}>
276
+ <OutputPreview output={output} onOpen={openOutput(output)} />
277
+ </div>
278
+ ))
279
+ )}
280
+ </div>
281
+
282
+ <div className="vgai-generation-card-body">
283
+ <div className="vgai-generation-card-heading">
284
+ <Text variant="label" truncate title={generationTitle(job)}>
285
+ {generationTitle(job)}
286
+ </Text>
287
+ <EditorBadge className="vgai-generation-chip" data-state={state}>
288
+ {GENERATION_STATE_LABEL[state]}
289
+ {state === 'running' && job.progress !== undefined
290
+ ? ` · ${Math.round(job.progress * 100)}%`
291
+ : ''}
292
+ </EditorBadge>
293
+ {unread && (
294
+ <EditorBadge className="vgai-generation-chip" data-state="new">
295
+ New
296
+ </EditorBadge>
297
+ )}
298
+ </div>
299
+
300
+ <Text variant="caption" tone="muted" truncate title={generationModelLabel(job)}>
301
+ {job.provider} · {generationModelLabel(job)}
302
+ </Text>
303
+ <Text variant="caption" tone="dim">
304
+ {generationBillingLabel(job.billing)}
305
+ {generationBillingSettled(job.billing) ? '' : ' (est.)'} · {generationRouteLabel(job)} ·{' '}
306
+ {generationAge(job.updatedAt)}
307
+ </Text>
308
+
309
+ {prompt && (
310
+ <p className="vgai-generation-card-prompt" title={prompt}>
311
+ {prompt}
312
+ </p>
313
+ )}
314
+
315
+ {(error ?? job.pollError) && (
316
+ // An ACTIVE job's poll error is transient — the server is still
317
+ // polling, and saying so is true. A finished job's error is a failed
318
+ // SAVE, whose automatic attempts the server has already spent, so the
319
+ // text promises nothing and the retry button carries it instead.
320
+ <EditorBanner
321
+ tone={error || state === 'save-failed' ? 'error' : 'warning'}
322
+ className="vgai-generation-card-alert"
323
+ >
324
+ {error ??
325
+ (state === 'save-failed'
326
+ ? `Could not save into the project: ${job.pollError}`
327
+ : `Polling will retry: ${job.pollError}`)}
328
+ </EditorBanner>
329
+ )}
330
+
331
+ <CardActions job={job} busy={busy} error={error} run={run} />
332
+ </div>
333
+ </article>
334
+ );
335
+ }
336
+
337
+ export function GenerationActivity() {
338
+ const { jobs } = useSyncExternalStore(
339
+ subscribeGenerationJobs,
340
+ generationJobsSnapshot,
341
+ generationJobsSnapshot,
342
+ );
343
+ const [busy, setBusy] = useState<string | null>(null);
344
+ const [actionErrors, setActionErrors] = useState<Record<string, string>>({});
345
+ const activeCount = jobs.filter(generationJobIsActive).length;
346
+ const savingCount = jobs.filter((job) => generationCardState(job) === 'saving').length;
347
+
348
+ const run = async (id: string, action: () => Promise<void>) => {
349
+ setBusy(id);
350
+ setActionErrors((current) => {
351
+ const next = { ...current };
352
+ delete next[id];
353
+ return next;
354
+ });
355
+ try {
356
+ await action();
357
+ } catch (cause) {
358
+ const message = cause instanceof Error ? cause.message : String(cause);
359
+ setActionErrors((current) => ({ ...current, [id]: message }));
360
+ } finally {
361
+ setBusy(null);
362
+ }
363
+ };
364
+
365
+ const storeError = generationJobsError();
366
+
367
+ return (
368
+ <div className="vgai-generation-gallery">
369
+ <EditorToolbar label="Generations" compact className="vgai-generation-gallery-bar">
370
+ <Text variant="label">Generations</Text>
371
+ <Text variant="caption" tone="muted">
372
+ {jobs.length} {jobs.length === 1 ? 'generation' : 'generations'}
373
+ {activeCount > 0 ? ` · ${activeCount} running` : ''}
374
+ {savingCount > 0 ? ` · ${savingCount} saving` : ''}
375
+ </Text>
376
+ <Spacer />
377
+ <Button
378
+ size="compact"
379
+ variant="ghost"
380
+ onClick={() => void pollActiveGenerationJobs().then(refreshGenerationJobs)}
381
+ >
382
+ Refresh
383
+ </Button>
384
+ <Button size="compact" variant="primary" onClick={() => openGenerationCreateDocument()}>
385
+ New asset
386
+ </Button>
387
+ </EditorToolbar>
388
+
389
+ {storeError && (
390
+ <EditorBanner tone="error" className="vgai-generation-gallery-alert">
391
+ {storeError}
392
+ </EditorBanner>
393
+ )}
394
+
395
+ {jobs.length === 0 ? (
396
+ <StateSurface
397
+ className="vgai-generation-gallery-empty"
398
+ title="No generated assets yet"
399
+ description="Preview generated images, video, audio and models here. Create an asset to get started."
400
+ action={
401
+ <Button size="compact" variant="primary" onClick={() => openGenerationCreateDocument()}>
402
+ New asset
403
+ </Button>
404
+ }
405
+ />
406
+ ) : (
407
+ <section className="vgai-generation-grid" aria-label="Generated assets">
408
+ {jobs.map((job) => (
409
+ <GenerationCard
410
+ key={job.id}
411
+ job={job}
412
+ busy={busy === job.id}
413
+ error={actionErrors[job.id] ?? null}
414
+ run={(action) => void run(job.id, action)}
415
+ />
416
+ ))}
417
+ </section>
418
+ )}
419
+ </div>
420
+ );
421
+ }
@@ -0,0 +1,231 @@
1
+ /* Generations utility — a responsive asset gallery (GenerationActivity.tsx).
2
+ *
3
+ * The card is preview-first: a fixed-aspect media box holds the generated file
4
+ * at `contain` (portrait and landscape both survive intact), and the job's text
5
+ * sits under it in a fixed reading order. The grid is `auto-fill` so the same
6
+ * panel works docked narrow at the side and wide across the bottom; a container
7
+ * query drops the card to a single readable column in a genuinely narrow dock.
8
+ */
9
+
10
+ .vgai-generation-gallery {
11
+ display: flex;
12
+ height: 100%;
13
+ flex-direction: column;
14
+ color: var(--vgai-content-primary);
15
+ /* `size`, not `inline-size`: the cards answer to the drawer's HEIGHT as well
16
+ * as its width — a short bottom drawer gets a shorter preview so the name,
17
+ * cost and actions still land inside it. */
18
+ container-type: size;
19
+ }
20
+ .vgai-generation-gallery .vgai-toolbar {
21
+ flex: 0 0 auto;
22
+ }
23
+ .vgai-generation-gallery-bar {
24
+ gap: var(--vgai-space-2);
25
+ }
26
+ .vgai-generation-gallery-alert {
27
+ margin: var(--vgai-space-3);
28
+ }
29
+ .vgai-generation-gallery-empty {
30
+ margin: auto;
31
+ max-width: 420px;
32
+ text-align: center;
33
+ }
34
+
35
+ .vgai-generation-grid {
36
+ display: grid;
37
+ min-height: 0;
38
+ flex: 1;
39
+ align-content: start;
40
+ gap: var(--vgai-space-3);
41
+ padding: var(--vgai-space-3);
42
+ overflow-y: auto;
43
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
44
+ grid-auto-rows: max-content;
45
+ }
46
+ @container (max-width: 380px) {
47
+ .vgai-generation-grid {
48
+ grid-template-columns: 1fr;
49
+ }
50
+ }
51
+ .vgai-generation-card {
52
+ display: flex;
53
+ flex-direction: column;
54
+ overflow: hidden;
55
+ border: var(--vgai-stroke-resting) solid var(--vgai-boundary-default);
56
+ border-radius: var(--vgai-radius-md);
57
+ background: var(--vgai-surface-panel);
58
+ }
59
+ .vgai-generation-card[data-state="failed"],
60
+ .vgai-generation-card[data-state="save-failed"] {
61
+ border-color: var(--vgai-danger-muted);
62
+ }
63
+ .vgai-generation-card[data-state="ready"] {
64
+ border-color: var(--vgai-boundary-strong);
65
+ }
66
+
67
+ /* The preview. One output fills the box; several share it as a tile grid, so a
68
+ * multi-output job shows every file it produced without a second surface. */
69
+ .vgai-generation-card-media {
70
+ display: grid;
71
+ /* `flex-shrink: 0` and the height cap are load-bearing TOGETHER: in the
72
+ * ordinary ~260px bottom drawer the card was squeezed to a ~50px strip with
73
+ * every piece of metadata hidden, and an uncapped 4:3 box pushed the text
74
+ * out of the drawer instead. */
75
+ flex-shrink: 0;
76
+ height: 160px;
77
+ max-height: 160px;
78
+ gap: var(--vgai-stroke-active);
79
+ background: var(--vgai-bg-inset);
80
+ grid-template-columns: 1fr;
81
+ }
82
+ .vgai-generation-card-media[data-outputs="2"],
83
+ .vgai-generation-card-media[data-outputs="3"],
84
+ .vgai-generation-card-media[data-outputs="4"] {
85
+ grid-template-columns: 1fr 1fr;
86
+ }
87
+ .vgai-generation-output {
88
+ display: flex;
89
+ min-width: 0;
90
+ min-height: 0;
91
+ align-items: center;
92
+ justify-content: center;
93
+ overflow: hidden;
94
+ padding: var(--vgai-space-1);
95
+ }
96
+ /* `contain`, never `cover`: cropping a generated frame hides exactly the part
97
+ * a game developer is judging. */
98
+ .vgai-generation-output-media {
99
+ display: block;
100
+ width: 100%;
101
+ height: 100%;
102
+ max-width: 100%;
103
+ max-height: 100%;
104
+ border-radius: var(--vgai-radius-sm);
105
+ object-fit: contain;
106
+ }
107
+ /* The preview's own click target: a real Button stripped back to the image it
108
+ * contains, so the gallery's most obvious gesture is still a focusable control. */
109
+ .vgai-generation-output .vgai-generation-output-open.vgai-btn {
110
+ display: flex;
111
+ width: 100%;
112
+ height: 100%;
113
+ max-height: 100%;
114
+ min-height: 0;
115
+ align-items: center;
116
+ justify-content: center;
117
+ padding: 0;
118
+ border: 0;
119
+ background: transparent;
120
+ cursor: zoom-in;
121
+ }
122
+ video.vgai-generation-output-media {
123
+ width: 100%;
124
+ }
125
+ .vgai-generation-output-audio {
126
+ display: flex;
127
+ width: 100%;
128
+ height: 100%;
129
+ flex-direction: column;
130
+ align-items: center;
131
+ justify-content: center;
132
+ gap: var(--vgai-space-2);
133
+ padding: var(--vgai-space-2);
134
+ }
135
+ .vgai-generation-output-audio audio {
136
+ width: 100%;
137
+ }
138
+ .vgai-generation-output-file {
139
+ display: flex;
140
+ flex-direction: column;
141
+ align-items: center;
142
+ gap: var(--vgai-space-2);
143
+ padding: var(--vgai-space-3);
144
+ color: var(--vgai-content-muted);
145
+ text-align: center;
146
+ overflow-wrap: anywhere;
147
+ }
148
+
149
+ .vgai-generation-state-media {
150
+ display: flex;
151
+ flex-direction: column;
152
+ align-items: center;
153
+ justify-content: center;
154
+ gap: var(--vgai-space-2);
155
+ padding: var(--vgai-space-3);
156
+ text-align: center;
157
+ }
158
+ .vgai-generation-progress {
159
+ width: 70%;
160
+ height: 4px;
161
+ accent-color: var(--vgai-accent);
162
+ }
163
+
164
+ .vgai-generation-card-body {
165
+ display: flex;
166
+ min-width: 0;
167
+ flex-direction: column;
168
+ gap: var(--vgai-space-1);
169
+ padding: var(--vgai-space-3);
170
+ }
171
+ .vgai-generation-card-heading {
172
+ display: flex;
173
+ min-width: 0;
174
+ align-items: center;
175
+ gap: var(--vgai-space-2);
176
+ }
177
+ .vgai-generation-card-heading > .vgai-text {
178
+ min-width: 0;
179
+ flex: 1;
180
+ }
181
+ .vgai-generation-chip {
182
+ flex: 0 0 auto;
183
+ }
184
+ .vgai-generation-chip[data-state="ready"] {
185
+ color: var(--vgai-success);
186
+ border-color: var(--vgai-success-muted);
187
+ }
188
+ .vgai-generation-chip[data-state="failed"],
189
+ .vgai-generation-chip[data-state="save-failed"] {
190
+ color: var(--vgai-danger);
191
+ border-color: var(--vgai-danger-muted);
192
+ }
193
+ .vgai-generation-chip[data-state="queued"],
194
+ .vgai-generation-chip[data-state="running"],
195
+ .vgai-generation-chip[data-state="saving"],
196
+ .vgai-generation-chip[data-state="new"] {
197
+ color: var(--vgai-accent);
198
+ border-color: var(--vgai-accent-muted);
199
+ }
200
+
201
+ /* The prompt is available, never dominant: two lines, full text on hover. */
202
+ .vgai-generation-card-prompt {
203
+ display: -webkit-box;
204
+ margin: var(--vgai-space-1) 0 0;
205
+ overflow: hidden;
206
+ color: var(--vgai-content-dim);
207
+ font-size: var(--vgai-font-sm);
208
+ line-height: var(--vgai-leading-normal);
209
+ -webkit-box-orient: vertical;
210
+ -webkit-line-clamp: 2;
211
+ line-clamp: 2;
212
+ }
213
+ .vgai-generation-card-alert {
214
+ margin-top: var(--vgai-space-2);
215
+ font-size: var(--vgai-font-sm);
216
+ }
217
+ .vgai-generation-card-actions {
218
+ display: flex;
219
+ flex-wrap: wrap;
220
+ gap: var(--vgai-space-2);
221
+ margin-top: var(--vgai-space-2);
222
+ }
223
+
224
+ @container (max-height: 300px) {
225
+ .vgai-generation-card-media {
226
+ height: 96px;
227
+ }
228
+ .vgai-generation-card-prompt {
229
+ display: none;
230
+ }
231
+ }