@portel/photon 1.20.1 → 1.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -5
- package/dist/ag-ui/adapter.d.ts +4 -1
- package/dist/ag-ui/adapter.d.ts.map +1 -1
- package/dist/ag-ui/adapter.js +58 -3
- package/dist/ag-ui/adapter.js.map +1 -1
- package/dist/ag-ui/types.d.ts +12 -0
- package/dist/ag-ui/types.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-browse.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-browse.js +8 -49
- package/dist/auto-ui/beam/routes/api-browse.js.map +1 -1
- package/dist/auto-ui/beam/routes/api-config.d.ts +1 -1
- package/dist/auto-ui/beam/routes/api-config.d.ts.map +1 -1
- package/dist/auto-ui/beam/routes/api-config.js +79 -1
- package/dist/auto-ui/beam/routes/api-config.js.map +1 -1
- package/dist/auto-ui/beam.d.ts.map +1 -1
- package/dist/auto-ui/beam.js +23 -31
- package/dist/auto-ui/beam.js.map +1 -1
- package/dist/auto-ui/bridge/index.d.ts.map +1 -1
- package/dist/auto-ui/bridge/index.js +107 -11
- package/dist/auto-ui/bridge/index.js.map +1 -1
- package/dist/auto-ui/bridge/renderers.d.ts +14 -0
- package/dist/auto-ui/bridge/renderers.d.ts.map +1 -1
- package/dist/auto-ui/bridge/renderers.js +680 -57
- package/dist/auto-ui/bridge/renderers.js.map +1 -1
- package/dist/auto-ui/frontend/index.html +3 -3
- package/dist/auto-ui/frontend/pure-view.html +19 -19
- package/dist/auto-ui/streamable-http-transport.d.ts.map +1 -1
- package/dist/auto-ui/streamable-http-transport.js +53 -2
- package/dist/auto-ui/streamable-http-transport.js.map +1 -1
- package/dist/auto-ui/ui-resolver.d.ts +25 -0
- package/dist/auto-ui/ui-resolver.d.ts.map +1 -0
- package/dist/auto-ui/ui-resolver.js +95 -0
- package/dist/auto-ui/ui-resolver.js.map +1 -0
- package/dist/beam-form.bundle.js +7 -7
- package/dist/beam-form.bundle.js.map +1 -1
- package/dist/beam.bundle.js +905 -185
- package/dist/beam.bundle.js.map +4 -4
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +9 -5
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/init.js +93 -53
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/publish.d.ts +14 -0
- package/dist/cli/commands/publish.d.ts.map +1 -0
- package/dist/cli/commands/publish.js +126 -0
- package/dist/cli/commands/publish.js.map +1 -0
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +2 -0
- package/dist/cli/commands/run.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +3 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +11 -1
- package/dist/cli.js.map +1 -1
- package/dist/context.d.ts +6 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +17 -5
- package/dist/context.js.map +1 -1
- package/dist/daemon/client.d.ts +9 -1
- package/dist/daemon/client.d.ts.map +1 -1
- package/dist/daemon/client.js +54 -1
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/manager.d.ts +3 -0
- package/dist/daemon/manager.d.ts.map +1 -1
- package/dist/daemon/manager.js +88 -38
- package/dist/daemon/manager.js.map +1 -1
- package/dist/daemon/ownership.d.ts +12 -0
- package/dist/daemon/ownership.d.ts.map +1 -0
- package/dist/daemon/ownership.js +55 -0
- package/dist/daemon/ownership.js.map +1 -0
- package/dist/daemon/protocol.d.ts +4 -2
- package/dist/daemon/protocol.d.ts.map +1 -1
- package/dist/daemon/protocol.js +15 -2
- package/dist/daemon/protocol.js.map +1 -1
- package/dist/daemon/server.js +557 -83
- package/dist/daemon/server.js.map +1 -1
- package/dist/daemon/session-manager.d.ts +9 -1
- package/dist/daemon/session-manager.d.ts.map +1 -1
- package/dist/daemon/session-manager.js +54 -1
- package/dist/daemon/session-manager.js.map +1 -1
- package/dist/daemon/worker-manager.d.ts +12 -0
- package/dist/daemon/worker-manager.d.ts.map +1 -1
- package/dist/daemon/worker-manager.js +89 -6
- package/dist/daemon/worker-manager.js.map +1 -1
- package/dist/loader.d.ts +17 -9
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +415 -141
- package/dist/loader.js.map +1 -1
- package/dist/photon-cli-runner.d.ts.map +1 -1
- package/dist/photon-cli-runner.js +26 -2
- package/dist/photon-cli-runner.js.map +1 -1
- package/dist/photons/canvas/ui/canvas.photon.html +1493 -0
- package/dist/photons/canvas.photon.d.ts +400 -0
- package/dist/photons/canvas.photon.d.ts.map +1 -0
- package/dist/photons/canvas.photon.js +662 -0
- package/dist/photons/canvas.photon.js.map +1 -0
- package/dist/photons/canvas.photon.ts +814 -0
- package/dist/photons/publish.photon.d.ts +97 -0
- package/dist/photons/publish.photon.d.ts.map +1 -0
- package/dist/photons/publish.photon.js +569 -0
- package/dist/photons/publish.photon.js.map +1 -0
- package/dist/photons/publish.photon.ts +683 -0
- package/dist/photons/ui/canvas.photon.html +624 -0
- package/dist/resource-server.d.ts.map +1 -1
- package/dist/resource-server.js +7 -1
- package/dist/resource-server.js.map +1 -1
- package/dist/server.d.ts +7 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +67 -37
- package/dist/server.js.map +1 -1
- package/dist/shared/error-handler.d.ts +1 -0
- package/dist/shared/error-handler.d.ts.map +1 -1
- package/dist/shared/error-handler.js +68 -10
- package/dist/shared/error-handler.js.map +1 -1
- package/dist/shared/logger.d.ts.map +1 -1
- package/dist/shared/logger.js +34 -0
- package/dist/shared/logger.js.map +1 -1
- package/dist/shared-utils.d.ts.map +1 -1
- package/dist/shared-utils.js +2 -2
- package/dist/shared-utils.js.map +1 -1
- package/dist/telemetry/context.d.ts +24 -0
- package/dist/telemetry/context.d.ts.map +1 -0
- package/dist/telemetry/context.js +17 -0
- package/dist/telemetry/context.js.map +1 -0
- package/dist/telemetry/logs.d.ts +38 -0
- package/dist/telemetry/logs.d.ts.map +1 -0
- package/dist/telemetry/logs.js +108 -0
- package/dist/telemetry/logs.js.map +1 -0
- package/dist/telemetry/metrics.d.ts +71 -0
- package/dist/telemetry/metrics.d.ts.map +1 -0
- package/dist/telemetry/metrics.js +184 -0
- package/dist/telemetry/metrics.js.map +1 -0
- package/dist/telemetry/otel.d.ts +20 -1
- package/dist/telemetry/otel.d.ts.map +1 -1
- package/dist/telemetry/otel.js +79 -2
- package/dist/telemetry/otel.js.map +1 -1
- package/dist/telemetry/sdk.d.ts +49 -0
- package/dist/telemetry/sdk.d.ts.map +1 -0
- package/dist/telemetry/sdk.js +110 -0
- package/dist/telemetry/sdk.js.map +1 -0
- package/dist/tsx-compiler.d.ts +23 -0
- package/dist/tsx-compiler.d.ts.map +1 -0
- package/dist/tsx-compiler.js +221 -0
- package/dist/tsx-compiler.js.map +1 -0
- 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">⬇</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">▶</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 = '▮▮'; // 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 = '▶'; // 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>
|