@portel/photon 1.20.0 → 1.21.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.
Files changed (149) hide show
  1. package/README.md +5 -5
  2. package/dist/ag-ui/adapter.d.ts.map +1 -1
  3. package/dist/ag-ui/adapter.js +25 -0
  4. package/dist/ag-ui/adapter.js.map +1 -1
  5. package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
  6. package/dist/auto-ui/beam/routes/api-browse.js +8 -49
  7. package/dist/auto-ui/beam/routes/api-browse.js.map +1 -1
  8. package/dist/auto-ui/beam/routes/api-config.d.ts.map +1 -1
  9. package/dist/auto-ui/beam/routes/api-config.js +161 -20
  10. package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
  11. package/dist/auto-ui/beam.d.ts.map +1 -1
  12. package/dist/auto-ui/beam.js +24 -31
  13. package/dist/auto-ui/beam.js.map +1 -1
  14. package/dist/auto-ui/bridge/index.d.ts.map +1 -1
  15. package/dist/auto-ui/bridge/index.js +107 -11
  16. package/dist/auto-ui/bridge/index.js.map +1 -1
  17. package/dist/auto-ui/bridge/renderers.d.ts +14 -0
  18. package/dist/auto-ui/bridge/renderers.d.ts.map +1 -1
  19. package/dist/auto-ui/bridge/renderers.js +692 -61
  20. package/dist/auto-ui/bridge/renderers.js.map +1 -1
  21. package/dist/auto-ui/frontend/index.html +3 -3
  22. package/dist/auto-ui/frontend/pure-view.html +19 -19
  23. package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
  24. package/dist/auto-ui/streamable-http-transport.js +144 -28
  25. package/dist/auto-ui/streamable-http-transport.js.map +1 -1
  26. package/dist/auto-ui/ui-resolver.d.ts +25 -0
  27. package/dist/auto-ui/ui-resolver.d.ts.map +1 -0
  28. package/dist/auto-ui/ui-resolver.js +95 -0
  29. package/dist/auto-ui/ui-resolver.js.map +1 -0
  30. package/dist/beam-form.bundle.js +26 -189
  31. package/dist/beam-form.bundle.js.map +4 -4
  32. package/dist/beam.bundle.js +1646 -494
  33. package/dist/beam.bundle.js.map +4 -4
  34. package/dist/cli/commands/beam.d.ts.map +1 -1
  35. package/dist/cli/commands/beam.js +47 -30
  36. package/dist/cli/commands/beam.js.map +1 -1
  37. package/dist/cli/commands/build.d.ts.map +1 -1
  38. package/dist/cli/commands/build.js +36 -7
  39. package/dist/cli/commands/build.js.map +1 -1
  40. package/dist/cli/commands/daemon.d.ts.map +1 -1
  41. package/dist/cli/commands/daemon.js +12 -6
  42. package/dist/cli/commands/daemon.js.map +1 -1
  43. package/dist/cli/commands/init.d.ts.map +1 -1
  44. package/dist/cli/commands/init.js +90 -50
  45. package/dist/cli/commands/init.js.map +1 -1
  46. package/dist/cli/commands/mcp.d.ts.map +1 -1
  47. package/dist/cli/commands/mcp.js +18 -6
  48. package/dist/cli/commands/mcp.js.map +1 -1
  49. package/dist/cli/commands/publish.d.ts +14 -0
  50. package/dist/cli/commands/publish.d.ts.map +1 -0
  51. package/dist/cli/commands/publish.js +126 -0
  52. package/dist/cli/commands/publish.js.map +1 -0
  53. package/dist/cli/commands/run.d.ts.map +1 -1
  54. package/dist/cli/commands/run.js +2 -0
  55. package/dist/cli/commands/run.js.map +1 -1
  56. package/dist/cli/commands/serve.d.ts.map +1 -1
  57. package/dist/cli/commands/serve.js +14 -2
  58. package/dist/cli/commands/serve.js.map +1 -1
  59. package/dist/cli/index.d.ts.map +1 -1
  60. package/dist/cli/index.js +3 -0
  61. package/dist/cli/index.js.map +1 -1
  62. package/dist/cli-alias.d.ts.map +1 -1
  63. package/dist/cli-alias.js +2 -3
  64. package/dist/cli-alias.js.map +1 -1
  65. package/dist/context-store.d.ts +4 -4
  66. package/dist/context-store.d.ts.map +1 -1
  67. package/dist/context-store.js +18 -15
  68. package/dist/context-store.js.map +1 -1
  69. package/dist/context.d.ts +31 -2
  70. package/dist/context.d.ts.map +1 -1
  71. package/dist/context.js +86 -9
  72. package/dist/context.js.map +1 -1
  73. package/dist/daemon/client.d.ts +9 -1
  74. package/dist/daemon/client.d.ts.map +1 -1
  75. package/dist/daemon/client.js +58 -2
  76. package/dist/daemon/client.js.map +1 -1
  77. package/dist/daemon/manager.d.ts +5 -0
  78. package/dist/daemon/manager.d.ts.map +1 -1
  79. package/dist/daemon/manager.js +116 -34
  80. package/dist/daemon/manager.js.map +1 -1
  81. package/dist/daemon/ownership.d.ts +12 -0
  82. package/dist/daemon/ownership.d.ts.map +1 -0
  83. package/dist/daemon/ownership.js +55 -0
  84. package/dist/daemon/ownership.js.map +1 -0
  85. package/dist/daemon/protocol.d.ts +3 -1
  86. package/dist/daemon/protocol.d.ts.map +1 -1
  87. package/dist/daemon/protocol.js +14 -2
  88. package/dist/daemon/protocol.js.map +1 -1
  89. package/dist/daemon/server.js +587 -77
  90. package/dist/daemon/server.js.map +1 -1
  91. package/dist/daemon/session-manager.d.ts +9 -1
  92. package/dist/daemon/session-manager.d.ts.map +1 -1
  93. package/dist/daemon/session-manager.js +54 -1
  94. package/dist/daemon/session-manager.js.map +1 -1
  95. package/dist/daemon/worker-host.js +7 -0
  96. package/dist/daemon/worker-host.js.map +1 -1
  97. package/dist/daemon/worker-manager.d.ts +12 -0
  98. package/dist/daemon/worker-manager.d.ts.map +1 -1
  99. package/dist/daemon/worker-manager.js +147 -16
  100. package/dist/daemon/worker-manager.js.map +1 -1
  101. package/dist/daemon/worker-protocol.d.ts +3 -0
  102. package/dist/daemon/worker-protocol.d.ts.map +1 -1
  103. package/dist/deploy/cloudflare.d.ts.map +1 -1
  104. package/dist/deploy/cloudflare.js +2 -4
  105. package/dist/deploy/cloudflare.js.map +1 -1
  106. package/dist/loader.d.ts +10 -9
  107. package/dist/loader.d.ts.map +1 -1
  108. package/dist/loader.js +224 -115
  109. package/dist/loader.js.map +1 -1
  110. package/dist/marketplace-manager.d.ts +1 -1
  111. package/dist/marketplace-manager.d.ts.map +1 -1
  112. package/dist/marketplace-manager.js +5 -4
  113. package/dist/marketplace-manager.js.map +1 -1
  114. package/dist/photon-cli-runner.d.ts.map +1 -1
  115. package/dist/photon-cli-runner.js +66 -23
  116. package/dist/photon-cli-runner.js.map +1 -1
  117. package/dist/photon-doc-extractor.d.ts.map +1 -1
  118. package/dist/photon-doc-extractor.js +59 -15
  119. package/dist/photon-doc-extractor.js.map +1 -1
  120. package/dist/photons/canvas/ui/canvas.photon.html +1493 -0
  121. package/dist/photons/canvas.photon.d.ts +400 -0
  122. package/dist/photons/canvas.photon.d.ts.map +1 -0
  123. package/dist/photons/canvas.photon.js +662 -0
  124. package/dist/photons/canvas.photon.js.map +1 -0
  125. package/dist/photons/canvas.photon.ts +814 -0
  126. package/dist/photons/publish.photon.d.ts +97 -0
  127. package/dist/photons/publish.photon.d.ts.map +1 -0
  128. package/dist/photons/publish.photon.js +569 -0
  129. package/dist/photons/publish.photon.js.map +1 -0
  130. package/dist/photons/publish.photon.ts +683 -0
  131. package/dist/photons/ui/canvas.photon.html +624 -0
  132. package/dist/resource-server.d.ts.map +1 -1
  133. package/dist/resource-server.js +7 -1
  134. package/dist/resource-server.js.map +1 -1
  135. package/dist/server.d.ts.map +1 -1
  136. package/dist/server.js +14 -16
  137. package/dist/server.js.map +1 -1
  138. package/dist/shared-utils.d.ts +4 -0
  139. package/dist/shared-utils.d.ts.map +1 -1
  140. package/dist/shared-utils.js +24 -2
  141. package/dist/shared-utils.js.map +1 -1
  142. package/dist/template-manager.d.ts.map +1 -1
  143. package/dist/template-manager.js +56 -234
  144. package/dist/template-manager.js.map +1 -1
  145. package/dist/tsx-compiler.d.ts +23 -0
  146. package/dist/tsx-compiler.d.ts.map +1 -0
  147. package/dist/tsx-compiler.js +221 -0
  148. package/dist/tsx-compiler.js.map +1 -0
  149. package/package.json +7 -7
