@memberjunction/ng-whiteboard 0.0.1 → 5.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +302 -28
  2. package/dist/lib/whiteboard-agent-sees-popover.component.d.ts +31 -0
  3. package/dist/lib/whiteboard-agent-sees-popover.component.d.ts.map +1 -0
  4. package/dist/lib/whiteboard-agent-sees-popover.component.js +132 -0
  5. package/dist/lib/whiteboard-agent-sees-popover.component.js.map +1 -0
  6. package/dist/lib/whiteboard-board.component.d.ts +458 -0
  7. package/dist/lib/whiteboard-board.component.d.ts.map +1 -0
  8. package/dist/lib/whiteboard-board.component.js +2357 -0
  9. package/dist/lib/whiteboard-board.component.js.map +1 -0
  10. package/dist/lib/whiteboard-context-menu.d.ts +57 -0
  11. package/dist/lib/whiteboard-context-menu.d.ts.map +1 -0
  12. package/dist/lib/whiteboard-context-menu.js +69 -0
  13. package/dist/lib/whiteboard-context-menu.js.map +1 -0
  14. package/dist/lib/whiteboard-export.d.ts +94 -0
  15. package/dist/lib/whiteboard-export.d.ts.map +1 -0
  16. package/dist/lib/whiteboard-export.js +592 -0
  17. package/dist/lib/whiteboard-export.js.map +1 -0
  18. package/dist/lib/whiteboard-host.component.d.ts +174 -0
  19. package/dist/lib/whiteboard-host.component.d.ts.map +1 -0
  20. package/dist/lib/whiteboard-host.component.js +758 -0
  21. package/dist/lib/whiteboard-host.component.js.map +1 -0
  22. package/dist/lib/whiteboard-pages.component.d.ts +83 -0
  23. package/dist/lib/whiteboard-pages.component.d.ts.map +1 -0
  24. package/dist/lib/whiteboard-pages.component.js +211 -0
  25. package/dist/lib/whiteboard-pages.component.js.map +1 -0
  26. package/dist/lib/whiteboard-snapshot.component.d.ts +30 -0
  27. package/dist/lib/whiteboard-snapshot.component.d.ts.map +1 -0
  28. package/dist/lib/whiteboard-snapshot.component.js +91 -0
  29. package/dist/lib/whiteboard-snapshot.component.js.map +1 -0
  30. package/dist/lib/whiteboard-srcdoc.pipe.d.ts +60 -0
  31. package/dist/lib/whiteboard-srcdoc.pipe.d.ts.map +1 -0
  32. package/dist/lib/whiteboard-srcdoc.pipe.js +75 -0
  33. package/dist/lib/whiteboard-srcdoc.pipe.js.map +1 -0
  34. package/dist/lib/whiteboard-state.d.ts +1113 -0
  35. package/dist/lib/whiteboard-state.d.ts.map +1 -0
  36. package/dist/lib/whiteboard-state.js +1396 -0
  37. package/dist/lib/whiteboard-state.js.map +1 -0
  38. package/dist/lib/whiteboard-toolbar.component.d.ts +95 -0
  39. package/dist/lib/whiteboard-toolbar.component.d.ts.map +1 -0
  40. package/dist/lib/whiteboard-toolbar.component.js +344 -0
  41. package/dist/lib/whiteboard-toolbar.component.js.map +1 -0
  42. package/dist/lib/whiteboard-tools.d.ts +103 -0
  43. package/dist/lib/whiteboard-tools.d.ts.map +1 -0
  44. package/dist/lib/whiteboard-tools.js +748 -0
  45. package/dist/lib/whiteboard-tools.js.map +1 -0
  46. package/dist/lib/whiteboard-widget-bridge.d.ts +217 -0
  47. package/dist/lib/whiteboard-widget-bridge.d.ts.map +1 -0
  48. package/dist/lib/whiteboard-widget-bridge.js +254 -0
  49. package/dist/lib/whiteboard-widget-bridge.js.map +1 -0
  50. package/dist/lib/whiteboard-zoom.component.d.ts +55 -0
  51. package/dist/lib/whiteboard-zoom.component.d.ts.map +1 -0
  52. package/dist/lib/whiteboard-zoom.component.js +139 -0
  53. package/dist/lib/whiteboard-zoom.component.js.map +1 -0
  54. package/dist/lib/whiteboard.module.d.ts +29 -0
  55. package/dist/lib/whiteboard.module.d.ts.map +1 -0
  56. package/dist/lib/whiteboard.module.js +78 -0
  57. package/dist/lib/whiteboard.module.js.map +1 -0
  58. package/dist/public-api.d.ts +49 -0
  59. package/dist/public-api.d.ts.map +1 -0
  60. package/dist/public-api.js +59 -0
  61. package/dist/public-api.js.map +1 -0
  62. package/package.json +43 -6
