@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,662 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas — co-creative scene graph
|
|
3
|
+
*
|
|
4
|
+
* Shared canvas where AI agents and humans collaboratively place,
|
|
5
|
+
* move, resize, and update rendered elements. Either party can start,
|
|
6
|
+
* either can edit, control passes back and forth fluidly.
|
|
7
|
+
*
|
|
8
|
+
* Each element renders using the runtime's 50+ format renderers
|
|
9
|
+
* (metric, chart:bar, table, gauge, timeline, etc.).
|
|
10
|
+
*
|
|
11
|
+
* @description Co-creative canvas for AI and humans
|
|
12
|
+
* @icon 🎨
|
|
13
|
+
* @stateful
|
|
14
|
+
*/
|
|
15
|
+
export default class Canvas {
|
|
16
|
+
// Injected by runtime — declared for capability detection
|
|
17
|
+
emit;
|
|
18
|
+
formats;
|
|
19
|
+
memory;
|
|
20
|
+
/** Scene graph: element ID → element */
|
|
21
|
+
_scene = {};
|
|
22
|
+
_nextZ = 1;
|
|
23
|
+
_loaded = false;
|
|
24
|
+
/** Turn state: who has control */
|
|
25
|
+
_turn = { agent: 'human', since: Date.now() };
|
|
26
|
+
/** Max snapshots to keep per canvas */
|
|
27
|
+
static MAX_SNAPSHOTS = 100;
|
|
28
|
+
/** Load scene from persistent storage (or from fork data) */
|
|
29
|
+
async _load() {
|
|
30
|
+
if (this._loaded)
|
|
31
|
+
return;
|
|
32
|
+
this._loaded = true;
|
|
33
|
+
try {
|
|
34
|
+
const saved = await this.memory.get('scene');
|
|
35
|
+
if (saved) {
|
|
36
|
+
this._scene = saved.scene || {};
|
|
37
|
+
this._nextZ = saved.nextZ || 1;
|
|
38
|
+
if (saved.turn)
|
|
39
|
+
this._turn = saved.turn;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// First run or corrupted — start fresh
|
|
44
|
+
}
|
|
45
|
+
// Check for fork data in global scope (written by another instance's fork())
|
|
46
|
+
if (Object.keys(this._scene).length === 0) {
|
|
47
|
+
try {
|
|
48
|
+
const globalKeys = await this.memory.keys('global');
|
|
49
|
+
for (const key of globalKeys) {
|
|
50
|
+
// Match fork data addressed to this instance
|
|
51
|
+
if (key.startsWith('canvas-fork:')) {
|
|
52
|
+
const forkData = await this.memory.get(key, 'global');
|
|
53
|
+
if (forkData && forkData.scene) {
|
|
54
|
+
this._scene = forkData.scene;
|
|
55
|
+
this._nextZ = forkData.nextZ || 1;
|
|
56
|
+
this._turn = forkData.turn || this._turn;
|
|
57
|
+
await this.memory.delete(key, 'global'); // consume
|
|
58
|
+
await this._save('forked');
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Fork check failed — continue with empty scene
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** Save scene to persistent storage + auto-snapshot */
|
|
70
|
+
async _save(action) {
|
|
71
|
+
await this.memory.set('scene', {
|
|
72
|
+
scene: this._scene,
|
|
73
|
+
nextZ: this._nextZ,
|
|
74
|
+
turn: this._turn,
|
|
75
|
+
});
|
|
76
|
+
// Auto-snapshot: record every mutation
|
|
77
|
+
const timeline = (await this.memory.get('timeline')) || [];
|
|
78
|
+
timeline.push({
|
|
79
|
+
ts: Date.now(),
|
|
80
|
+
action: action || 'edit',
|
|
81
|
+
scene: JSON.parse(JSON.stringify(this._scene)),
|
|
82
|
+
elementCount: Object.keys(this._scene).length,
|
|
83
|
+
});
|
|
84
|
+
// Trim old snapshots
|
|
85
|
+
if (timeline.length > Canvas.MAX_SNAPSHOTS) {
|
|
86
|
+
timeline.splice(0, timeline.length - Canvas.MAX_SNAPSHOTS);
|
|
87
|
+
}
|
|
88
|
+
await this.memory.set('timeline', timeline);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Open the canvas
|
|
92
|
+
* @ui canvas
|
|
93
|
+
* @readOnly
|
|
94
|
+
*/
|
|
95
|
+
async main() {
|
|
96
|
+
await this._load();
|
|
97
|
+
return { elements: Object.values(this._scene), turn: this._turn };
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Place or update an element on the canvas.
|
|
101
|
+
* Creates if new, merges if exists — only provided fields change.
|
|
102
|
+
*
|
|
103
|
+
* @param id Element identifier
|
|
104
|
+
* @param format Renderer format (metric, chart:bar, table, gauge, etc.)
|
|
105
|
+
* @param data Data matching the format spec
|
|
106
|
+
* @param x Horizontal position in pixels
|
|
107
|
+
* @param y Vertical position in pixels
|
|
108
|
+
* @param w Width in pixels
|
|
109
|
+
* @param h Height in pixels
|
|
110
|
+
* @param z Z-order layer (higher = on top)
|
|
111
|
+
* @param label Human-readable label shown on the element
|
|
112
|
+
*/
|
|
113
|
+
async put({ id, format, data, x, y, w, h, z, label, }) {
|
|
114
|
+
await this._load();
|
|
115
|
+
const existing = this._scene[id];
|
|
116
|
+
const element = {
|
|
117
|
+
id,
|
|
118
|
+
format: format ?? existing?.format ?? 'card',
|
|
119
|
+
data: data !== undefined ? data : existing?.data,
|
|
120
|
+
x: x ?? existing?.x ?? 50,
|
|
121
|
+
y: y ?? existing?.y ?? 50,
|
|
122
|
+
w: w ?? existing?.w ?? 300,
|
|
123
|
+
h: h ?? existing?.h ?? 200,
|
|
124
|
+
z: z ?? existing?.z ?? this._nextZ++,
|
|
125
|
+
label: label ?? existing?.label,
|
|
126
|
+
createdBy: existing?.createdBy ?? 'ai',
|
|
127
|
+
updatedAt: Date.now(),
|
|
128
|
+
};
|
|
129
|
+
this._scene[id] = element;
|
|
130
|
+
const action = existing ? `update ${id}` : `add ${id}`;
|
|
131
|
+
await this._save(action);
|
|
132
|
+
// Emit scene change — flows through SSE → bridge → onEmit
|
|
133
|
+
this.emit({
|
|
134
|
+
emit: 'scene:put',
|
|
135
|
+
element,
|
|
136
|
+
});
|
|
137
|
+
return element;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Remove an element from the canvas
|
|
141
|
+
* @param id Element identifier to remove
|
|
142
|
+
*/
|
|
143
|
+
async remove({ id }) {
|
|
144
|
+
await this._load();
|
|
145
|
+
const existed = id in this._scene;
|
|
146
|
+
delete this._scene[id];
|
|
147
|
+
await this._save(`remove ${id}`);
|
|
148
|
+
this.emit({
|
|
149
|
+
emit: 'scene:remove',
|
|
150
|
+
id,
|
|
151
|
+
});
|
|
152
|
+
return { removed: existed, id };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Clear all elements from the canvas
|
|
156
|
+
* @destructive
|
|
157
|
+
*/
|
|
158
|
+
async clear() {
|
|
159
|
+
await this._load();
|
|
160
|
+
const count = Object.keys(this._scene).length;
|
|
161
|
+
this._scene = {};
|
|
162
|
+
this._nextZ = 1;
|
|
163
|
+
await this._save('clear');
|
|
164
|
+
this.emit({
|
|
165
|
+
emit: 'scene:clear',
|
|
166
|
+
});
|
|
167
|
+
return { cleared: count };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Get the full scene graph — all elements with positions, sizes, and data
|
|
171
|
+
* @readOnly
|
|
172
|
+
*/
|
|
173
|
+
async scene() {
|
|
174
|
+
await this._load();
|
|
175
|
+
return {
|
|
176
|
+
elements: Object.values(this._scene),
|
|
177
|
+
count: Object.keys(this._scene).length,
|
|
178
|
+
turn: this._turn,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Pass control to another agent or back to the human.
|
|
183
|
+
* The recipient sees a status banner with the optional message.
|
|
184
|
+
*
|
|
185
|
+
* @param to Who gets control next (e.g. 'human', 'ai', agent name)
|
|
186
|
+
* @param message Optional message explaining what to do next
|
|
187
|
+
*/
|
|
188
|
+
async pass({ to, message }) {
|
|
189
|
+
await this._load();
|
|
190
|
+
this._turn = { agent: to, message, since: Date.now() };
|
|
191
|
+
await this._save();
|
|
192
|
+
this.emit({
|
|
193
|
+
emit: 'turn:change',
|
|
194
|
+
turn: this._turn,
|
|
195
|
+
});
|
|
196
|
+
return this._turn;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Lock an element so only the specified agent can modify it.
|
|
200
|
+
*
|
|
201
|
+
* @param id Element to lock
|
|
202
|
+
* @param agent Agent name claiming the lock
|
|
203
|
+
*/
|
|
204
|
+
async lock({ id, agent }) {
|
|
205
|
+
await this._load();
|
|
206
|
+
const el = this._scene[id];
|
|
207
|
+
if (!el)
|
|
208
|
+
return { error: 'Element not found', id };
|
|
209
|
+
if (el.locked && el.locked !== agent) {
|
|
210
|
+
return { error: `Locked by ${el.locked}`, id };
|
|
211
|
+
}
|
|
212
|
+
el.locked = agent;
|
|
213
|
+
el.updatedAt = Date.now();
|
|
214
|
+
await this._save(`lock ${id}`);
|
|
215
|
+
this.emit({ emit: 'scene:put', element: el });
|
|
216
|
+
return el;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Unlock an element, allowing anyone to modify it.
|
|
220
|
+
*
|
|
221
|
+
* @param id Element to unlock
|
|
222
|
+
*/
|
|
223
|
+
async unlock({ id }) {
|
|
224
|
+
await this._load();
|
|
225
|
+
const el = this._scene[id];
|
|
226
|
+
if (!el)
|
|
227
|
+
return { error: 'Element not found', id };
|
|
228
|
+
delete el.locked;
|
|
229
|
+
el.updatedAt = Date.now();
|
|
230
|
+
await this._save(`unlock ${id}`);
|
|
231
|
+
this.emit({ emit: 'scene:put', element: el });
|
|
232
|
+
return el;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Describe the current canvas layout in natural language.
|
|
236
|
+
* Useful for AI agents to understand spatial arrangement without a screenshot.
|
|
237
|
+
* @readOnly
|
|
238
|
+
*/
|
|
239
|
+
async describe() {
|
|
240
|
+
await this._load();
|
|
241
|
+
const els = Object.values(this._scene);
|
|
242
|
+
if (els.length === 0) {
|
|
243
|
+
return {
|
|
244
|
+
description: 'The canvas is empty.',
|
|
245
|
+
count: 0,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
// Sort by z-order (back to front)
|
|
249
|
+
const sorted = [...els].sort((a, b) => a.z - b.z);
|
|
250
|
+
// Compute canvas bounds
|
|
251
|
+
const maxX = Math.max(...els.map((e) => e.x + e.w));
|
|
252
|
+
const maxY = Math.max(...els.map((e) => e.y + e.h));
|
|
253
|
+
// Describe each element with spatial context
|
|
254
|
+
const descriptions = sorted.map((el) => {
|
|
255
|
+
const cx = el.x + el.w / 2;
|
|
256
|
+
const cy = el.y + el.h / 2;
|
|
257
|
+
const hPos = cx < maxX * 0.33 ? 'left' : cx > maxX * 0.66 ? 'right' : 'center';
|
|
258
|
+
const vPos = cy < maxY * 0.33 ? 'top' : cy > maxY * 0.66 ? 'bottom' : 'middle';
|
|
259
|
+
const pos = vPos === 'middle' && hPos === 'center' ? 'center' : `${vPos}-${hPos}`;
|
|
260
|
+
const size = `${el.w}x${el.h}`;
|
|
261
|
+
const lock = el.locked ? ` [locked by ${el.locked}]` : '';
|
|
262
|
+
const label = el.label || el.id;
|
|
263
|
+
return `- "${label}" (${el.format}, ${size}) at ${pos}, placed by ${el.createdBy || 'unknown'}${lock}`;
|
|
264
|
+
});
|
|
265
|
+
// Detect spatial patterns
|
|
266
|
+
const patterns = [];
|
|
267
|
+
const xGroups = this._groupBy(els, (e) => Math.round(e.y / 50) * 50);
|
|
268
|
+
const rows = Object.values(xGroups).filter((g) => g.length > 1);
|
|
269
|
+
if (rows.length > 0) {
|
|
270
|
+
patterns.push(`${rows.length} row(s) of aligned elements`);
|
|
271
|
+
}
|
|
272
|
+
const yGroups = this._groupBy(els, (e) => Math.round(e.x / 50) * 50);
|
|
273
|
+
const cols = Object.values(yGroups).filter((g) => g.length > 1);
|
|
274
|
+
if (cols.length > 0) {
|
|
275
|
+
patterns.push(`${cols.length} column(s) of aligned elements`);
|
|
276
|
+
}
|
|
277
|
+
const summary = `Canvas has ${els.length} element(s) spanning ${maxX}x${maxY}px.` +
|
|
278
|
+
(patterns.length > 0 ? ' Layout: ' + patterns.join(', ') + '.' : '') +
|
|
279
|
+
` Turn: ${this._turn.agent}.`;
|
|
280
|
+
return {
|
|
281
|
+
description: summary,
|
|
282
|
+
elements: descriptions,
|
|
283
|
+
bounds: { width: maxX, height: maxY },
|
|
284
|
+
turn: this._turn,
|
|
285
|
+
count: els.length,
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
_groupBy(items, keyFn) {
|
|
289
|
+
const groups = {};
|
|
290
|
+
for (const item of items) {
|
|
291
|
+
const key = keyFn(item);
|
|
292
|
+
if (!groups[key])
|
|
293
|
+
groups[key] = [];
|
|
294
|
+
groups[key].push(item);
|
|
295
|
+
}
|
|
296
|
+
return groups;
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Request a screenshot from the canvas UI.
|
|
300
|
+
* Returns the latest captured screenshot as a base64 data URL.
|
|
301
|
+
* The UI captures the screenshot and sends it back via the capture method.
|
|
302
|
+
* @readOnly
|
|
303
|
+
*/
|
|
304
|
+
async screenshot() {
|
|
305
|
+
await this._load();
|
|
306
|
+
// Emit request — client will capture and call 'capture' with the data
|
|
307
|
+
this.emit({ emit: 'canvas:screenshot-request' });
|
|
308
|
+
// Return the last captured screenshot if available
|
|
309
|
+
const last = await this.memory.get('last-screenshot');
|
|
310
|
+
return {
|
|
311
|
+
available: !!last,
|
|
312
|
+
dataUrl: last || null,
|
|
313
|
+
hint: last
|
|
314
|
+
? 'Screenshot available. Pass the dataUrl to a multimodal model.'
|
|
315
|
+
: 'Screenshot requested. Call screenshot() again after a moment to retrieve it.',
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* Store a screenshot captured by the canvas UI.
|
|
320
|
+
* Called by the client after a screenshot-request event.
|
|
321
|
+
* @internal
|
|
322
|
+
* @param dataUrl Base64 data URL of the captured image
|
|
323
|
+
*/
|
|
324
|
+
async capture({ dataUrl }) {
|
|
325
|
+
await this.memory.set('last-screenshot', dataUrl);
|
|
326
|
+
return { stored: true };
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Register a custom component that can be used as a format on the canvas.
|
|
330
|
+
* Once registered, use the component name as the format in put().
|
|
331
|
+
*
|
|
332
|
+
* @param name Component name (used as format value in put)
|
|
333
|
+
* @param html HTML template string. Use {{key}} placeholders for data binding.
|
|
334
|
+
* @param defaults Default data values for the component
|
|
335
|
+
*/
|
|
336
|
+
async registerComponent({ name, html, defaults, }) {
|
|
337
|
+
await this._load();
|
|
338
|
+
const components = (await this.memory.get('components')) || {};
|
|
339
|
+
components[name] = { html, defaults: defaults || {} };
|
|
340
|
+
await this.memory.set('components', components);
|
|
341
|
+
this.emit({
|
|
342
|
+
emit: 'component:registered',
|
|
343
|
+
name,
|
|
344
|
+
html,
|
|
345
|
+
defaults: defaults || {},
|
|
346
|
+
});
|
|
347
|
+
return { registered: name };
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* List registered custom components with their templates
|
|
351
|
+
* @readOnly
|
|
352
|
+
*/
|
|
353
|
+
async listComponents() {
|
|
354
|
+
const components = (await this.memory.get('components')) || {};
|
|
355
|
+
return Object.entries(components).map(([name, spec]) => ({
|
|
356
|
+
name,
|
|
357
|
+
html: spec.html,
|
|
358
|
+
defaults: spec.defaults || {},
|
|
359
|
+
}));
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* View the canvas timeline — a history of every change.
|
|
363
|
+
* Each entry has a timestamp, action label, and element count.
|
|
364
|
+
* @readOnly
|
|
365
|
+
*/
|
|
366
|
+
async history() {
|
|
367
|
+
const timeline = (await this.memory.get('timeline')) || [];
|
|
368
|
+
return timeline.map((entry, i) => ({
|
|
369
|
+
index: i,
|
|
370
|
+
time: new Date(entry.ts).toISOString(),
|
|
371
|
+
action: entry.action,
|
|
372
|
+
elements: entry.elementCount,
|
|
373
|
+
}));
|
|
374
|
+
}
|
|
375
|
+
/**
|
|
376
|
+
* Get the full timeline with scene data for animated playback.
|
|
377
|
+
* Each frame contains the complete scene state so the client can
|
|
378
|
+
* compute Magic Move transitions between consecutive frames.
|
|
379
|
+
* @readOnly
|
|
380
|
+
* @internal
|
|
381
|
+
*/
|
|
382
|
+
async playback() {
|
|
383
|
+
const timeline = (await this.memory.get('timeline')) || [];
|
|
384
|
+
return timeline.map((entry, i) => ({
|
|
385
|
+
index: i,
|
|
386
|
+
action: entry.action,
|
|
387
|
+
elements: Object.values(entry.scene),
|
|
388
|
+
}));
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Save a named checkpoint at the current state.
|
|
392
|
+
* Checkpoints appear in the timeline with a label for easy reference.
|
|
393
|
+
*
|
|
394
|
+
* @param label Name for this checkpoint (e.g. 'before reorganizing')
|
|
395
|
+
*/
|
|
396
|
+
async checkpoint({ label }) {
|
|
397
|
+
await this._load();
|
|
398
|
+
await this._save(`checkpoint: ${label}`);
|
|
399
|
+
this.emit({
|
|
400
|
+
emit: 'timeline:checkpoint',
|
|
401
|
+
label,
|
|
402
|
+
ts: Date.now(),
|
|
403
|
+
});
|
|
404
|
+
return { checkpointed: label, elements: Object.keys(this._scene).length };
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Restore the canvas to a previous point in the timeline.
|
|
408
|
+
* Replaces the current scene with the snapshot at that index.
|
|
409
|
+
*
|
|
410
|
+
* @param index Timeline index to restore (from history())
|
|
411
|
+
*/
|
|
412
|
+
async restore({ index }) {
|
|
413
|
+
const timeline = (await this.memory.get('timeline')) || [];
|
|
414
|
+
if (index < 0 || index >= timeline.length) {
|
|
415
|
+
return { error: `Invalid index ${index}. Timeline has ${timeline.length} entries.` };
|
|
416
|
+
}
|
|
417
|
+
const snapshot = timeline[index];
|
|
418
|
+
this._scene = JSON.parse(JSON.stringify(snapshot.scene));
|
|
419
|
+
this._nextZ = Math.max(0, ...Object.values(this._scene).map((e) => e.z || 0)) + 1;
|
|
420
|
+
await this._save(`restore to #${index} (${snapshot.action})`);
|
|
421
|
+
// Emit full scene refresh
|
|
422
|
+
this.emit({
|
|
423
|
+
emit: 'scene:restore',
|
|
424
|
+
elements: Object.values(this._scene),
|
|
425
|
+
});
|
|
426
|
+
return {
|
|
427
|
+
restored: index,
|
|
428
|
+
action: snapshot.action,
|
|
429
|
+
time: new Date(snapshot.ts).toISOString(),
|
|
430
|
+
elements: Object.keys(this._scene).length,
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Fork the canvas at a timeline point into a new instance.
|
|
435
|
+
* Creates a new canvas instance with the scene from that snapshot.
|
|
436
|
+
*
|
|
437
|
+
* @param name New instance name (e.g. 'dashboard-v2')
|
|
438
|
+
* @param index Timeline index to fork from (defaults to current state)
|
|
439
|
+
*/
|
|
440
|
+
async fork({ name, index }) {
|
|
441
|
+
await this._load();
|
|
442
|
+
let forkScene;
|
|
443
|
+
let forkAction;
|
|
444
|
+
if (index !== undefined) {
|
|
445
|
+
const timeline = (await this.memory.get('timeline')) || [];
|
|
446
|
+
if (index < 0 || index >= timeline.length) {
|
|
447
|
+
return { error: `Invalid index ${index}. Timeline has ${timeline.length} entries.` };
|
|
448
|
+
}
|
|
449
|
+
forkScene = timeline[index].scene;
|
|
450
|
+
forkAction = `forked from #${index}`;
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
forkScene = this._scene;
|
|
454
|
+
forkAction = 'forked from current';
|
|
455
|
+
}
|
|
456
|
+
// Store fork data in global scope so the target instance can find it
|
|
457
|
+
const forkData = {
|
|
458
|
+
scene: JSON.parse(JSON.stringify(forkScene)),
|
|
459
|
+
nextZ: Math.max(0, ...Object.values(forkScene).map((e) => e.z || 0)) + 1,
|
|
460
|
+
turn: { agent: 'human', since: Date.now() },
|
|
461
|
+
};
|
|
462
|
+
await this.memory.set(`canvas-fork:${name}`, forkData, 'global');
|
|
463
|
+
this.emit({
|
|
464
|
+
emit: 'timeline:fork',
|
|
465
|
+
name,
|
|
466
|
+
elements: Object.keys(forkScene).length,
|
|
467
|
+
});
|
|
468
|
+
return {
|
|
469
|
+
forked: name,
|
|
470
|
+
from: forkAction,
|
|
471
|
+
elements: Object.keys(forkScene).length,
|
|
472
|
+
hint: `Open canvas/${name} to use the forked canvas`,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Export the current canvas as a standalone photon.
|
|
477
|
+
* Compiles the scene graph into a .photon.ts (data methods) and
|
|
478
|
+
* .photon.html (CSS grid layout with format renderers).
|
|
479
|
+
*
|
|
480
|
+
* @param name Photon name for the exported file (e.g. 'my-dashboard')
|
|
481
|
+
* @param description One-line description of the exported photon
|
|
482
|
+
* @readOnly
|
|
483
|
+
*/
|
|
484
|
+
async export({ name, description }) {
|
|
485
|
+
await this._load();
|
|
486
|
+
const els = Object.values(this._scene);
|
|
487
|
+
if (els.length === 0) {
|
|
488
|
+
return { error: 'Canvas is empty — nothing to export' };
|
|
489
|
+
}
|
|
490
|
+
const desc = description || `Exported from canvas on ${new Date().toISOString().split('T')[0]}`;
|
|
491
|
+
const sorted = [...els].sort((a, b) => a.z - b.z);
|
|
492
|
+
// ── Detect layout: rows and columns ──
|
|
493
|
+
const rows = this._detectRows(sorted);
|
|
494
|
+
// ── Generate .photon.ts ──
|
|
495
|
+
const methodEntries = sorted.map((el) => {
|
|
496
|
+
const safeName = el.id.replace(/[^a-zA-Z0-9]/g, '_');
|
|
497
|
+
const dataStr = JSON.stringify(el.data, null, 4);
|
|
498
|
+
return [
|
|
499
|
+
` /**`,
|
|
500
|
+
` * ${el.label || el.id}`,
|
|
501
|
+
` * @format ${el.format}`,
|
|
502
|
+
` * @readOnly`,
|
|
503
|
+
` */`,
|
|
504
|
+
` ${safeName}() {`,
|
|
505
|
+
` return ${dataStr};`,
|
|
506
|
+
` }`,
|
|
507
|
+
].join('\n');
|
|
508
|
+
});
|
|
509
|
+
const tsFile = [
|
|
510
|
+
`/**`,
|
|
511
|
+
` * ${name}`,
|
|
512
|
+
` *`,
|
|
513
|
+
` * ${desc}`,
|
|
514
|
+
` *`,
|
|
515
|
+
` * @description ${desc}`,
|
|
516
|
+
` * @ui main`,
|
|
517
|
+
` */`,
|
|
518
|
+
`export default class ${this._toPascalCase(name)} {`,
|
|
519
|
+
` /**`,
|
|
520
|
+
` * Dashboard view`,
|
|
521
|
+
` * @ui main`,
|
|
522
|
+
` * @readOnly`,
|
|
523
|
+
` */`,
|
|
524
|
+
` main() {`,
|
|
525
|
+
` return {`,
|
|
526
|
+
...sorted.map((el) => {
|
|
527
|
+
const safeName = el.id.replace(/[^a-zA-Z0-9]/g, '_');
|
|
528
|
+
return ` ${safeName}: this.${safeName}(),`;
|
|
529
|
+
}),
|
|
530
|
+
` };`,
|
|
531
|
+
` }`,
|
|
532
|
+
``,
|
|
533
|
+
...methodEntries,
|
|
534
|
+
`}`,
|
|
535
|
+
``,
|
|
536
|
+
].join('\n');
|
|
537
|
+
// ── Generate .photon.html with CSS grid ──
|
|
538
|
+
const gridCells = rows
|
|
539
|
+
.map((row, ri) => row
|
|
540
|
+
.map((el) => {
|
|
541
|
+
const safeName = el.id.replace(/[^a-zA-Z0-9]/g, '_');
|
|
542
|
+
return [
|
|
543
|
+
` <div class="cell" data-method="${safeName}" data-format="${el.format}">`,
|
|
544
|
+
` <div class="cell-label">${el.label || el.id}</div>`,
|
|
545
|
+
` <div class="cell-body" id="${safeName}"></div>`,
|
|
546
|
+
` </div>`,
|
|
547
|
+
].join('\n');
|
|
548
|
+
})
|
|
549
|
+
.join('\n'))
|
|
550
|
+
.join('\n');
|
|
551
|
+
// Compute grid template from row structure
|
|
552
|
+
const maxCols = Math.max(...rows.map((r) => r.length));
|
|
553
|
+
const colTemplate = `repeat(${maxCols}, 1fr)`;
|
|
554
|
+
const rowTemplate = rows.map(() => 'auto').join(' ');
|
|
555
|
+
const htmlFile = [
|
|
556
|
+
`<style>`,
|
|
557
|
+
` * { box-sizing: border-box; margin: 0; padding: 0; }`,
|
|
558
|
+
` body {`,
|
|
559
|
+
` font-family: var(--font-family-sans, -apple-system, BlinkMacSystemFont, sans-serif);`,
|
|
560
|
+
` background: var(--color-surface, #1a1b26);`,
|
|
561
|
+
` color: var(--color-on-surface, #e6e6e6);`,
|
|
562
|
+
` padding: 16px;`,
|
|
563
|
+
` }`,
|
|
564
|
+
` .grid {`,
|
|
565
|
+
` display: grid;`,
|
|
566
|
+
` grid-template-columns: ${colTemplate};`,
|
|
567
|
+
` grid-template-rows: ${rowTemplate};`,
|
|
568
|
+
` gap: 12px;`,
|
|
569
|
+
` }`,
|
|
570
|
+
` .cell {`,
|
|
571
|
+
` background: var(--color-surface-container, #1e2030);`,
|
|
572
|
+
` border: 1px solid var(--color-outline-variant, #333);`,
|
|
573
|
+
` border-radius: 8px;`,
|
|
574
|
+
` padding: 12px;`,
|
|
575
|
+
` min-height: 120px;`,
|
|
576
|
+
` }`,
|
|
577
|
+
` .cell-label {`,
|
|
578
|
+
` font-size: 11px;`,
|
|
579
|
+
` color: var(--color-on-surface-muted, #999);`,
|
|
580
|
+
` margin-bottom: 8px;`,
|
|
581
|
+
` text-transform: uppercase;`,
|
|
582
|
+
` letter-spacing: 0.5px;`,
|
|
583
|
+
` }`,
|
|
584
|
+
` .cell-body { min-height: 0; }`,
|
|
585
|
+
`</style>`,
|
|
586
|
+
``,
|
|
587
|
+
`<div class="grid">`,
|
|
588
|
+
gridCells,
|
|
589
|
+
`</div>`,
|
|
590
|
+
``,
|
|
591
|
+
`<script>`,
|
|
592
|
+
`(function() {`,
|
|
593
|
+
` window.photon.onResult(function(result) {`,
|
|
594
|
+
` if (!result) return;`,
|
|
595
|
+
` var cells = document.querySelectorAll('.cell');`,
|
|
596
|
+
` for (var i = 0; i < cells.length; i++) {`,
|
|
597
|
+
` var method = cells[i].getAttribute('data-method');`,
|
|
598
|
+
` var format = cells[i].getAttribute('data-format');`,
|
|
599
|
+
` var body = cells[i].querySelector('.cell-body');`,
|
|
600
|
+
` if (method && result[method] && body) {`,
|
|
601
|
+
` window.photon.render(body, result[method], format);`,
|
|
602
|
+
` }`,
|
|
603
|
+
` }`,
|
|
604
|
+
` });`,
|
|
605
|
+
`})();`,
|
|
606
|
+
`</script>`,
|
|
607
|
+
].join('\n');
|
|
608
|
+
return {
|
|
609
|
+
name,
|
|
610
|
+
files: {
|
|
611
|
+
[`${name}.photon.ts`]: tsFile,
|
|
612
|
+
[`${name}/ui/main.html`]: htmlFile,
|
|
613
|
+
},
|
|
614
|
+
elements: sorted.length,
|
|
615
|
+
grid: `${maxCols} columns x ${rows.length} rows`,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
_detectRows(els) {
|
|
619
|
+
if (els.length === 0)
|
|
620
|
+
return [];
|
|
621
|
+
// Group elements into rows by y-proximity (within 50px = same row)
|
|
622
|
+
const sorted = [...els].sort((a, b) => a.y - b.y || a.x - b.x);
|
|
623
|
+
const rows = [];
|
|
624
|
+
let currentRow = [sorted[0]];
|
|
625
|
+
let rowY = sorted[0].y;
|
|
626
|
+
for (let i = 1; i < sorted.length; i++) {
|
|
627
|
+
if (Math.abs(sorted[i].y - rowY) < 50) {
|
|
628
|
+
currentRow.push(sorted[i]);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
rows.push(currentRow.sort((a, b) => a.x - b.x));
|
|
632
|
+
currentRow = [sorted[i]];
|
|
633
|
+
rowY = sorted[i].y;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
rows.push(currentRow.sort((a, b) => a.x - b.x));
|
|
637
|
+
return rows;
|
|
638
|
+
}
|
|
639
|
+
_toPascalCase(str) {
|
|
640
|
+
return str
|
|
641
|
+
.replace(/[^a-zA-Z0-9]+/g, ' ')
|
|
642
|
+
.split(' ')
|
|
643
|
+
.filter(Boolean)
|
|
644
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase())
|
|
645
|
+
.join('');
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* List all available render formats with expected data shapes
|
|
649
|
+
* @readOnly
|
|
650
|
+
* @format table
|
|
651
|
+
*/
|
|
652
|
+
listFormats() {
|
|
653
|
+
const catalog = this.formats;
|
|
654
|
+
if (!catalog || typeof catalog !== 'object')
|
|
655
|
+
return [];
|
|
656
|
+
return Object.entries(catalog).map(([name, spec]) => ({
|
|
657
|
+
format: name,
|
|
658
|
+
data: spec.data,
|
|
659
|
+
}));
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
//# sourceMappingURL=canvas.photon.js.map
|