@@ -0,0 +1,1493 @@
1
+ <style>
2
+ * { box-sizing: border-box; margin: 0; padding: 0; }
3
+
4
+ body {
5
+ font-family: var(--font-family-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
6
+ background: var(--color-surface, #1a1b26);
7
+ color: var(--color-on-surface, #e6e6e6);
8
+ overflow: hidden;
9
+ height: 100vh;
10
+ user-select: none;
11
+ }
12
+
13
+ /* ── Canvas Surface ── */
14
+ .canvas-surface {
15
+ position: relative;
16
+ width: 100%;
17
+ height: 100%;
18
+ overflow: auto;
19
+ cursor: default;
20
+ }
21
+
22
+ /* ── Grid dots background ── */
23
+ .canvas-surface::before {
24
+ content: '';
25
+ position: absolute;
26
+ inset: 0;
27
+ width: 4000px;
28
+ height: 4000px;
29
+ background-image: radial-gradient(circle, var(--color-outline-variant, #333) 1px, transparent 1px);
30
+ background-size: 24px 24px;
31
+ pointer-events: none;
32
+ opacity: 0.3;
33
+ }
34
+
35
+ /* ── Canvas Element ── */
36
+ .ce {
37
+ position: absolute;
38
+ background: var(--color-surface-container, #1e2030);
39
+ border: 1px solid var(--color-outline-variant, #333);
40
+ border-radius: 8px;
41
+ overflow: hidden;
42
+ display: flex;
43
+ flex-direction: column;
44
+ transition: box-shadow 0.15s ease;
45
+ }
46
+
47
+ .ce:hover {
48
+ border-color: var(--color-primary, #6366f1);
49
+ }
50
+
51
+ .ce.selected {
52
+ border-color: var(--color-primary, #6366f1);
53
+ box-shadow: 0 0 0 2px var(--color-primary, #6366f1);
54
+ }
55
+
56
+ /* ── Title Bar ── */
57
+ .ce-bar {
58
+ display: flex;
59
+ align-items: center;
60
+ gap: 6px;
61
+ padding: 4px 8px;
62
+ background: var(--color-surface-container-high, #252738);
63
+ cursor: grab;
64
+ font-size: 11px;
65
+ color: var(--color-on-surface-muted, #999);
66
+ min-height: 28px;
67
+ flex-shrink: 0;
68
+ }
69
+
70
+ .ce-bar:active { cursor: grabbing; }
71
+
72
+ .ce-bar .ce-label {
73
+ flex: 1;
74
+ overflow: hidden;
75
+ text-overflow: ellipsis;
76
+ white-space: nowrap;
77
+ }
78
+
79
+ .ce-bar .ce-format {
80
+ opacity: 0.5;
81
+ font-size: 10px;
82
+ font-family: var(--font-family-mono, monospace);
83
+ }
84
+
85
+ .ce-bar .ce-delete {
86
+ opacity: 0;
87
+ cursor: pointer;
88
+ padding: 2px 4px;
89
+ border-radius: 3px;
90
+ font-size: 12px;
91
+ line-height: 1;
92
+ transition: opacity 0.1s;
93
+ }
94
+
95
+ .ce:hover .ce-delete,
96
+ .ce.selected .ce-delete { opacity: 0.6; }
97
+ .ce-delete:hover { opacity: 1 !important; background: var(--color-error, #ef4444); color: #fff; }
98
+
99
+ /* ── Body (rendered content) ── */
100
+ .ce-body {
101
+ flex: 1;
102
+ overflow: auto;
103
+ padding: 8px;
104
+ min-height: 0;
105
+ }
106
+
107
+ /* ── Resize Handles (all corners) ── */
108
+ .ce-resize {
109
+ position: absolute;
110
+ width: 12px;
111
+ height: 12px;
112
+ opacity: 0;
113
+ transition: opacity 0.1s;
114
+ z-index: 2;
115
+ }
116
+
117
+ .ce:hover .ce-resize,
118
+ .ce.selected .ce-resize { opacity: 1; }
119
+
120
+ .ce-resize::after {
121
+ content: '';
122
+ position: absolute;
123
+ width: 6px;
124
+ height: 6px;
125
+ border-radius: 1px;
126
+ }
127
+
128
+ .ce-resize-se { right: 0; bottom: 0; cursor: nwse-resize; }
129
+ .ce-resize-se::after { right: 2px; bottom: 2px; border-right: 2px solid var(--color-primary, #6366f1); border-bottom: 2px solid var(--color-primary, #6366f1); }
130
+
131
+ .ce-resize-sw { left: 0; bottom: 0; cursor: nesw-resize; }
132
+ .ce-resize-sw::after { left: 2px; bottom: 2px; border-left: 2px solid var(--color-primary, #6366f1); border-bottom: 2px solid var(--color-primary, #6366f1); }
133
+
134
+ .ce-resize-ne { right: 0; top: 0; cursor: nesw-resize; }
135
+ .ce-resize-ne::after { right: 2px; top: 2px; border-right: 2px solid var(--color-primary, #6366f1); border-top: 2px solid var(--color-primary, #6366f1); }
136
+
137
+ .ce-resize-nw { left: 0; top: 0; cursor: nwse-resize; }
138
+ .ce-resize-nw::after { left: 2px; top: 2px; border-left: 2px solid var(--color-primary, #6366f1); border-top: 2px solid var(--color-primary, #6366f1); }
139
+
140
+ /* ── Insert Button ── */
141
+ .insert-btn {
142
+ position: fixed;
143
+ bottom: 20px;
144
+ right: 20px;
145
+ width: 48px;
146
+ height: 48px;
147
+ border-radius: 50%;
148
+ background: var(--color-primary, #6366f1);
149
+ color: #fff;
150
+ border: none;
151
+ font-size: 24px;
152
+ cursor: pointer;
153
+ display: flex;
154
+ align-items: center;
155
+ justify-content: center;
156
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
157
+ z-index: 10000;
158
+ transition: transform 0.15s;
159
+ }
160
+
161
+ .insert-btn:hover { transform: scale(1.1); }
162
+
163
+ /* ── Format Picker ── */
164
+ .format-picker {
165
+ position: fixed;
166
+ bottom: 80px;
167
+ right: 20px;
168
+ width: 260px;
169
+ max-height: 400px;
170
+ overflow-y: auto;
171
+ background: var(--color-surface-container, #1e2030);
172
+ border: 1px solid var(--color-outline-variant, #333);
173
+ border-radius: 12px;
174
+ padding: 8px;
175
+ z-index: 10001;
176
+ box-shadow: 0 8px 24px rgba(0,0,0,0.4);
177
+ display: none;
178
+ }
179
+
180
+ .format-picker.open { display: block; }
181
+
182
+ .format-picker input {
183
+ width: 100%;
184
+ padding: 8px 10px;
185
+ background: var(--color-surface, #1a1b26);
186
+ border: 1px solid var(--color-outline-variant, #333);
187
+ border-radius: 6px;
188
+ color: var(--color-on-surface, #e6e6e6);
189
+ font-size: 13px;
190
+ margin-bottom: 6px;
191
+ outline: none;
192
+ }
193
+
194
+ .format-picker input:focus { border-color: var(--color-primary, #6366f1); }
195
+
196
+ .format-item {
197
+ padding: 6px 10px;
198
+ border-radius: 6px;
199
+ cursor: pointer;
200
+ font-size: 13px;
201
+ display: flex;
202
+ justify-content: space-between;
203
+ align-items: center;
204
+ }
205
+
206
+ .format-item:hover { background: var(--color-surface-container-high, #252738); }
207
+
208
+ .format-item .fi-name { font-weight: 500; }
209
+ .format-item .fi-shape { font-size: 11px; opacity: 0.5; font-family: var(--font-family-mono, monospace); }
210
+
211
+ /* ── Locked Elements ── */
212
+ .ce.locked {
213
+ opacity: 0.85;
214
+ }
215
+
216
+ .ce.locked .ce-bar {
217
+ cursor: default;
218
+ }
219
+
220
+ .ce-lock {
221
+ font-size: 10px;
222
+ opacity: 0.6;
223
+ }
224
+
225
+ /* ── Agent Colors ── */
226
+ .ce[data-agent="ai"] .ce-bar { border-left: 3px solid #6366f1; }
227
+ .ce[data-agent="human"] .ce-bar { border-left: 3px solid #22c55e; }
228
+ .ce[data-agent="agent-1"] .ce-bar { border-left: 3px solid #f59e0b; }
229
+ .ce[data-agent="agent-2"] .ce-bar { border-left: 3px solid #ec4899; }
230
+ .ce[data-agent="agent-3"] .ce-bar { border-left: 3px solid #06b6d4; }
231
+
232
+ /* ── Turn Banner ── */
233
+ .turn-banner {
234
+ position: fixed;
235
+ top: 0;
236
+ left: 0;
237
+ right: 0;
238
+ display: flex;
239
+ align-items: center;
240
+ justify-content: center;
241
+ gap: 8px;
242
+ padding: 6px 16px;
243
+ font-size: 12px;
244
+ z-index: 10002;
245
+ transition: background 0.2s;
246
+ }
247
+
248
+ .turn-banner[data-turn="human"] {
249
+ background: rgba(34, 197, 94, 0.15);
250
+ color: #22c55e;
251
+ }
252
+
253
+ .turn-banner[data-turn="ai"] {
254
+ background: rgba(99, 102, 241, 0.15);
255
+ color: #a5b4fc;
256
+ }
257
+
258
+ .turn-banner .turn-msg {
259
+ opacity: 0.7;
260
+ font-style: italic;
261
+ }
262
+
263
+ .turn-banner .turn-pass {
264
+ margin-left: 12px;
265
+ padding: 3px 10px;
266
+ border-radius: 4px;
267
+ border: 1px solid currentColor;
268
+ background: transparent;
269
+ color: inherit;
270
+ cursor: pointer;
271
+ font-size: 11px;
272
+ }
273
+
274
+ .turn-banner .turn-pass:hover {
275
+ background: rgba(255,255,255,0.1);
276
+ }
277
+
278
+ /* ── Export Button ── */
279
+ .export-btn {
280
+ position: fixed;
281
+ bottom: 20px;
282
+ right: 80px;
283
+ width: 48px;
284
+ height: 48px;
285
+ border-radius: 50%;
286
+ background: var(--color-surface-container-high, #252738);
287
+ color: var(--color-on-surface-muted, #999);
288
+ border: 1px solid var(--color-outline-variant, #333);
289
+ font-size: 20px;
290
+ cursor: pointer;
291
+ display: flex;
292
+ align-items: center;
293
+ justify-content: center;
294
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
295
+ z-index: 10000;
296
+ transition: transform 0.15s, border-color 0.15s;
297
+ }
298
+
299
+ .export-btn:hover {
300
+ transform: scale(1.1);
301
+ border-color: var(--color-primary, #6366f1);
302
+ color: var(--color-primary, #6366f1);
303
+ }
304
+
305
+ /* ── Timeline Bar ── */
306
+ .timeline-bar {
307
+ position: fixed;
308
+ bottom: 0;
309
+ left: 0;
310
+ right: 0;
311
+ height: 36px;
312
+ background: var(--color-surface-container-high, #252738);
313
+ border-top: 1px solid var(--color-outline-variant, #333);
314
+ display: flex;
315
+ align-items: center;
316
+ gap: 8px;
317
+ padding: 0 12px;
318
+ z-index: 10003;
319
+ font-size: 11px;
320
+ color: var(--color-on-surface-muted, #999);
321
+ display: none; /* hidden until timeline has data */
322
+ }
323
+
324
+ .timeline-bar.visible { display: flex; }
325
+
326
+ .timeline-bar .tl-label {
327
+ white-space: nowrap;
328
+ font-weight: 500;
329
+ min-width: 50px;
330
+ }
331
+
332
+ .timeline-bar input[type="range"] {
333
+ flex: 1;
334
+ height: 4px;
335
+ -webkit-appearance: none;
336
+ appearance: none;
337
+ background: var(--color-outline-variant, #333);
338
+ border-radius: 2px;
339
+ outline: none;
340
+ }
341
+
342
+ .timeline-bar input[type="range"]::-webkit-slider-thumb {
343
+ -webkit-appearance: none;
344
+ width: 14px;
345
+ height: 14px;
346
+ border-radius: 50%;
347
+ background: var(--color-primary, #6366f1);
348
+ cursor: pointer;
349
+ }
350
+
351
+ .timeline-bar .tl-action {
352
+ font-family: var(--font-family-mono, monospace);
353
+ font-size: 10px;
354
+ max-width: 160px;
355
+ overflow: hidden;
356
+ text-overflow: ellipsis;
357
+ white-space: nowrap;
358
+ }
359
+
360
+ .timeline-bar .tl-btn {
361
+ padding: 2px 8px;
362
+ border-radius: 4px;
363
+ border: 1px solid var(--color-outline-variant, #333);
364
+ background: transparent;
365
+ color: var(--color-on-surface-muted, #999);
366
+ cursor: pointer;
367
+ font-size: 10px;
368
+ white-space: nowrap;
369
+ }
370
+
371
+ .timeline-bar .tl-btn:hover {
372
+ border-color: var(--color-primary, #6366f1);
373
+ color: var(--color-primary, #6366f1);
374
+ }
375
+
376
+ .timeline-bar .tl-btn.playing {
377
+ border-color: var(--color-primary, #6366f1);
378
+ color: var(--color-primary, #6366f1);
379
+ background: rgba(99, 102, 241, 0.15);
380
+ }
381
+
382
+ .tl-speed {
383
+ font-size: 10px;
384
+ opacity: 0.6;
385
+ cursor: pointer;
386
+ min-width: 30px;
387
+ text-align: center;
388
+ }
389
+
390
+ .tl-speed:hover { opacity: 1; }
391
+
392
+ /* ── Magic Move Transitions ── */
393
+ .ce.magic-move {
394
+ transition: left 0.6s cubic-bezier(0.4, 0, 0.2, 1),
395
+ top 0.6s cubic-bezier(0.4, 0, 0.2, 1),
396
+ width 0.6s cubic-bezier(0.4, 0, 0.2, 1),
397
+ height 0.6s cubic-bezier(0.4, 0, 0.2, 1),
398
+ opacity 0.4s ease;
399
+ }
400
+
401
+ .ce.magic-enter {
402
+ opacity: 0;
403
+ transform: scale(0.9);
404
+ }
405
+
406
+ .ce.magic-enter-active {
407
+ opacity: 1;
408
+ transform: scale(1);
409
+ transition: opacity 0.4s ease, transform 0.4s ease;
410
+ }
411
+
412
+ .ce.magic-exit {
413
+ opacity: 0;
414
+ transform: scale(0.9);
415
+ transition: opacity 0.3s ease, transform 0.3s ease;
416
+ pointer-events: none;
417
+ }
418
+
419
+ /* ── Empty State ── */
420
+ .empty-state {
421
+ position: absolute;
422
+ inset: 0;
423
+ display: flex;
424
+ flex-direction: column;
425
+ align-items: center;
426
+ justify-content: center;
427
+ gap: 12px;
428
+ color: var(--color-on-surface-muted, #888);
429
+ pointer-events: none;
430
+ }
431
+
432
+ .empty-state .hint { font-size: 14px; opacity: 0.6; }
433
+ </style>
434
+
435
+ <div class="turn-banner" id="turnBanner" data-turn="human">
436
+ <span id="turnAgent">human's turn</span>
437
+ <span class="turn-msg" id="turnMsg"></span>
438
+ <button class="turn-pass" id="turnPass">Pass to AI</button>
439
+ </div>
440
+
441
+ <div class="canvas-surface" id="surface"></div>
442
+
443
+ <div class="empty-state" id="empty">
444
+ <div style="font-size: 48px; opacity: 0.3;">+</div>
445
+ <div class="hint">Click + to add elements, or let AI place them</div>
446
+ </div>
447
+
448
+ <button class="export-btn" id="exportBtn" title="Export as photon">&#x2B07;</button>
449
+ <button class="insert-btn" id="insertBtn" title="Add element">+</button>
450
+
451
+ <div class="format-picker" id="formatPicker">
452
+ <input type="text" placeholder="Search formats..." id="formatSearch">
453
+ <div id="formatList"></div>
454
+ </div>
455
+
456
+ <div class="timeline-bar" id="timelineBar">
457
+ <button class="tl-btn" id="tlPlay" title="Play/Pause">&#9654;</button>
458
+ <span class="tl-speed" id="tlSpeed" title="Click to cycle speed">1x</span>
459
+ <span class="tl-label" id="tlLabel">0 / 0</span>
460
+ <input type="range" id="tlSlider" min="0" max="0" value="0">
461
+ <span class="tl-action" id="tlAction"></span>
462
+ <button class="tl-btn" id="tlRestore">Restore</button>
463
+ <button class="tl-btn" id="tlCheckpoint">Checkpoint</button>
464
+ </div>
465
+
466
+ <script>
467
+ (function() {
468
+ 'use strict';
469
+
470
+ var exportBtn = document.getElementById('exportBtn');
471
+ var surface = document.getElementById('surface');
472
+ var emptyEl = document.getElementById('empty');
473
+ var insertBtn = document.getElementById('insertBtn');
474
+ var formatPicker = document.getElementById('formatPicker');
475
+ var formatSearch = document.getElementById('formatSearch');
476
+ var formatList = document.getElementById('formatList');
477
+ var turnBanner = document.getElementById('turnBanner');
478
+ var turnAgentEl = document.getElementById('turnAgent');
479
+ var turnMsgEl = document.getElementById('turnMsg');
480
+ var turnPassBtn = document.getElementById('turnPass');
481
+
482
+ // ── Scene State (local mirror) ──
483
+ var elements = {}; // id → { ...CanvasElement }
484
+ var containers = {}; // id → HTMLElement
485
+ var selected = null; // currently selected element ID
486
+ var customComponents = {}; // name → { html, defaults }
487
+
488
+ // ── Custom HTML Rendering ──
489
+
490
+ function renderCustomHTML(container, html, css) {
491
+ var iframe = container.querySelector('iframe.ce-custom');
492
+ if (!iframe) {
493
+ iframe = document.createElement('iframe');
494
+ iframe.className = 'ce-custom';
495
+ iframe.style.cssText = 'width:100%;height:100%;border:none;background:transparent;';
496
+ iframe.setAttribute('sandbox', 'allow-scripts');
497
+ container.innerHTML = '';
498
+ container.appendChild(iframe);
499
+ }
500
+ var doc = iframe.contentDocument || iframe.contentWindow.document;
501
+ var styleBlock = css ? '<style>' + css + '</style>' : '';
502
+ doc.open();
503
+ doc.write('<!DOCTYPE html><html><head><meta charset="utf-8">' +
504
+ '<style>*{box-sizing:border-box;margin:0;padding:0}body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;color:#e6e6e6;background:transparent;padding:4px;font-size:13px}</style>' +
505
+ styleBlock + '</head><body>' + html + '</body></html>');
506
+ doc.close();
507
+ }
508
+
509
+ function bindTemplate(tpl, data) {
510
+ if (!data || typeof data !== 'object') return tpl;
511
+ return tpl.replace(/\{\{(\w+)\}\}/g, function(_, key) {
512
+ return data[key] !== undefined ? String(data[key]) : '';
513
+ });
514
+ }
515
+ var currentTurn = { agent: 'human', message: '', since: 0 };
516
+
517
+ // ── Agent color map (deterministic for unknown agents) ──
518
+ var AGENT_COLORS = {
519
+ ai: '#6366f1',
520
+ human: '#22c55e',
521
+ 'agent-1': '#f59e0b',
522
+ 'agent-2': '#ec4899',
523
+ 'agent-3': '#06b6d4',
524
+ };
525
+ var EXTRA_COLORS = ['#f97316', '#8b5cf6', '#14b8a6', '#e11d48', '#84cc16'];
526
+
527
+ function agentColor(name) {
528
+ if (AGENT_COLORS[name]) return AGENT_COLORS[name];
529
+ // Deterministic hash for unknown agents
530
+ var hash = 0;
531
+ for (var i = 0; i < (name || '').length; i++) hash = (hash * 31 + name.charCodeAt(i)) | 0;
532
+ return EXTRA_COLORS[Math.abs(hash) % EXTRA_COLORS.length];
533
+ }
534
+
535
+ // ── Turn Banner ──
536
+
537
+ function updateTurnBanner(turn) {
538
+ if (!turn) return;
539
+ currentTurn = turn;
540
+ var isHuman = turn.agent === 'human';
541
+ turnBanner.setAttribute('data-turn', isHuman ? 'human' : 'ai');
542
+ turnBanner.style.borderBottom = '2px solid ' + agentColor(turn.agent);
543
+ turnAgentEl.textContent = turn.agent + "'s turn";
544
+ turnMsgEl.textContent = turn.message || '';
545
+ turnPassBtn.textContent = isHuman ? 'Pass to AI' : 'Take control';
546
+ }
547
+
548
+ turnPassBtn.addEventListener('click', function() {
549
+ var next = currentTurn.agent === 'human' ? 'ai' : 'human';
550
+ window.photon.callTool('pass', { to: next });
551
+ });
552
+
553
+ // ── Rendering ──
554
+
555
+ function upsertElement(el) {
556
+ elements[el.id] = el;
557
+
558
+ var container = containers[el.id];
559
+ if (!container) {
560
+ container = createContainer(el);
561
+ containers[el.id] = container;
562
+ surface.appendChild(container);
563
+ }
564
+
565
+ // Update position/size
566
+ container.style.left = el.x + 'px';
567
+ container.style.top = el.y + 'px';
568
+ container.style.width = el.w + 'px';
569
+ container.style.height = el.h + 'px';
570
+ container.style.zIndex = el.z;
571
+
572
+ // Update label
573
+ var labelEl = container.querySelector('.ce-label');
574
+ if (labelEl) labelEl.textContent = el.label || el.id;
575
+
576
+ var formatEl = container.querySelector('.ce-format');
577
+ if (formatEl) formatEl.textContent = el.format;
578
+
579
+ // Agent color on title bar
580
+ var agent = el.createdBy || 'ai';
581
+ container.setAttribute('data-agent', agent);
582
+ var bar = container.querySelector('.ce-bar');
583
+ if (bar && !AGENT_COLORS[agent]) {
584
+ bar.style.borderLeftColor = agentColor(agent);
585
+ }
586
+
587
+ // Lock state
588
+ var lockEl = container.querySelector('.ce-lock');
589
+ if (el.locked) {
590
+ container.classList.add('locked');
591
+ if (lockEl) lockEl.textContent = '\uD83D\uDD12 ' + el.locked;
592
+ } else {
593
+ container.classList.remove('locked');
594
+ if (lockEl) lockEl.textContent = '';
595
+ }
596
+
597
+ // Re-render content
598
+ var body = container.querySelector('.ce-body');
599
+ if (body && el.data !== undefined && el.data !== null) {
600
+ if (el.format === '_custom' && el.data.html) {
601
+ // AI-generated custom HTML — render in sandboxed iframe
602
+ renderCustomHTML(body, el.data.html, el.data.css);
603
+ } else if (customComponents[el.format]) {
604
+ // Registered custom component — template with data binding
605
+ var tpl = customComponents[el.format].html;
606
+ var rendered = bindTemplate(tpl, el.data);
607
+ renderCustomHTML(body, rendered);
608
+ } else {
609
+ window.photon.render(body, el.data, el.format);
610
+ }
611
+ }
612
+
613
+ updateEmpty();
614
+ }
615
+
616
+ function removeElement(id) {
617
+ delete elements[id];
618
+ if (containers[id]) {
619
+ containers[id].remove();
620
+ delete containers[id];
621
+ }
622
+ if (selected === id) selected = null;
623
+ updateEmpty();
624
+ }
625
+
626
+ function clearAll() {
627
+ for (var id in containers) containers[id].remove();
628
+ elements = {};
629
+ containers = {};
630
+ selected = null;
631
+ updateEmpty();
632
+ }
633
+
634
+ function updateEmpty() {
635
+ emptyEl.style.display = Object.keys(elements).length === 0 ? 'flex' : 'none';
636
+ }
637
+
638
+ // ── Create Element Container ──
639
+
640
+ function createContainer(el) {
641
+ var div = document.createElement('div');
642
+ div.className = 'ce';
643
+ div.setAttribute('data-id', el.id);
644
+
645
+ // Title bar
646
+ var bar = document.createElement('div');
647
+ bar.className = 'ce-bar';
648
+
649
+ var label = document.createElement('span');
650
+ label.className = 'ce-label';
651
+ label.textContent = el.label || el.id;
652
+
653
+ var fmt = document.createElement('span');
654
+ fmt.className = 'ce-format';
655
+ fmt.textContent = el.format;
656
+
657
+ var lockIcon = document.createElement('span');
658
+ lockIcon.className = 'ce-lock';
659
+
660
+ var del = document.createElement('span');
661
+ del.className = 'ce-delete';
662
+ del.textContent = '\u00d7';
663
+ del.addEventListener('click', function(e) {
664
+ e.stopPropagation();
665
+ if (elements[el.id] && elements[el.id].locked) return; // locked — no delete
666
+ window.photon.callTool('remove', { id: el.id });
667
+ });
668
+
669
+ bar.appendChild(label);
670
+ bar.appendChild(lockIcon);
671
+ bar.appendChild(fmt);
672
+ bar.appendChild(del);
673
+
674
+ // Double-click label to rename
675
+ label.addEventListener('dblclick', function(e) {
676
+ e.stopPropagation();
677
+ var current = label.textContent;
678
+ var input = document.createElement('input');
679
+ input.type = 'text';
680
+ input.value = current;
681
+ input.style.cssText = 'background:transparent;border:1px solid var(--color-primary,#6366f1);color:inherit;font:inherit;width:100%;padding:0 2px;border-radius:3px;outline:none;';
682
+ label.textContent = '';
683
+ label.appendChild(input);
684
+ input.focus();
685
+ input.select();
686
+
687
+ function commit() {
688
+ var val = input.value.trim() || current;
689
+ label.textContent = val;
690
+ if (val !== current) {
691
+ window.photon.callTool('put', { id: el.id, label: val });
692
+ }
693
+ }
694
+ input.addEventListener('blur', commit);
695
+ input.addEventListener('keydown', function(ev) {
696
+ if (ev.key === 'Enter') { ev.preventDefault(); input.blur(); }
697
+ if (ev.key === 'Escape') { input.value = current; input.blur(); }
698
+ });
699
+ });
700
+
701
+ // Body
702
+ var body = document.createElement('div');
703
+ body.className = 'ce-body';
704
+
705
+ // Resize handles (all 4 corners)
706
+ var corners = ['se', 'sw', 'ne', 'nw'];
707
+ var resizeHandles = {};
708
+ for (var ci = 0; ci < corners.length; ci++) {
709
+ var rh = document.createElement('div');
710
+ rh.className = 'ce-resize ce-resize-' + corners[ci];
711
+ resizeHandles[corners[ci]] = rh;
712
+ div.appendChild(rh);
713
+ }
714
+
715
+ div.appendChild(bar);
716
+ div.appendChild(body);
717
+
718
+ // ── Selection ──
719
+ div.addEventListener('pointerdown', function() {
720
+ selectElement(el.id);
721
+ });
722
+
723
+ // ── Drag (title bar) ──
724
+ setupDrag(bar, el.id);
725
+
726
+ // ── Resize (all corners) ──
727
+ for (var ri = 0; ri < corners.length; ri++) {
728
+ setupCornerResize(resizeHandles[corners[ri]], el.id, corners[ri]);
729
+ }
730
+
731
+ return div;
732
+ }
733
+
734
+ function selectElement(id) {
735
+ // Deselect previous
736
+ if (selected && containers[selected]) {
737
+ containers[selected].classList.remove('selected');
738
+ }
739
+ selected = id;
740
+ if (containers[id]) {
741
+ containers[id].classList.add('selected');
742
+ // Bring to front
743
+ var maxZ = 0;
744
+ for (var k in elements) {
745
+ if (elements[k].z > maxZ) maxZ = elements[k].z;
746
+ }
747
+ if (elements[id] && elements[id].z <= maxZ) {
748
+ var newZ = maxZ + 1;
749
+ elements[id].z = newZ;
750
+ containers[id].style.zIndex = newZ;
751
+ // Sync z-order to server
752
+ window.photon.callTool('put', { id: id, z: newZ });
753
+ }
754
+ }
755
+ }
756
+
757
+ // ── Drag Logic ──
758
+
759
+ function setupDrag(handle, id) {
760
+ var dragging = false;
761
+ var startX, startY, origX, origY;
762
+
763
+ handle.addEventListener('pointerdown', function(e) {
764
+ if (e.button !== 0) return;
765
+ var el = elements[id];
766
+ if (el && el.locked) return; // locked — no drag
767
+ e.preventDefault();
768
+ e.stopPropagation();
769
+ pushUndo();
770
+ dragging = true;
771
+ startX = e.clientX;
772
+ startY = e.clientY;
773
+ origX = el ? el.x : 0;
774
+ origY = el ? el.y : 0;
775
+ handle.setPointerCapture(e.pointerId);
776
+ selectElement(id);
777
+ });
778
+
779
+ handle.addEventListener('pointermove', function(e) {
780
+ if (!dragging) return;
781
+ var dx = e.clientX - startX;
782
+ var dy = e.clientY - startY;
783
+ var newX = Math.max(0, origX + dx);
784
+ var newY = Math.max(0, origY + dy);
785
+ var container = containers[id];
786
+ if (container) {
787
+ container.style.left = newX + 'px';
788
+ container.style.top = newY + 'px';
789
+ }
790
+ });
791
+
792
+ handle.addEventListener('pointerup', function(e) {
793
+ if (!dragging) return;
794
+ dragging = false;
795
+ var dx = e.clientX - startX;
796
+ var dy = e.clientY - startY;
797
+ var newX = Math.max(0, origX + dx);
798
+ var newY = Math.max(0, origY + dy);
799
+ // Sync to server
800
+ window.photon.callTool('put', { id: id, x: newX, y: newY });
801
+ });
802
+ }
803
+
804
+ // ── Resize Logic (all corners) ──
805
+
806
+ function setupCornerResize(handle, id, corner) {
807
+ var resizing = false;
808
+ var startX, startY, origX, origY, origW, origH;
809
+
810
+ handle.addEventListener('pointerdown', function(e) {
811
+ if (e.button !== 0) return;
812
+ var el = elements[id];
813
+ if (el && el.locked) return; // locked — no resize
814
+ e.preventDefault();
815
+ e.stopPropagation();
816
+ pushUndo();
817
+ resizing = true;
818
+ startX = e.clientX;
819
+ startY = e.clientY;
820
+ var el = elements[id];
821
+ origX = el ? el.x : 0;
822
+ origY = el ? el.y : 0;
823
+ origW = el ? el.w : 300;
824
+ origH = el ? el.h : 200;
825
+ handle.setPointerCapture(e.pointerId);
826
+ });
827
+
828
+ handle.addEventListener('pointermove', function(e) {
829
+ if (!resizing) return;
830
+ var dx = e.clientX - startX;
831
+ var dy = e.clientY - startY;
832
+ var result = calcCornerResize(corner, origX, origY, origW, origH, dx, dy);
833
+ var container = containers[id];
834
+ if (container) {
835
+ container.style.left = result.x + 'px';
836
+ container.style.top = result.y + 'px';
837
+ container.style.width = result.w + 'px';
838
+ container.style.height = result.h + 'px';
839
+ }
840
+ });
841
+
842
+ handle.addEventListener('pointerup', function(e) {
843
+ if (!resizing) return;
844
+ resizing = false;
845
+ var dx = e.clientX - startX;
846
+ var dy = e.clientY - startY;
847
+ var result = calcCornerResize(corner, origX, origY, origW, origH, dx, dy);
848
+ window.photon.callTool('put', { id: id, x: result.x, y: result.y, w: result.w, h: result.h });
849
+ });
850
+ }
851
+
852
+ function calcCornerResize(corner, ox, oy, ow, oh, dx, dy) {
853
+ var x = ox, y = oy, w = ow, h = oh;
854
+ if (corner === 'se') {
855
+ w = Math.max(120, ow + dx);
856
+ h = Math.max(80, oh + dy);
857
+ } else if (corner === 'sw') {
858
+ w = Math.max(120, ow - dx);
859
+ h = Math.max(80, oh + dy);
860
+ x = ox + ow - w;
861
+ } else if (corner === 'ne') {
862
+ w = Math.max(120, ow + dx);
863
+ h = Math.max(80, oh - dy);
864
+ y = oy + oh - h;
865
+ } else if (corner === 'nw') {
866
+ w = Math.max(120, ow - dx);
867
+ h = Math.max(80, oh - dy);
868
+ x = ox + ow - w;
869
+ y = oy + oh - h;
870
+ }
871
+ return { x: Math.max(0, x), y: Math.max(0, y), w: w, h: h };
872
+ }
873
+
874
+ // ── Deselect + Pan on surface click ──
875
+ var panning = false;
876
+ var panStartX, panStartY, panScrollX, panScrollY;
877
+
878
+ surface.addEventListener('pointerdown', function(e) {
879
+ if (e.target === surface || e.target === emptyEl || e.target.closest('.empty-state')) {
880
+ if (selected && containers[selected]) {
881
+ containers[selected].classList.remove('selected');
882
+ }
883
+ selected = null;
884
+
885
+ // Start panning
886
+ if (e.button === 0) {
887
+ panning = true;
888
+ panStartX = e.clientX;
889
+ panStartY = e.clientY;
890
+ panScrollX = surface.scrollLeft;
891
+ panScrollY = surface.scrollTop;
892
+ surface.style.cursor = 'grabbing';
893
+ surface.setPointerCapture(e.pointerId);
894
+ }
895
+ }
896
+ });
897
+
898
+ surface.addEventListener('pointermove', function(e) {
899
+ if (!panning) return;
900
+ var dx = e.clientX - panStartX;
901
+ var dy = e.clientY - panStartY;
902
+ surface.scrollLeft = panScrollX - dx;
903
+ surface.scrollTop = panScrollY - dy;
904
+ });
905
+
906
+ surface.addEventListener('pointerup', function() {
907
+ if (panning) {
908
+ panning = false;
909
+ surface.style.cursor = 'default';
910
+ }
911
+ });
912
+
913
+ // ── Undo/Redo History ──
914
+ var undoStack = []; // snapshots of scene state
915
+ var redoStack = [];
916
+ var MAX_HISTORY = 50;
917
+
918
+ function snapshotScene() {
919
+ return JSON.stringify(elements);
920
+ }
921
+
922
+ function pushUndo() {
923
+ undoStack.push(snapshotScene());
924
+ if (undoStack.length > MAX_HISTORY) undoStack.shift();
925
+ redoStack = []; // new action clears redo
926
+ }
927
+
928
+ function restoreSnapshot(snapshot) {
929
+ var state = JSON.parse(snapshot);
930
+ // Remove elements not in snapshot
931
+ for (var id in elements) {
932
+ if (!(id in state)) {
933
+ window.photon.callTool('remove', { id: id });
934
+ }
935
+ }
936
+ // Upsert elements from snapshot
937
+ for (var sid in state) {
938
+ var el = state[sid];
939
+ window.photon.callTool('put', el);
940
+ }
941
+ }
942
+
943
+ // ── Keyboard shortcuts ──
944
+ document.addEventListener('keydown', function(e) {
945
+ // Don't handle if typing in an input
946
+ if (document.activeElement && document.activeElement.tagName === 'INPUT') return;
947
+
948
+ // Delete/Backspace — remove selected (unless locked)
949
+ if ((e.key === 'Delete' || e.key === 'Backspace') && selected) {
950
+ if (elements[selected] && elements[selected].locked) return;
951
+ pushUndo();
952
+ window.photon.callTool('remove', { id: selected });
953
+ return;
954
+ }
955
+
956
+ // Ctrl/Cmd+Z — Undo
957
+ if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
958
+ e.preventDefault();
959
+ if (undoStack.length === 0) return;
960
+ redoStack.push(snapshotScene());
961
+ restoreSnapshot(undoStack.pop());
962
+ return;
963
+ }
964
+
965
+ // Ctrl/Cmd+Shift+Z or Ctrl/Cmd+Y — Redo
966
+ if ((e.ctrlKey || e.metaKey) && (e.key === 'Z' || e.key === 'y')) {
967
+ e.preventDefault();
968
+ if (redoStack.length === 0) return;
969
+ undoStack.push(snapshotScene());
970
+ restoreSnapshot(redoStack.pop());
971
+ return;
972
+ }
973
+ });
974
+
975
+ // ── Format Picker ──
976
+
977
+ var pickerOpen = false;
978
+
979
+ insertBtn.addEventListener('click', function(e) {
980
+ e.stopPropagation();
981
+ pickerOpen = !pickerOpen;
982
+ formatPicker.classList.toggle('open', pickerOpen);
983
+ if (pickerOpen) {
984
+ formatSearch.value = '';
985
+ renderFormatList('');
986
+ formatSearch.focus();
987
+ }
988
+ });
989
+
990
+ document.addEventListener('pointerdown', function(e) {
991
+ if (pickerOpen && !formatPicker.contains(e.target) && e.target !== insertBtn) {
992
+ pickerOpen = false;
993
+ formatPicker.classList.remove('open');
994
+ }
995
+ });
996
+
997
+ formatSearch.addEventListener('input', function() {
998
+ renderFormatList(formatSearch.value.toLowerCase());
999
+ });
1000
+
1001
+ function renderFormatList(filter) {
1002
+ var formats = window.photon.formats || [];
1003
+ var catalog = {};
1004
+ // formats might be array of names or object catalog
1005
+ if (Array.isArray(formats)) {
1006
+ formats.forEach(function(f) { catalog[f] = { data: '' }; });
1007
+ } else {
1008
+ catalog = formats;
1009
+ }
1010
+
1011
+ var html = '';
1012
+ var keys = Object.keys(catalog).sort();
1013
+ for (var i = 0; i < keys.length; i++) {
1014
+ var name = keys[i];
1015
+ if (filter && name.indexOf(filter) === -1) continue;
1016
+ var spec = catalog[name] || {};
1017
+ html += '<div class="format-item" data-format="' + name + '">'
1018
+ + '<span class="fi-name">' + name + '</span>'
1019
+ + '<span class="fi-shape">' + (spec.data || '').substring(0, 30) + '</span>'
1020
+ + '</div>';
1021
+ }
1022
+ formatList.innerHTML = html || '<div style="padding:8px;opacity:0.5">No formats found</div>';
1023
+
1024
+ // Bind clicks
1025
+ var items = formatList.querySelectorAll('.format-item');
1026
+ for (var j = 0; j < items.length; j++) {
1027
+ items[j].addEventListener('click', function() {
1028
+ var fmt = this.getAttribute('data-format');
1029
+ insertElement(fmt);
1030
+ pickerOpen = false;
1031
+ formatPicker.classList.remove('open');
1032
+ });
1033
+ }
1034
+ }
1035
+
1036
+ function insertElement(format) {
1037
+ pushUndo();
1038
+ // Place near center of visible area
1039
+ var scrollX = surface.scrollLeft || 0;
1040
+ var scrollY = surface.scrollTop || 0;
1041
+ var viewW = surface.clientWidth;
1042
+ var viewH = surface.clientHeight;
1043
+ var x = scrollX + Math.round(viewW / 2) - 150 + Math.round(Math.random() * 40 - 20);
1044
+ var y = scrollY + Math.round(viewH / 2) - 100 + Math.round(Math.random() * 40 - 20);
1045
+
1046
+ var id = 'el_' + Date.now();
1047
+
1048
+ // Get example data from catalog
1049
+ var catalog = window.photon.formats || {};
1050
+ var spec = catalog[format];
1051
+ var data = spec && spec.example ? spec.example : null;
1052
+
1053
+ window.photon.callTool('put', {
1054
+ id: id,
1055
+ format: format,
1056
+ x: x,
1057
+ y: y,
1058
+ w: 300,
1059
+ h: 200,
1060
+ data: data,
1061
+ label: format,
1062
+ });
1063
+ }
1064
+
1065
+ // ── Event Subscriptions ──
1066
+
1067
+ // Initial scene from main() result
1068
+ window.photon.onResult(function(result) {
1069
+ if (result && result.elements) {
1070
+ for (var i = 0; i < result.elements.length; i++) {
1071
+ upsertElement(result.elements[i]);
1072
+ }
1073
+ }
1074
+ if (result && result.turn) {
1075
+ updateTurnBanner(result.turn);
1076
+ }
1077
+ updateEmpty();
1078
+ });
1079
+
1080
+ // Live updates from AI or other agents
1081
+ window.photon.onEmit(function(event) {
1082
+ if (!event) return;
1083
+ if (event.emit === 'scene:put' && event.element) {
1084
+ upsertElement(event.element);
1085
+ } else if (event.emit === 'scene:remove' && event.id) {
1086
+ removeElement(event.id);
1087
+ } else if (event.emit === 'scene:clear') {
1088
+ clearAll();
1089
+ } else if (event.emit === 'turn:change' && event.turn) {
1090
+ updateTurnBanner(event.turn);
1091
+ } else if (event.emit === 'scene:restore' && event.elements) {
1092
+ // Full scene replacement from timeline restore
1093
+ clearAll();
1094
+ for (var ri = 0; ri < event.elements.length; ri++) {
1095
+ upsertElement(event.elements[ri]);
1096
+ }
1097
+ loadTimeline();
1098
+ } else if (event.emit === 'canvas:screenshot-request') {
1099
+ captureScreenshot();
1100
+ } else if (event.emit === 'component:registered' && event.name) {
1101
+ customComponents[event.name] = { html: event.html, defaults: event.defaults };
1102
+ } else if (event.emit === 'timeline:checkpoint') {
1103
+ loadTimeline();
1104
+ }
1105
+ });
1106
+
1107
+ // ── Screenshot Capture ──
1108
+
1109
+ function captureScreenshot() {
1110
+ // Calculate bounds of all elements
1111
+ var minX = Infinity, minY = Infinity, maxX = 0, maxY = 0;
1112
+ for (var id in elements) {
1113
+ var el = elements[id];
1114
+ minX = Math.min(minX, el.x);
1115
+ minY = Math.min(minY, el.y);
1116
+ maxX = Math.max(maxX, el.x + el.w);
1117
+ maxY = Math.max(maxY, el.y + el.h);
1118
+ }
1119
+ if (minX === Infinity) return; // empty canvas
1120
+
1121
+ var pad = 20;
1122
+ var w = maxX - minX + pad * 2;
1123
+ var h = maxY - minY + pad * 2;
1124
+
1125
+ var canvas = document.createElement('canvas');
1126
+ canvas.width = w;
1127
+ canvas.height = h;
1128
+ var ctx = canvas.getContext('2d');
1129
+ if (!ctx) return;
1130
+
1131
+ // Background
1132
+ ctx.fillStyle = '#1a1b26';
1133
+ ctx.fillRect(0, 0, w, h);
1134
+
1135
+ // Draw each element as a simple rectangle with label
1136
+ var sorted = Object.values(elements).sort(function(a, b) { return a.z - b.z; });
1137
+ for (var i = 0; i < sorted.length; i++) {
1138
+ var el = sorted[i];
1139
+ var ex = el.x - minX + pad;
1140
+ var ey = el.y - minY + pad;
1141
+
1142
+ // Card background
1143
+ ctx.fillStyle = '#1e2030';
1144
+ ctx.fillRect(ex, ey, el.w, el.h);
1145
+
1146
+ // Agent color bar
1147
+ var color = agentColor(el.createdBy || 'ai');
1148
+ ctx.fillStyle = color;
1149
+ ctx.fillRect(ex, ey, 3, el.h);
1150
+
1151
+ // Title bar
1152
+ ctx.fillStyle = '#252738';
1153
+ ctx.fillRect(ex, ey, el.w, 24);
1154
+
1155
+ // Label text
1156
+ ctx.fillStyle = '#e6e6e6';
1157
+ ctx.font = '11px sans-serif';
1158
+ ctx.fillText((el.label || el.id).substring(0, 30), ex + 8, ey + 16);
1159
+
1160
+ // Format badge
1161
+ ctx.fillStyle = '#666';
1162
+ ctx.font = '9px monospace';
1163
+ ctx.fillText(el.format, ex + el.w - ctx.measureText(el.format).width - 8, ey + 16);
1164
+
1165
+ // Border
1166
+ ctx.strokeStyle = '#333';
1167
+ ctx.lineWidth = 1;
1168
+ ctx.strokeRect(ex, ey, el.w, el.h);
1169
+
1170
+ // Lock icon
1171
+ if (el.locked) {
1172
+ ctx.fillStyle = '#f59e0b';
1173
+ ctx.font = '10px sans-serif';
1174
+ ctx.fillText('\uD83D\uDD12', ex + el.w - 18, ey + el.h - 6);
1175
+ }
1176
+ }
1177
+
1178
+ var dataUrl = canvas.toDataURL('image/png');
1179
+ window.photon.callTool('capture', { dataUrl: dataUrl });
1180
+ }
1181
+
1182
+ // ── Export Button ──
1183
+
1184
+ exportBtn.addEventListener('click', function(e) {
1185
+ e.stopPropagation();
1186
+ var count = Object.keys(elements).length;
1187
+ if (count === 0) return;
1188
+
1189
+ // Inline name input
1190
+ var name = 'my-dashboard';
1191
+ var input = document.createElement('input');
1192
+ input.type = 'text';
1193
+ input.value = name;
1194
+ input.placeholder = 'photon name...';
1195
+ input.style.cssText = 'position:fixed;bottom:76px;right:80px;width:200px;padding:8px 10px;background:var(--color-surface-container,#1e2030);border:1px solid var(--color-primary,#6366f1);border-radius:6px;color:var(--color-on-surface,#e6e6e6);font-size:13px;z-index:10002;outline:none;';
1196
+ document.body.appendChild(input);
1197
+ input.focus();
1198
+ input.select();
1199
+
1200
+ function doExport() {
1201
+ var val = input.value.trim() || name;
1202
+ input.remove();
1203
+ window.photon.callTool('export', { name: val }).then(function(result) {
1204
+ if (result && result.files) {
1205
+ // Download the files as a JSON blob
1206
+ var blob = new Blob([JSON.stringify(result, null, 2)], { type: 'application/json' });
1207
+ var url = URL.createObjectURL(blob);
1208
+ var a = document.createElement('a');
1209
+ a.href = url;
1210
+ a.download = val + '.photon.json';
1211
+ a.click();
1212
+ URL.revokeObjectURL(url);
1213
+ }
1214
+ }).catch(function() {});
1215
+ }
1216
+
1217
+ input.addEventListener('keydown', function(ev) {
1218
+ if (ev.key === 'Enter') { ev.preventDefault(); doExport(); }
1219
+ if (ev.key === 'Escape') { input.remove(); }
1220
+ });
1221
+ input.addEventListener('blur', function() {
1222
+ setTimeout(function() { if (document.body.contains(input)) input.remove(); }, 200);
1223
+ });
1224
+ });
1225
+
1226
+ // ── Timeline Scrubber + Magic Move Playback ──
1227
+
1228
+ var timelineBar = document.getElementById('timelineBar');
1229
+ var tlPlay = document.getElementById('tlPlay');
1230
+ var tlSpeedEl = document.getElementById('tlSpeed');
1231
+ var tlSlider = document.getElementById('tlSlider');
1232
+ var tlLabel = document.getElementById('tlLabel');
1233
+ var tlAction = document.getElementById('tlAction');
1234
+ var tlRestore = document.getElementById('tlRestore');
1235
+ var tlCheckpoint = document.getElementById('tlCheckpoint');
1236
+
1237
+ var timelineData = []; // from history()
1238
+ var playbackFrames = null; // from playback() — full scene data per frame
1239
+ var isPlaying = false;
1240
+ var playTimer = null;
1241
+ var playSpeed = 1; // 0.5x, 1x, 2x, 4x
1242
+ var SPEEDS = [0.5, 1, 2, 4];
1243
+ var speedIdx = 1;
1244
+ var FRAME_DURATION = 1200; // ms per frame at 1x
1245
+
1246
+ function loadTimeline() {
1247
+ window.photon.callTool('history', {}).then(function(result) {
1248
+ if (!Array.isArray(result) || result.length === 0) return;
1249
+ timelineData = result;
1250
+ timelineBar.classList.add('visible');
1251
+ tlSlider.max = String(result.length - 1);
1252
+ tlSlider.value = String(result.length - 1);
1253
+ updateTimelineLabel(result.length - 1);
1254
+ }).catch(function() {});
1255
+ }
1256
+
1257
+ function updateTimelineLabel(idx) {
1258
+ var entry = timelineData[idx];
1259
+ if (!entry) return;
1260
+ tlLabel.textContent = (idx + 1) + ' / ' + timelineData.length;
1261
+ tlAction.textContent = entry.action + ' (' + entry.elements + ' els)';
1262
+ }
1263
+
1264
+ tlSlider.addEventListener('input', function() {
1265
+ var idx = parseInt(tlSlider.value, 10);
1266
+ updateTimelineLabel(idx);
1267
+ if (playbackFrames && playbackFrames[idx]) {
1268
+ magicMoveToFrame(playbackFrames[idx].elements);
1269
+ }
1270
+ });
1271
+
1272
+ // ── Play/Pause ──
1273
+
1274
+ tlPlay.addEventListener('click', function() {
1275
+ if (isPlaying) {
1276
+ stopPlayback();
1277
+ } else {
1278
+ startPlayback();
1279
+ }
1280
+ });
1281
+
1282
+ function startPlayback() {
1283
+ // Load full playback data first
1284
+ window.photon.callTool('playback', {}).then(function(frames) {
1285
+ if (!Array.isArray(frames) || frames.length < 2) return;
1286
+ playbackFrames = frames;
1287
+ isPlaying = true;
1288
+ tlPlay.innerHTML = '&#9646;&#9646;'; // pause icon
1289
+ tlPlay.classList.add('playing');
1290
+
1291
+ // Start from beginning or current position
1292
+ var startIdx = parseInt(tlSlider.value, 10);
1293
+ if (startIdx >= frames.length - 1) startIdx = 0;
1294
+ tlSlider.value = String(startIdx);
1295
+
1296
+ advanceFrame();
1297
+ }).catch(function() {});
1298
+ }
1299
+
1300
+ function stopPlayback() {
1301
+ isPlaying = false;
1302
+ if (playTimer) clearTimeout(playTimer);
1303
+ playTimer = null;
1304
+ tlPlay.innerHTML = '&#9654;'; // play icon
1305
+ tlPlay.classList.remove('playing');
1306
+ }
1307
+
1308
+ function advanceFrame() {
1309
+ if (!isPlaying || !playbackFrames) return;
1310
+ var idx = parseInt(tlSlider.value, 10);
1311
+ if (idx >= playbackFrames.length - 1) {
1312
+ stopPlayback();
1313
+ return;
1314
+ }
1315
+
1316
+ var nextIdx = idx + 1;
1317
+ tlSlider.value = String(nextIdx);
1318
+ updateTimelineLabel(nextIdx);
1319
+
1320
+ // Magic Move to next frame
1321
+ magicMoveToFrame(playbackFrames[nextIdx].elements);
1322
+
1323
+ // Schedule next frame
1324
+ var delay = Math.round(FRAME_DURATION / playSpeed);
1325
+ playTimer = setTimeout(advanceFrame, delay);
1326
+ }
1327
+
1328
+ // ── Speed Control ──
1329
+
1330
+ tlSpeedEl.addEventListener('click', function() {
1331
+ speedIdx = (speedIdx + 1) % SPEEDS.length;
1332
+ playSpeed = SPEEDS[speedIdx];
1333
+ tlSpeedEl.textContent = playSpeed + 'x';
1334
+ });
1335
+
1336
+ // ── Magic Move Engine ──
1337
+
1338
+ function magicMoveToFrame(frameElements) {
1339
+ var frameMap = {};
1340
+ for (var i = 0; i < frameElements.length; i++) {
1341
+ frameMap[frameElements[i].id] = frameElements[i];
1342
+ }
1343
+
1344
+ // Enable transitions on all existing elements
1345
+ for (var id in containers) {
1346
+ containers[id].classList.add('magic-move');
1347
+ }
1348
+
1349
+ // Elements that exist in frame but not on canvas → appear
1350
+ for (var fid in frameMap) {
1351
+ var fel = frameMap[fid];
1352
+ if (!containers[fid]) {
1353
+ // Create container with enter animation
1354
+ var container = createContainer(fel);
1355
+ container.classList.add('magic-move', 'magic-enter');
1356
+ containers[fid] = container;
1357
+ surface.appendChild(container);
1358
+ // Position it
1359
+ container.style.left = fel.x + 'px';
1360
+ container.style.top = fel.y + 'px';
1361
+ container.style.width = fel.w + 'px';
1362
+ container.style.height = fel.h + 'px';
1363
+ container.style.zIndex = fel.z;
1364
+ // Trigger enter animation
1365
+ requestAnimationFrame(function(c) {
1366
+ return function() {
1367
+ c.classList.remove('magic-enter');
1368
+ c.classList.add('magic-enter-active');
1369
+ };
1370
+ }(container));
1371
+ }
1372
+ // Update element data + position (CSS transition handles the animation)
1373
+ elements[fid] = fel;
1374
+ upsertElement(fel);
1375
+ }
1376
+
1377
+ // Elements on canvas but not in frame → exit
1378
+ for (var eid in elements) {
1379
+ if (!(eid in frameMap)) {
1380
+ var exitContainer = containers[eid];
1381
+ if (exitContainer) {
1382
+ exitContainer.classList.add('magic-exit');
1383
+ // Remove after animation
1384
+ (function(removeId, removeContainer) {
1385
+ setTimeout(function() {
1386
+ delete elements[removeId];
1387
+ removeContainer.remove();
1388
+ delete containers[removeId];
1389
+ }, 350);
1390
+ })(eid, exitContainer);
1391
+ } else {
1392
+ delete elements[eid];
1393
+ }
1394
+ }
1395
+ }
1396
+
1397
+ updateEmpty();
1398
+
1399
+ // Clean up transition classes after animation completes
1400
+ setTimeout(function() {
1401
+ for (var cid in containers) {
1402
+ containers[cid].classList.remove('magic-move', 'magic-enter-active');
1403
+ }
1404
+ }, 700);
1405
+ }
1406
+
1407
+ // ── Restore ──
1408
+
1409
+ tlRestore.addEventListener('click', function() {
1410
+ var idx = parseInt(tlSlider.value, 10);
1411
+ stopPlayback();
1412
+ window.photon.callTool('restore', { index: idx }).then(function() {
1413
+ loadTimeline();
1414
+ }).catch(function() {});
1415
+ });
1416
+
1417
+ // ── Checkpoint ──
1418
+
1419
+ tlCheckpoint.addEventListener('click', function() {
1420
+ var label = 'manual checkpoint';
1421
+ var input = document.createElement('input');
1422
+ input.type = 'text';
1423
+ input.value = label;
1424
+ input.placeholder = 'Checkpoint name...';
1425
+ input.style.cssText = 'position:fixed;bottom:44px;right:12px;width:200px;padding:6px 8px;background:var(--color-surface-container,#1e2030);border:1px solid var(--color-primary,#6366f1);border-radius:4px;color:var(--color-on-surface,#e6e6e6);font-size:12px;z-index:10004;outline:none;';
1426
+ document.body.appendChild(input);
1427
+ input.focus();
1428
+ input.select();
1429
+
1430
+ function doCheckpoint() {
1431
+ var val = input.value.trim() || label;
1432
+ input.remove();
1433
+ window.photon.callTool('checkpoint', { label: val }).then(function() {
1434
+ loadTimeline();
1435
+ }).catch(function() {});
1436
+ }
1437
+
1438
+ input.addEventListener('keydown', function(ev) {
1439
+ if (ev.key === 'Enter') { ev.preventDefault(); doCheckpoint(); }
1440
+ if (ev.key === 'Escape') { input.remove(); }
1441
+ });
1442
+ input.addEventListener('blur', function() {
1443
+ setTimeout(function() { if (document.body.contains(input)) input.remove(); }, 200);
1444
+ });
1445
+ });
1446
+
1447
+ // ── Spacebar to play/pause ──
1448
+ document.addEventListener('keydown', function(e) {
1449
+ if (e.key === ' ' && document.activeElement && document.activeElement.tagName !== 'INPUT') {
1450
+ if (timelineData.length > 1) {
1451
+ e.preventDefault();
1452
+ if (isPlaying) stopPlayback();
1453
+ else startPlayback();
1454
+ }
1455
+ }
1456
+ });
1457
+
1458
+ // Load registered custom components
1459
+ setTimeout(function() {
1460
+ window.photon.callTool('listComponents', {}).then(function(result) {
1461
+ if (Array.isArray(result)) {
1462
+ for (var i = 0; i < result.length; i++) {
1463
+ var c = result[i];
1464
+ if (c.name && c.html) {
1465
+ customComponents[c.name] = { html: c.html, defaults: c.defaults || {} };
1466
+ }
1467
+ }
1468
+ }
1469
+ }).catch(function() {});
1470
+ }, 100);
1471
+
1472
+ // Reconcile on load — fetch latest scene in case events were missed
1473
+ setTimeout(function() {
1474
+ window.photon.callTool('scene', {}).then(function(result) {
1475
+ if (result && result.elements) {
1476
+ // Clear and re-render from server truth
1477
+ for (var id in containers) {
1478
+ if (!result.elements.find(function(e) { return e.id === id; })) {
1479
+ removeElement(id);
1480
+ }
1481
+ }
1482
+ for (var i = 0; i < result.elements.length; i++) {
1483
+ upsertElement(result.elements[i]);
1484
+ }
1485
+ }
1486
+ }).catch(function() {});
1487
+ }, 500);
1488
+
1489
+ // Load timeline after scene is ready
1490
+ setTimeout(function() { loadTimeline(); }, 800);
1491
+
1492
+ })();
1493
+ </script>