@@ -0,0 +1,748 @@
1
+ import { WHITEBOARD_FONT_SIZES } from './whiteboard-state';
2
+ /**
3
+ * The shared name prefix of every whiteboard tool — the key an integration layer uses to
4
+ * route all `Whiteboard_*` tool calls locally to {@link ApplyWhiteboardAgentTool} (e.g.
5
+ * MJ's realtime sessions register it with `RealtimeSessionService.RegisterClientToolHandler`
6
+ * so calls execute in the browser instead of the server relay).
7
+ */
8
+ export const WHITEBOARD_TOOL_PREFIX = 'Whiteboard_';
9
+ /** Names of the whiteboard tools, as registered with the agent/automation runtime. */
10
+ export const WHITEBOARD_TOOL_NAMES = {
11
+ AddNote: 'Whiteboard_AddNote',
12
+ AddShape: 'Whiteboard_AddShape',
13
+ AddText: 'Whiteboard_AddText',
14
+ AddMarkdown: 'Whiteboard_AddMarkdown',
15
+ AddHtml: 'Whiteboard_AddHtml',
16
+ UpdateContent: 'Whiteboard_UpdateContent',
17
+ DrawConnector: 'Whiteboard_DrawConnector',
18
+ Highlight: 'Whiteboard_Highlight',
19
+ MoveItem: 'Whiteboard_MoveItem',
20
+ RemoveItem: 'Whiteboard_RemoveItem',
21
+ StyleItem: 'Whiteboard_StyleItem',
22
+ AddPage: 'Whiteboard_AddPage',
23
+ SwitchPage: 'Whiteboard_SwitchPage',
24
+ RenamePage: 'Whiteboard_RenamePage'
25
+ };
26
+ /** Max markdown source length accepted by AddMarkdown / UpdateContent (chars). */
27
+ export const WHITEBOARD_MARKDOWN_MAX_CHARS = 32_000;
28
+ /** Max HTML source length accepted by AddHtml / UpdateContent (chars). */
29
+ export const WHITEBOARD_HTML_MAX_CHARS = 64_000;
30
+ /**
31
+ * The PAGE-level tools (they navigate/manage pages rather than items, so the shared
32
+ * "targets the ACTIVE page" item-tool sentence is not appended to their descriptions).
33
+ */
34
+ const WHITEBOARD_PAGE_TOOL_NAMES = new Set([
35
+ WHITEBOARD_TOOL_NAMES.AddPage,
36
+ WHITEBOARD_TOOL_NAMES.SwitchPage,
37
+ WHITEBOARD_TOOL_NAMES.RenamePage
38
+ ]);
39
+ /**
40
+ * The shared sentence appended (once, programmatically — see
41
+ * {@link WHITEBOARD_TOOL_DEFINITIONS}) to every ITEM tool's description, so the model
42
+ * always knows item operations are scoped to the page that is currently active.
43
+ */
44
+ export const WHITEBOARD_ACTIVE_PAGE_NOTE = ' This tool targets the ACTIVE page only — use Whiteboard_SwitchPage first to work with items on another page.';
45
+ /** The raw tool definitions, before the shared active-page sentence is appended. */
46
+ const RAW_WHITEBOARD_TOOL_DEFINITIONS = [
47
+ {
48
+ Name: WHITEBOARD_TOOL_NAMES.AddNote,
49
+ Description: 'Add a sticky note to the shared whiteboard. Your notes render in your reserved violet style so the user always knows they came from you.',
50
+ ParametersSchema: {
51
+ type: 'object',
52
+ properties: {
53
+ text: { type: 'string', description: 'The note text. Keep it short — sticky-note sized.' },
54
+ x: { type: 'number', description: 'Left position in board coordinates. Omit to auto-place near existing content.' },
55
+ y: { type: 'number', description: 'Top position in board coordinates. Omit to auto-place near existing content.' },
56
+ fontSize: { type: 'number', enum: [...WHITEBOARD_FONT_SIZES], description: 'Optional note font size in px (curated steps).' }
57
+ },
58
+ required: ['text']
59
+ }
60
+ },
61
+ {
62
+ Name: WHITEBOARD_TOOL_NAMES.AddShape,
63
+ Description: 'Add a labeled shape box (rect, ellipse or diamond) to the whiteboard — for process steps, tracks, groupings.',
64
+ ParametersSchema: {
65
+ type: 'object',
66
+ properties: {
67
+ label: { type: 'string', description: 'Main label rendered inside the shape.' },
68
+ sub: { type: 'string', description: 'Optional smaller sub-label under the main label.' },
69
+ shape: { type: 'string', enum: ['rect', 'ellipse', 'diamond'], description: 'Shape geometry. Defaults to rect.' },
70
+ x: { type: 'number', description: 'Left position in board coordinates.' },
71
+ y: { type: 'number', description: 'Top position in board coordinates.' },
72
+ w: { type: 'number', description: 'Width in px. Defaults to 172.' },
73
+ h: { type: 'number', description: 'Height in px. Defaults to 56.' }
74
+ },
75
+ required: ['label']
76
+ }
77
+ },
78
+ {
79
+ Name: WHITEBOARD_TOOL_NAMES.AddText,
80
+ Description: 'Add a small free-floating text label to the whiteboard — for annotating the user\'s diagram without covering it.',
81
+ ParametersSchema: {
82
+ type: 'object',
83
+ properties: {
84
+ text: { type: 'string', description: 'The label text.' },
85
+ x: { type: 'number', description: 'Left position in board coordinates.' },
86
+ y: { type: 'number', description: 'Top position in board coordinates.' },
87
+ w: { type: 'number', description: 'Optional wrap width in px (60-800). Long text wraps at this width — use for sentences/paragraphs; omit for short labels.' },
88
+ fontSize: { type: 'number', enum: [...WHITEBOARD_FONT_SIZES], description: 'Optional label font size in px (curated steps).' },
89
+ fontFamily: { type: 'string', enum: ['sans', 'serif', 'mono'], description: 'Optional font family. Defaults to sans.' },
90
+ bold: { type: 'boolean', description: 'Optional bold weight. Labels render bold by default.' }
91
+ },
92
+ required: ['text']
93
+ }
94
+ },
95
+ {
96
+ Name: WHITEBOARD_TOOL_NAMES.AddMarkdown,
97
+ Description: 'Add a rendered MARKDOWN panel to the whiteboard — headings, lists, code blocks and links, for richer illustrative content than a sticky note. The source is rendered safely (sanitized; raw HTML in the markdown is NOT executed). Renders in your reserved violet chrome.',
98
+ ParametersSchema: {
99
+ type: 'object',
100
+ properties: {
101
+ markdown: { type: 'string', description: `The markdown source (max ${WHITEBOARD_MARKDOWN_MAX_CHARS} chars).` },
102
+ x: { type: 'number', description: 'Left position in board coordinates. Omit to auto-place near existing content.' },
103
+ y: { type: 'number', description: 'Top position in board coordinates.' },
104
+ w: { type: 'number', description: 'Panel width in px (160-800). Defaults to 280.' },
105
+ h: { type: 'number', description: 'Optional max height in px (80-800); content beyond it is clipped. Omit for content-driven height.' }
106
+ },
107
+ required: ['markdown']
108
+ }
109
+ },
110
+ {
111
+ Name: WHITEBOARD_TOOL_NAMES.AddHtml,
112
+ Description: 'Add an interactive HTML widget to the whiteboard — a self-contained HTML document (inline CSS/JS allowed) rendered in a STRICTLY SANDBOXED iframe with an opaque origin: scripts run isolated with NO access to the app, its session, cookies or storage, and network access is not guaranteed — make the widget fully self-contained (no external scripts, styles or data). USE WIDGETS TO TEACH AND GET USER INPUT: inline SVG diagrams and explainers, CSS concept animations, micro-quizzes and micro-forms. To collect input, have a button/form handler call MJWhiteboard.submit(data) — the host injects that helper into every widget automatically, and the submitted data (JSON-serialized, max 8000 chars) reaches you as a "[whiteboard] the user submitted input…" context note. Example: a one-question quiz whose Submit button calls MJWhiteboard.submit({question:"…",answer:picked}). PASSIVE FORMS NEED NO SCRIPTING: button/select/typing activity inside the widget reaches you automatically as ambient "[whiteboard]" background context notes — reserve MJWhiteboard.submit for explicit submissions.',
113
+ ParametersSchema: {
114
+ type: 'object',
115
+ properties: {
116
+ html: { type: 'string', description: `The widget's full HTML source (max ${WHITEBOARD_HTML_MAX_CHARS} chars). Self-contained — inline everything.` },
117
+ title: { type: 'string', description: 'Optional short title shown on the widget header bar.' },
118
+ x: { type: 'number', description: 'Left position in board coordinates. Omit to auto-place near existing content.' },
119
+ y: { type: 'number', description: 'Top position in board coordinates.' },
120
+ w: { type: 'number', description: 'Widget width in px (200-960). Defaults to 360.' },
121
+ h: { type: 'number', description: 'Widget height in px (120-800). Defaults to 240.' }
122
+ },
123
+ required: ['html']
124
+ }
125
+ },
126
+ {
127
+ Name: WHITEBOARD_TOOL_NAMES.UpdateContent,
128
+ Description: 'Replace the CONTENT of an existing whiteboard item: pass exactly one of "markdown" (for a markdown panel), "html" (for an HTML widget — re-renders its sandboxed iframe), or "text" (for a sticky note or text label). The field must match the item\'s kind.',
129
+ ParametersSchema: {
130
+ type: 'object',
131
+ properties: {
132
+ itemId: { type: 'string', description: 'ID of the item to update (from a scene delta), e.g. "markdown-3".' },
133
+ markdown: { type: 'string', description: `New markdown source — markdown panels only (max ${WHITEBOARD_MARKDOWN_MAX_CHARS} chars).` },
134
+ html: { type: 'string', description: `New HTML source — HTML widgets only (max ${WHITEBOARD_HTML_MAX_CHARS} chars). Still rendered sandboxed.` },
135
+ text: { type: 'string', description: 'New text — sticky notes and text labels only.' },
136
+ title: { type: 'string', description: 'Optional new header title — HTML widgets only (alongside "html", or alone).' }
137
+ },
138
+ required: ['itemId']
139
+ }
140
+ },
141
+ {
142
+ Name: WHITEBOARD_TOOL_NAMES.DrawConnector,
143
+ Description: 'Draw an arrow connector between two whiteboard items (by item id), or between two absolute points.',
144
+ ParametersSchema: {
145
+ type: 'object',
146
+ properties: {
147
+ fromId: { type: 'string', description: 'ID of the source item (from a scene delta), e.g. "shape-2".' },
148
+ toId: { type: 'string', description: 'ID of the target item.' },
149
+ fromX: { type: 'number', description: 'Source X when not anchoring to an item.' },
150
+ fromY: { type: 'number', description: 'Source Y when not anchoring to an item.' },
151
+ toX: { type: 'number', description: 'Target X when not anchoring to an item.' },
152
+ toY: { type: 'number', description: 'Target Y when not anchoring to an item.' }
153
+ }
154
+ }
155
+ },
156
+ {
157
+ Name: WHITEBOARD_TOOL_NAMES.Highlight,
158
+ Description: 'Pulse a highlight region on the whiteboard while you narrate — pointing without touching the user\'s work. Pass itemIds to highlight around existing items, or an explicit x/y/w/h region. The user dismisses it with a click.',
159
+ ParametersSchema: {
160
+ type: 'object',
161
+ properties: {
162
+ itemIds: { type: 'array', items: { type: 'string' }, description: 'IDs of items to highlight around (bounding box + padding).' },
163
+ x: { type: 'number', description: 'Region left (when not using itemIds).' },
164
+ y: { type: 'number', description: 'Region top.' },
165
+ w: { type: 'number', description: 'Region width.' },
166
+ h: { type: 'number', description: 'Region height.' },
167
+ label: { type: 'string', description: 'Optional tag shown on the region edge.' }
168
+ }
169
+ }
170
+ },
171
+ {
172
+ Name: WHITEBOARD_TOOL_NAMES.MoveItem,
173
+ Description: 'Move and/or RESIZE a whiteboard item: provide x+y to reposition (board coordinates of the top-left corner), w and/or h to resize (pixels, min 24), or all of them together.',
174
+ ParametersSchema: {
175
+ type: 'object',
176
+ properties: {
177
+ itemId: { type: 'string', description: 'ID of the item to move/resize.' },
178
+ x: { type: 'number', description: 'New left position (provide together with y to move).' },
179
+ y: { type: 'number', description: 'New top position (provide together with x to move).' },
180
+ w: { type: 'number', description: 'New width in pixels (resize; min 24).' },
181
+ h: { type: 'number', description: 'New height in pixels (resize; min 24).' }
182
+ },
183
+ required: ['itemId']
184
+ }
185
+ },
186
+ {
187
+ Name: WHITEBOARD_TOOL_NAMES.RemoveItem,
188
+ Description: 'Remove an item from the whiteboard. Prefer removing only your own items unless the user asks.',
189
+ ParametersSchema: {
190
+ type: 'object',
191
+ properties: {
192
+ itemId: { type: 'string', description: 'ID of the item to remove.' }
193
+ },
194
+ required: ['itemId']
195
+ }
196
+ },
197
+ {
198
+ Name: WHITEBOARD_TOOL_NAMES.StyleItem,
199
+ Description: 'Restyle the text of an existing sticky note or text label — font size, family, bold, and (text labels only) color. Pass at least one style field.',
200
+ ParametersSchema: {
201
+ type: 'object',
202
+ properties: {
203
+ itemId: { type: 'string', description: 'ID of the sticky note or text label to restyle.' },
204
+ fontSize: { type: 'number', enum: [...WHITEBOARD_FONT_SIZES], description: 'New font size in px (curated steps).' },
205
+ fontFamily: { type: 'string', enum: ['sans', 'serif', 'mono'], description: 'New font family.' },
206
+ bold: { type: 'boolean', description: 'Bold on/off.' },
207
+ color: { type: 'string', description: 'Hex text color, e.g. "#fbbf24" (text labels only — violet is already yours).' }
208
+ },
209
+ required: ['itemId']
210
+ }
211
+ },
212
+ {
213
+ Name: WHITEBOARD_TOOL_NAMES.AddPage,
214
+ Description: 'Create a NEW PAGE on the whiteboard and switch to it. Start a new page for a new topic, exercise or diagram instead of crowding the current board — pages keep prior work intact and the user can flip back any time. All item tools (notes, shapes, widgets, …) always target the page that is currently active.',
215
+ ParametersSchema: {
216
+ type: 'object',
217
+ properties: {
218
+ name: { type: 'string', description: 'Optional page name (e.g. "Practice problems"). Omit to auto-name it "Page N".' }
219
+ }
220
+ }
221
+ },
222
+ {
223
+ Name: WHITEBOARD_TOOL_NAMES.SwitchPage,
224
+ Description: 'Switch the whiteboard to another existing page (by page name, case-insensitive, or page id). Use it to return to earlier work — the page list (with the active page marked) is included in every scene update you receive. Item tools only see the active page, so switch before editing items that live elsewhere.',
225
+ ParametersSchema: {
226
+ type: 'object',
227
+ properties: {
228
+ name: { type: 'string', description: 'The target page\'s name (case-insensitive) or its id, e.g. "Page 2" or "page-2".' }
229
+ },
230
+ required: ['name']
231
+ }
232
+ },
233
+ {
234
+ Name: WHITEBOARD_TOOL_NAMES.RenamePage,
235
+ Description: 'Rename an existing whiteboard page — give pages meaningful names ("Warm-up", "Final design") so the user can navigate them. Identify the page by its current name (case-insensitive) or id.',
236
+ ParametersSchema: {
237
+ type: 'object',
238
+ properties: {
239
+ name: { type: 'string', description: 'The page\'s CURRENT name (case-insensitive) or its id.' },
240
+ newName: { type: 'string', description: 'The new page name. Keep it short — it renders on a tab chip.' }
241
+ },
242
+ required: ['name', 'newName']
243
+ }
244
+ }
245
+ ];
246
+ /**
247
+ * The full `Whiteboard_*` tool set, ready for registration with an agent/automation
248
+ * runtime (shape-compatible with `RealtimeToolDefinition` from `@memberjunction/ai`).
249
+ *
250
+ * Every ITEM tool's description carries the shared {@link WHITEBOARD_ACTIVE_PAGE_NOTE}
251
+ * sentence (appended programmatically here — ONE mechanism, not eleven hand edits);
252
+ * the three page tools keep their page-navigation descriptions unmodified.
253
+ */
254
+ export const WHITEBOARD_TOOL_DEFINITIONS = RAW_WHITEBOARD_TOOL_DEFINITIONS.map((def) => WHITEBOARD_PAGE_TOOL_NAMES.has(def.Name)
255
+ ? def
256
+ : { ...def, Description: `${def.Description}${WHITEBOARD_ACTIVE_PAGE_NOTE}` });
257
+ /** Padding added around item bounds when highlighting by itemIds. */
258
+ const HIGHLIGHT_PAD = 18;
259
+ /** The font family keys accepted by the style tool params. */
260
+ const WHITEBOARD_FONT_FAMILIES = ['sans', 'serif', 'mono'];
261
+ /** Hex colors accepted for text color ("#abc" / "#aabbcc"). */
262
+ const HEX_COLOR = /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
263
+ function ok(itemId, summary) {
264
+ const result = { success: true, itemId, summary };
265
+ return JSON.stringify(result);
266
+ }
267
+ function fail(error) {
268
+ const result = { success: false, error };
269
+ return JSON.stringify(result);
270
+ }
271
+ function asString(value) {
272
+ return typeof value === 'string' && value.length > 0 ? value : undefined;
273
+ }
274
+ function asNumber(value) {
275
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
276
+ }
277
+ /**
278
+ * Extract + validate the optional text-style args shared by AddNote / AddText / StyleItem
279
+ * into an {@link WhiteboardItemPatch}. `allowColor` is true only for text labels — sticky
280
+ * tints are categorical and agent violet comes from authorship, never from `Color`.
281
+ */
282
+ function textStylePatch(args, allowColor) {
283
+ const patch = {};
284
+ if (args['fontSize'] !== undefined) {
285
+ const size = asNumber(args['fontSize']);
286
+ if (size === undefined || !WHITEBOARD_FONT_SIZES.includes(size)) {
287
+ return { error: `"fontSize" must be one of ${WHITEBOARD_FONT_SIZES.join(', ')}.` };
288
+ }
289
+ patch.FontSize = size;
290
+ }
291
+ if (args['fontFamily'] !== undefined) {
292
+ const family = args['fontFamily'];
293
+ if (typeof family !== 'string' || !WHITEBOARD_FONT_FAMILIES.includes(family)) {
294
+ return { error: `"fontFamily" must be one of ${WHITEBOARD_FONT_FAMILIES.join(', ')}.` };
295
+ }
296
+ patch.FontFamily = family;
297
+ }
298
+ if (args['bold'] !== undefined) {
299
+ if (typeof args['bold'] !== 'boolean') {
300
+ return { error: '"bold" must be a boolean.' };
301
+ }
302
+ patch.FontWeight = args['bold'] ? 700 : 400;
303
+ }
304
+ if (args['color'] !== undefined) {
305
+ if (!allowColor) {
306
+ return { error: '"color" only applies to text labels.' };
307
+ }
308
+ const color = args['color'];
309
+ if (typeof color !== 'string' || !HEX_COLOR.test(color)) {
310
+ return { error: '"color" must be a hex color like "#fbbf24".' };
311
+ }
312
+ patch.Color = color;
313
+ }
314
+ return { patch };
315
+ }
316
+ /** Auto-placement when the agent omits coordinates: just right of existing content. */
317
+ function autoPlace(state) {
318
+ const bounds = state.ContentBounds();
319
+ if (!bounds) {
320
+ return { X: 120, Y: 100 };
321
+ }
322
+ return { X: bounds.X + bounds.W + 40, Y: bounds.Y + 20 };
323
+ }
324
+ function unionBounds(state, items) {
325
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
326
+ for (const item of items) {
327
+ const b = state.ItemBounds(item);
328
+ minX = Math.min(minX, b.X);
329
+ minY = Math.min(minY, b.Y);
330
+ maxX = Math.max(maxX, b.X + b.W);
331
+ maxY = Math.max(maxY, b.Y + b.H);
332
+ }
333
+ return { X: minX, Y: minY, W: maxX - minX, H: maxY - minY };
334
+ }
335
+ /**
336
+ * Execute one agent whiteboard tool against the state engine (author `'agent'`, one undo
337
+ * batch per call) and return the JSON result string for the tool round-trip.
338
+ *
339
+ * Never throws: malformed args / unknown tools / unknown item IDs return a
340
+ * `{ success: false, error }` payload so the model can self-correct conversationally.
341
+ */
342
+ export function ApplyWhiteboardAgentTool(state, toolName, argsJson) {
343
+ let args;
344
+ try {
345
+ const parsed = argsJson.trim().length === 0 ? {} : JSON.parse(argsJson);
346
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
347
+ return fail('Tool arguments must be a JSON object.');
348
+ }
349
+ args = parsed;
350
+ }
351
+ catch {
352
+ return fail('Tool arguments are not valid JSON.');
353
+ }
354
+ switch (toolName) {
355
+ case WHITEBOARD_TOOL_NAMES.AddNote: return addNote(state, args);
356
+ case WHITEBOARD_TOOL_NAMES.AddShape: return addShape(state, args);
357
+ case WHITEBOARD_TOOL_NAMES.AddText: return addText(state, args);
358
+ case WHITEBOARD_TOOL_NAMES.AddMarkdown: return addMarkdown(state, args);
359
+ case WHITEBOARD_TOOL_NAMES.AddHtml: return addHtml(state, args);
360
+ case WHITEBOARD_TOOL_NAMES.UpdateContent: return updateContent(state, args);
361
+ case WHITEBOARD_TOOL_NAMES.DrawConnector: return drawConnector(state, args);
362
+ case WHITEBOARD_TOOL_NAMES.Highlight: return highlight(state, args);
363
+ case WHITEBOARD_TOOL_NAMES.MoveItem: return moveItem(state, args);
364
+ case WHITEBOARD_TOOL_NAMES.RemoveItem: return removeItem(state, args);
365
+ case WHITEBOARD_TOOL_NAMES.StyleItem: return styleItem(state, args);
366
+ case WHITEBOARD_TOOL_NAMES.AddPage: return addPage(state, args);
367
+ case WHITEBOARD_TOOL_NAMES.SwitchPage: return switchPage(state, args);
368
+ case WHITEBOARD_TOOL_NAMES.RenamePage: return renamePage(state, args);
369
+ default:
370
+ return fail(`Unknown whiteboard tool "${toolName}".`);
371
+ }
372
+ }
373
+ /** Success payload for the page tools (carries `pageId` instead of `itemId`). */
374
+ function okPage(pageId, summary) {
375
+ const result = { success: true, pageId, summary };
376
+ return JSON.stringify(result);
377
+ }
378
+ /** The page list rendered into not-found errors so the model can self-correct. */
379
+ function pageListText(state) {
380
+ return state.Pages.map((p) => `"${p.Name}"${p.Active ? ' (active)' : ''}`).join(', ');
381
+ }
382
+ function addPage(state, args) {
383
+ if (args['name'] !== undefined && typeof args['name'] !== 'string') {
384
+ return fail('AddPage: "name" must be a string when provided.');
385
+ }
386
+ const page = state.AddPage(asString(args['name']), 'agent');
387
+ if (!page) {
388
+ return fail('The host application canceled this operation.');
389
+ }
390
+ return okPage(page.ID, `Created page "${page.Name}" and switched to it — new items will land on this page.`);
391
+ }
392
+ function switchPage(state, args) {
393
+ const name = asString(args['name']);
394
+ if (!name) {
395
+ return fail('SwitchPage requires a non-empty "name" (the page\'s name or id).');
396
+ }
397
+ const page = state.FindPage(name);
398
+ if (!page) {
399
+ return fail(`SwitchPage: no page named "${name}". Pages: ${pageListText(state)}.`);
400
+ }
401
+ if (!state.SwitchPage(page.ID, 'agent')) {
402
+ return fail('The host application canceled this operation.');
403
+ }
404
+ return okPage(page.ID, `Switched to page "${page.Name}" (${page.ItemCount} items).`);
405
+ }
406
+ function renamePage(state, args) {
407
+ const name = asString(args['name']);
408
+ const newName = asString(args['newName']);
409
+ if (!name || !newName) {
410
+ return fail('RenamePage requires a non-empty "name" (current name or id) and "newName".');
411
+ }
412
+ const page = state.FindPage(name);
413
+ if (!page) {
414
+ return fail(`RenamePage: no page named "${name}". Pages: ${pageListText(state)}.`);
415
+ }
416
+ if (!state.RenamePage(page.ID, newName, 'agent')) {
417
+ return fail('The host application canceled this operation.');
418
+ }
419
+ return okPage(page.ID, `Renamed page "${page.Name}" to "${newName.trim()}".`);
420
+ }
421
+ function addNote(state, args) {
422
+ const text = asString(args['text']);
423
+ if (!text) {
424
+ return fail('AddNote requires a non-empty "text".');
425
+ }
426
+ const style = textStylePatch(args, false);
427
+ if ('error' in style) {
428
+ return fail(`AddNote: ${style.error}`);
429
+ }
430
+ const place = autoPlace(state);
431
+ const x = asNumber(args['x']) ?? place.X;
432
+ const y = asNumber(args['y']) ?? place.Y;
433
+ const item = state.RunBatch(() => state.AddItem({ Kind: 'sticky', X: x, Y: y, Text: text, Rotation: 1.2, ...style.patch }, 'agent'));
434
+ if (!item) {
435
+ return fail('The host application canceled this operation.');
436
+ }
437
+ return ok(item.ID, `Added sticky note "${text}" at (${Math.round(x)}, ${Math.round(y)}).`);
438
+ }
439
+ function addShape(state, args) {
440
+ const label = asString(args['label']);
441
+ if (!label) {
442
+ return fail('AddShape requires a non-empty "label".');
443
+ }
444
+ const shapeRaw = asString(args['shape']) ?? 'rect';
445
+ if (shapeRaw !== 'rect' && shapeRaw !== 'ellipse' && shapeRaw !== 'diamond') {
446
+ return fail(`AddShape "shape" must be rect, ellipse or diamond (got "${shapeRaw}").`);
447
+ }
448
+ const shape = shapeRaw;
449
+ const place = autoPlace(state);
450
+ const x = asNumber(args['x']) ?? place.X;
451
+ const y = asNumber(args['y']) ?? place.Y;
452
+ const w = asNumber(args['w']) ?? 172;
453
+ const h = asNumber(args['h']) ?? 56;
454
+ const item = state.RunBatch(() => state.AddItem({ Kind: 'shape', Shape: shape, X: x, Y: y, W: w, H: h, Label: label, Sub: asString(args['sub']) }, 'agent'));
455
+ if (!item) {
456
+ return fail('The host application canceled this operation.');
457
+ }
458
+ return ok(item.ID, `Added ${shape} "${label}" at (${Math.round(x)}, ${Math.round(y)}).`);
459
+ }
460
+ function addText(state, args) {
461
+ const text = asString(args['text']);
462
+ if (!text) {
463
+ return fail('AddText requires a non-empty "text".');
464
+ }
465
+ const style = textStylePatch(args, false);
466
+ if ('error' in style) {
467
+ return fail(`AddText: ${style.error}`);
468
+ }
469
+ const place = autoPlace(state);
470
+ const x = asNumber(args['x']) ?? place.X;
471
+ const y = asNumber(args['y']) ?? place.Y;
472
+ const wRaw = asNumber(args['w']);
473
+ const w = wRaw !== undefined ? Math.min(800, Math.max(60, Math.round(wRaw))) : undefined;
474
+ const item = state.RunBatch(() => state.AddItem({ Kind: 'text', X: x, Y: y, Text: text, ...(w !== undefined ? { W: w } : {}), ...style.patch }, 'agent'));
475
+ if (!item) {
476
+ return fail('The host application canceled this operation.');
477
+ }
478
+ return ok(item.ID, `Added text label "${text}".`);
479
+ }
480
+ /** Clamp an optional numeric arg into [min, max], or return the default when omitted. */
481
+ function clampedNumber(value, min, max, fallback) {
482
+ const raw = asNumber(value);
483
+ if (raw === undefined) {
484
+ return fallback;
485
+ }
486
+ return Math.min(max, Math.max(min, Math.round(raw)));
487
+ }
488
+ function addMarkdown(state, args) {
489
+ const markdown = asString(args['markdown']);
490
+ if (!markdown) {
491
+ return fail('AddMarkdown requires a non-empty "markdown".');
492
+ }
493
+ if (markdown.length > WHITEBOARD_MARKDOWN_MAX_CHARS) {
494
+ return fail(`AddMarkdown: "markdown" exceeds the ${WHITEBOARD_MARKDOWN_MAX_CHARS}-character limit (got ${markdown.length}).`);
495
+ }
496
+ const place = autoPlace(state);
497
+ const x = asNumber(args['x']) ?? place.X;
498
+ const y = asNumber(args['y']) ?? place.Y;
499
+ const w = clampedNumber(args['w'], 160, 800, 280);
500
+ const h = clampedNumber(args['h'], 80, 800, undefined);
501
+ const item = state.RunBatch(() => state.AddItem({ Kind: 'markdown', X: x, Y: y, W: w, ...(h !== undefined ? { H: h } : {}), Markdown: markdown }, 'agent'));
502
+ if (!item) {
503
+ return fail('The host application canceled this operation.');
504
+ }
505
+ return ok(item.ID, `Added a markdown panel at (${Math.round(x)}, ${Math.round(y)}).`);
506
+ }
507
+ function addHtml(state, args) {
508
+ const html = asString(args['html']);
509
+ if (!html) {
510
+ return fail('AddHtml requires a non-empty "html".');
511
+ }
512
+ if (html.length > WHITEBOARD_HTML_MAX_CHARS) {
513
+ return fail(`AddHtml: "html" exceeds the ${WHITEBOARD_HTML_MAX_CHARS}-character limit (got ${html.length}).`);
514
+ }
515
+ const title = asString(args['title']);
516
+ const place = autoPlace(state);
517
+ const x = asNumber(args['x']) ?? place.X;
518
+ const y = asNumber(args['y']) ?? place.Y;
519
+ const w = clampedNumber(args['w'], 200, 960, 360);
520
+ const h = clampedNumber(args['h'], 120, 800, 240);
521
+ const item = state.RunBatch(() => state.AddItem({ Kind: 'html', X: x, Y: y, W: w, H: h, Html: html, Title: title }, 'agent'));
522
+ if (!item) {
523
+ return fail('The host application canceled this operation.');
524
+ }
525
+ return ok(item.ID, `Added an HTML widget${title ? ` "${title}"` : ''} at (${Math.round(x)}, ${Math.round(y)}) — rendered sandboxed.`);
526
+ }
527
+ /** Validate one UpdateContent content field against the target item's kind. Returns the patch or an error. */
528
+ function contentPatchFor(item, args) {
529
+ if (args['text'] !== undefined && typeof args['text'] !== 'string') {
530
+ return { error: '"text" must be a string.' };
531
+ }
532
+ const markdown = asString(args['markdown']);
533
+ const html = asString(args['html']);
534
+ const text = args['text'];
535
+ const title = asString(args['title']);
536
+ const contentFields = [markdown, html, text].filter((v) => v !== undefined).length;
537
+ if (contentFields > 1) {
538
+ return { error: 'pass exactly ONE of "markdown", "html" or "text".' };
539
+ }
540
+ if (contentFields === 0 && title === undefined) {
541
+ return { error: 'pass one of "markdown", "html", "text" (or "title" for an HTML widget).' };
542
+ }
543
+ if (markdown !== undefined) {
544
+ if (item.Kind !== 'markdown') {
545
+ return { error: `"markdown" only applies to markdown panels (item ${item.ID} is a ${item.Kind}).` };
546
+ }
547
+ if (markdown.length > WHITEBOARD_MARKDOWN_MAX_CHARS) {
548
+ return { error: `"markdown" exceeds the ${WHITEBOARD_MARKDOWN_MAX_CHARS}-character limit (got ${markdown.length}).` };
549
+ }
550
+ return { patch: { Markdown: markdown } };
551
+ }
552
+ if (html !== undefined || title !== undefined) {
553
+ if (item.Kind !== 'html') {
554
+ return { error: `"html"/"title" only apply to HTML widgets (item ${item.ID} is a ${item.Kind}).` };
555
+ }
556
+ if (html !== undefined && html.length > WHITEBOARD_HTML_MAX_CHARS) {
557
+ return { error: `"html" exceeds the ${WHITEBOARD_HTML_MAX_CHARS}-character limit (got ${html.length}).` };
558
+ }
559
+ const patch = {};
560
+ if (html !== undefined) {
561
+ patch.Html = html;
562
+ }
563
+ if (title !== undefined) {
564
+ patch.Title = title;
565
+ }
566
+ return { patch };
567
+ }
568
+ // text — sticky notes and free text labels
569
+ if (item.Kind !== 'sticky' && item.Kind !== 'text') {
570
+ return { error: `"text" only applies to sticky notes and text labels (item ${item.ID} is a ${item.Kind}).` };
571
+ }
572
+ if (!text || text.trim().length === 0) {
573
+ return { error: '"text" must be a non-empty string.' };
574
+ }
575
+ return { patch: { Text: text } };
576
+ }
577
+ function updateContent(state, args) {
578
+ const itemId = asString(args['itemId']);
579
+ if (!itemId) {
580
+ return fail('UpdateContent requires "itemId".');
581
+ }
582
+ const item = state.GetItem(itemId);
583
+ if (!item) {
584
+ return fail(`UpdateContent: no item with id "${itemId}".`);
585
+ }
586
+ const result = contentPatchFor(item, args);
587
+ if ('error' in result) {
588
+ return fail(`UpdateContent: ${result.error}`);
589
+ }
590
+ // ONE UpdateItem in ONE batch — single undo step, single journal entry.
591
+ const applied = state.RunBatch(() => state.UpdateItem(itemId, result.patch, 'agent'));
592
+ if (!applied) {
593
+ return fail('The host application canceled this operation.');
594
+ }
595
+ return ok(itemId, `Updated the content of ${itemId}.`);
596
+ }
597
+ function drawConnector(state, args) {
598
+ const fromId = asString(args['fromId']);
599
+ const toId = asString(args['toId']);
600
+ const fromX = asNumber(args['fromX']);
601
+ const fromY = asNumber(args['fromY']);
602
+ const toX = asNumber(args['toX']);
603
+ const toY = asNumber(args['toY']);
604
+ if (fromId && !state.GetItem(fromId)) {
605
+ return fail(`DrawConnector: no item with id "${fromId}".`);
606
+ }
607
+ if (toId && !state.GetItem(toId)) {
608
+ return fail(`DrawConnector: no item with id "${toId}".`);
609
+ }
610
+ const hasFrom = !!fromId || (fromX !== undefined && fromY !== undefined);
611
+ const hasTo = !!toId || (toX !== undefined && toY !== undefined);
612
+ if (!hasFrom || !hasTo) {
613
+ return fail('DrawConnector requires both endpoints: fromId or fromX/fromY, and toId or toX/toY.');
614
+ }
615
+ const item = state.RunBatch(() => state.AddItem({
616
+ Kind: 'connector',
617
+ FromItemID: fromId ?? null,
618
+ ToItemID: toId ?? null,
619
+ FromPoint: fromId ? null : { X: fromX, Y: fromY },
620
+ ToPoint: toId ? null : { X: toX, Y: toY }
621
+ }, 'agent'));
622
+ if (!item) {
623
+ return fail('The host application canceled this operation.');
624
+ }
625
+ return ok(item.ID, `Drew a connector ${fromId ?? '(point)'} → ${toId ?? '(point)'}.`);
626
+ }
627
+ function highlight(state, args) {
628
+ const label = asString(args['label']);
629
+ const idsRaw = args['itemIds'];
630
+ if (Array.isArray(idsRaw) && idsRaw.length > 0) {
631
+ const items = [];
632
+ for (const raw of idsRaw) {
633
+ const id = asString(raw);
634
+ const item = id ? state.GetItem(id) : undefined;
635
+ if (!item) {
636
+ return fail(`Highlight: no item with id "${String(raw)}".`);
637
+ }
638
+ items.push(item);
639
+ }
640
+ const b = unionBounds(state, items);
641
+ const region = state.RunBatch(() => state.Highlight(b.X - HIGHLIGHT_PAD, b.Y - HIGHLIGHT_PAD, b.W + HIGHLIGHT_PAD * 2, b.H + HIGHLIGHT_PAD * 2, label, 'agent'));
642
+ if (!region) {
643
+ return fail('The host application canceled this operation.');
644
+ }
645
+ return ok(region.ID, `Highlighted ${items.length} item(s).`);
646
+ }
647
+ const x = asNumber(args['x']);
648
+ const y = asNumber(args['y']);
649
+ const w = asNumber(args['w']);
650
+ const h = asNumber(args['h']);
651
+ if (x === undefined || y === undefined || w === undefined || h === undefined) {
652
+ return fail('Highlight requires itemIds, or an explicit x/y/w/h region.');
653
+ }
654
+ const region = state.RunBatch(() => state.Highlight(x, y, w, h, label, 'agent'));
655
+ if (!region) {
656
+ return fail('The host application canceled this operation.');
657
+ }
658
+ return ok(region.ID, `Highlighted the region at (${Math.round(x)}, ${Math.round(y)}).`);
659
+ }
660
+ function moveItem(state, args) {
661
+ const itemId = asString(args['itemId']);
662
+ const x = asNumber(args['x']);
663
+ const y = asNumber(args['y']);
664
+ const w = asNumber(args['w']);
665
+ const h = asNumber(args['h']);
666
+ const moving = x !== undefined && y !== undefined;
667
+ const resizing = w !== undefined || h !== undefined;
668
+ if (!itemId || (!moving && !resizing)) {
669
+ return fail('MoveItem requires "itemId" plus "x"+"y" (move), "w"/"h" (resize), or both.');
670
+ }
671
+ if (!state.GetItem(itemId)) {
672
+ // Self-correction context: the id may be mistyped — or the item may live on
673
+ // another PAGE (item lookups are scoped to the active page).
674
+ const ids = state.Items.slice(0, 20).map((i) => i.ID).join(', ') || '(none)';
675
+ return fail(`MoveItem: no item with id "${itemId}" on the active page "${state.ActivePageName}". ` +
676
+ `Items here: ${ids}. If it lives on another page, switch first with ${WHITEBOARD_TOOL_NAMES.SwitchPage}.`);
677
+ }
678
+ const applied = state.RunBatch(() => {
679
+ let okAll = true;
680
+ if (moving) {
681
+ okAll = state.MoveItem(itemId, x, y, 'agent') && okAll;
682
+ }
683
+ if (resizing) {
684
+ const patch = {};
685
+ if (w !== undefined) {
686
+ patch.W = Math.max(24, w);
687
+ }
688
+ if (h !== undefined) {
689
+ patch.H = Math.max(24, h);
690
+ }
691
+ okAll = state.UpdateItem(itemId, patch, 'agent') && okAll;
692
+ }
693
+ return okAll;
694
+ });
695
+ if (!applied) {
696
+ return fail('The host application canceled this operation.');
697
+ }
698
+ const parts = [];
699
+ if (moving) {
700
+ parts.push(`moved to (${Math.round(x)}, ${Math.round(y)})`);
701
+ }
702
+ if (resizing) {
703
+ parts.push(`resized to ${w !== undefined ? Math.round(Math.max(24, w)) : '(unchanged)'}×${h !== undefined ? Math.round(Math.max(24, h)) : '(unchanged)'}`);
704
+ }
705
+ return ok(itemId, `${itemId} ${parts.join(' and ')}.`);
706
+ }
707
+ function removeItem(state, args) {
708
+ const itemId = asString(args['itemId']);
709
+ if (!itemId) {
710
+ return fail('RemoveItem requires "itemId".');
711
+ }
712
+ if (!state.GetItem(itemId)) {
713
+ return fail(`RemoveItem: no item with id "${itemId}".`);
714
+ }
715
+ const removed = state.RunBatch(() => state.RemoveItem(itemId, 'agent'));
716
+ if (!removed) {
717
+ return fail('The host application canceled this operation.');
718
+ }
719
+ return ok(itemId, `Removed ${itemId}.`);
720
+ }
721
+ function styleItem(state, args) {
722
+ const itemId = asString(args['itemId']);
723
+ if (!itemId) {
724
+ return fail('StyleItem requires "itemId".');
725
+ }
726
+ const item = state.GetItem(itemId);
727
+ if (!item) {
728
+ return fail(`StyleItem: no item with id "${itemId}".`);
729
+ }
730
+ if (item.Kind !== 'sticky' && item.Kind !== 'text') {
731
+ return fail(`StyleItem only styles sticky notes and text labels (got a ${item.Kind}).`);
732
+ }
733
+ const style = textStylePatch(args, item.Kind === 'text');
734
+ if ('error' in style) {
735
+ return fail(`StyleItem: ${style.error}`);
736
+ }
737
+ if (Object.keys(style.patch).length === 0) {
738
+ return fail('StyleItem requires at least one of "fontSize", "fontFamily", "bold" or "color".');
739
+ }
740
+ // ONE UpdateItem inside ONE batch — a single undo step and a single journal entry,
741
+ // so the toast Undo reverts it whole and the perception feed sees one update.
742
+ const styled = state.RunBatch(() => state.UpdateItem(itemId, style.patch, 'agent'));
743
+ if (!styled) {
744
+ return fail('The host application canceled this operation.');
745
+ }
746
+ return ok(itemId, `Restyled ${itemId}.`);
747
+ }
748
+ //# sourceMappingURL=whiteboard-tools.js.map