@memberjunction/ng-whiteboard 0.0.1 → 5.42.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 +302 -28
- package/dist/lib/whiteboard-agent-sees-popover.component.d.ts +31 -0
- package/dist/lib/whiteboard-agent-sees-popover.component.d.ts.map +1 -0
- package/dist/lib/whiteboard-agent-sees-popover.component.js +132 -0
- package/dist/lib/whiteboard-agent-sees-popover.component.js.map +1 -0
- package/dist/lib/whiteboard-board.component.d.ts +458 -0
- package/dist/lib/whiteboard-board.component.d.ts.map +1 -0
- package/dist/lib/whiteboard-board.component.js +2357 -0
- package/dist/lib/whiteboard-board.component.js.map +1 -0
- package/dist/lib/whiteboard-context-menu.d.ts +57 -0
- package/dist/lib/whiteboard-context-menu.d.ts.map +1 -0
- package/dist/lib/whiteboard-context-menu.js +69 -0
- package/dist/lib/whiteboard-context-menu.js.map +1 -0
- package/dist/lib/whiteboard-export.d.ts +94 -0
- package/dist/lib/whiteboard-export.d.ts.map +1 -0
- package/dist/lib/whiteboard-export.js +592 -0
- package/dist/lib/whiteboard-export.js.map +1 -0
- package/dist/lib/whiteboard-host.component.d.ts +174 -0
- package/dist/lib/whiteboard-host.component.d.ts.map +1 -0
- package/dist/lib/whiteboard-host.component.js +758 -0
- package/dist/lib/whiteboard-host.component.js.map +1 -0
- package/dist/lib/whiteboard-pages.component.d.ts +83 -0
- package/dist/lib/whiteboard-pages.component.d.ts.map +1 -0
- package/dist/lib/whiteboard-pages.component.js +211 -0
- package/dist/lib/whiteboard-pages.component.js.map +1 -0
- package/dist/lib/whiteboard-snapshot.component.d.ts +30 -0
- package/dist/lib/whiteboard-snapshot.component.d.ts.map +1 -0
- package/dist/lib/whiteboard-snapshot.component.js +91 -0
- package/dist/lib/whiteboard-snapshot.component.js.map +1 -0
- package/dist/lib/whiteboard-srcdoc.pipe.d.ts +60 -0
- package/dist/lib/whiteboard-srcdoc.pipe.d.ts.map +1 -0
- package/dist/lib/whiteboard-srcdoc.pipe.js +75 -0
- package/dist/lib/whiteboard-srcdoc.pipe.js.map +1 -0
- package/dist/lib/whiteboard-state.d.ts +1113 -0
- package/dist/lib/whiteboard-state.d.ts.map +1 -0
- package/dist/lib/whiteboard-state.js +1396 -0
- package/dist/lib/whiteboard-state.js.map +1 -0
- package/dist/lib/whiteboard-toolbar.component.d.ts +95 -0
- package/dist/lib/whiteboard-toolbar.component.d.ts.map +1 -0
- package/dist/lib/whiteboard-toolbar.component.js +344 -0
- package/dist/lib/whiteboard-toolbar.component.js.map +1 -0
- package/dist/lib/whiteboard-tools.d.ts +103 -0
- package/dist/lib/whiteboard-tools.d.ts.map +1 -0
- package/dist/lib/whiteboard-tools.js +748 -0
- package/dist/lib/whiteboard-tools.js.map +1 -0
- package/dist/lib/whiteboard-widget-bridge.d.ts +217 -0
- package/dist/lib/whiteboard-widget-bridge.d.ts.map +1 -0
- package/dist/lib/whiteboard-widget-bridge.js +254 -0
- package/dist/lib/whiteboard-widget-bridge.js.map +1 -0
- package/dist/lib/whiteboard-zoom.component.d.ts +55 -0
- package/dist/lib/whiteboard-zoom.component.d.ts.map +1 -0
- package/dist/lib/whiteboard-zoom.component.js +139 -0
- package/dist/lib/whiteboard-zoom.component.js.map +1 -0
- package/dist/lib/whiteboard.module.d.ts +29 -0
- package/dist/lib/whiteboard.module.d.ts.map +1 -0
- package/dist/lib/whiteboard.module.js +78 -0
- package/dist/lib/whiteboard.module.js.map +1 -0
- package/dist/public-api.d.ts +49 -0
- package/dist/public-api.d.ts.map +1 -0
- package/dist/public-api.js +59 -0
- package/dist/public-api.js.map +1 -0
- package/package.json +43 -6
package/README.md
CHANGED
|
@@ -1,45 +1,319 @@
|
|
|
1
1
|
# @memberjunction/ng-whiteboard
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A generic, reusable collaborative whiteboard for any Angular application — built for **dual authorship**: a human drawing with direct-manipulation tools, and a programmatic co-author (typically an AI agent) mutating the *same* board through a JSON-described tool API. One state engine, one undo history, one perception feed.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Installation
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```bash
|
|
8
|
+
npm install @memberjunction/ng-whiteboard
|
|
9
|
+
```
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
Peer dependencies: `@angular/common`, `@angular/core`, `@angular/platform-browser`. The package has **no Router dependency**, no MJ metadata/entity dependency, and no AI-framework dependency — it is a pure Generic-component package.
|
|
10
12
|
|
|
11
|
-
|
|
12
|
-
1. Configure OIDC trusted publishing for the package name `@memberjunction/ng-whiteboard`
|
|
13
|
-
2. Enable secure, token-less publishing from CI/CD workflows
|
|
14
|
-
3. Establish provenance for packages published under this name
|
|
13
|
+
## Overview
|
|
15
14
|
|
|
16
|
-
|
|
15
|
+
```mermaid
|
|
16
|
+
flowchart LR
|
|
17
|
+
subgraph Engine["WhiteboardState (Angular-free)"]
|
|
18
|
+
S["Items + selection"]
|
|
19
|
+
U["Undo/redo snapshots"]
|
|
20
|
+
J["Change journal → scene deltas"]
|
|
21
|
+
E["Before/after events"]
|
|
22
|
+
end
|
|
23
|
+
subgraph UI["Components"]
|
|
24
|
+
H["RealtimeWhiteboardHostComponent"]
|
|
25
|
+
B["RealtimeWhiteboardBoardComponent"]
|
|
26
|
+
T["Toolbar / Zoom / Popover"]
|
|
27
|
+
SN["WhiteboardSnapshotComponent (read-only)"]
|
|
28
|
+
end
|
|
29
|
+
subgraph Agent["Programmatic co-author"]
|
|
30
|
+
TD["WHITEBOARD_TOOL_DEFINITIONS"]
|
|
31
|
+
AP["ApplyWhiteboardAgentTool"]
|
|
32
|
+
end
|
|
33
|
+
H --> B
|
|
34
|
+
H --> T
|
|
35
|
+
B --> Engine
|
|
36
|
+
AP --> Engine
|
|
37
|
+
Engine -- "BuildSceneDelta" --> Agent
|
|
38
|
+
Engine -- "ToJSON / LoadFromJSON" --> P["Persistence (your storage)"]
|
|
39
|
+
SN --> Engine
|
|
40
|
+
```
|
|
17
41
|
|
|
18
|
-
|
|
42
|
+
The package is organized in layers, each usable on its own:
|
|
19
43
|
|
|
20
|
-
|
|
44
|
+
| Layer | File(s) | What it is |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| **State engine** | `whiteboard-state.ts` | `WhiteboardState` — the Angular-free typed board model. The *single* mutation API for both human tools and agent tools. |
|
|
47
|
+
| **Tool API** | `whiteboard-tools.ts` | The `Whiteboard_*` JSON tool definitions + `ApplyWhiteboardAgentTool`, the pure execute-one-call round-trip. |
|
|
48
|
+
| **Components** | `whiteboard-*.component.ts` | Host (full surface), board (canvas), toolbar, zoom cluster, "What the agent sees" popover, read-only snapshot. All standalone. |
|
|
49
|
+
| **Export** | `whiteboard-export.ts` | Pure builders for a self-contained HTML document and a standalone SVG. |
|
|
50
|
+
| **Widget bridge** | `whiteboard-widget-bridge.ts` | The `MJWhiteboard.submit` postMessage contract for sandboxed HTML widgets. |
|
|
51
|
+
| **Context menu** | `whiteboard-context-menu.ts` | The pure right-click menu model (what is offered per item kind). |
|
|
21
52
|
|
|
22
|
-
|
|
53
|
+
## Core Concepts
|
|
23
54
|
|
|
24
|
-
|
|
25
|
-
2. Configure the trusted publisher (e.g., GitHub Actions)
|
|
26
|
-
3. Specify the repository and workflow that should be allowed to publish
|
|
27
|
-
4. Use the configured workflow to publish your actual package
|
|
55
|
+
### Items
|
|
28
56
|
|
|
29
|
-
|
|
57
|
+
Everything on the board is a `WhiteboardItem` — a discriminated union over `Kind`:
|
|
30
58
|
|
|
31
|
-
|
|
32
|
-
- Contains no executable code
|
|
33
|
-
- Provides no functionality
|
|
34
|
-
- Should not be installed as a dependency
|
|
35
|
-
- Exists only for administrative purposes
|
|
59
|
+
`sticky` · `shape` (rect/ellipse/diamond) · `text` · `image` · `ink` (freehand stroke) · `connector` (item↔item or point↔point arrows) · `highlight` (transient "pointing" region) · `markdown` (rendered rich panel) · `html` (interactive sandboxed widget)
|
|
36
60
|
|
|
37
|
-
|
|
61
|
+
Every item carries an `Author` of `'user'` or `'agent'`. Authorship drives the ownership chrome — agent items render in a **reserved violet treatment** the user palette never offers, so provenance is always visible.
|
|
38
62
|
|
|
39
|
-
|
|
40
|
-
- [npm Trusted Publishing Documentation](https://docs.npmjs.com/generating-provenance-statements)
|
|
41
|
-
- [GitHub Actions OIDC Documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect)
|
|
63
|
+
### Pages (OneNote-style)
|
|
42
64
|
|
|
43
|
-
|
|
65
|
+
A board is an **ordered list of named pages**, each with its own items; exactly one page is ACTIVE and *every* item operation (add/update/move/remove/connectors/clear/perception/export) targets the active page. A fresh board starts with one page named **"Page 1"**.
|
|
44
66
|
|
|
45
|
-
**
|
|
67
|
+
- `AddPage(name?, author?)` — creates **and switches to** a new page; omit the name and it auto-names "Page N" from a monotonic counter (auto-names never repeat after removals).
|
|
68
|
+
- `SwitchPage(idOrName, author?)` — tolerant lookup: exact page ID first, then case-insensitive trimmed name. Switching journals a `'replace'` (the agent's visible scene swaps wholesale) but is **not** an undo step — it's navigation.
|
|
69
|
+
- `RenamePage(idOrName, newName, author?)` / `RemovePage(idOrName, author?)` — the **last remaining page can never be removed**; removing the *active* page activates a neighbor (the next page, else the previous).
|
|
70
|
+
- Reads: `Pages` (snapshots: `{ ID, Name, ItemCount, Active }`), `ActivePageID`, `ActivePageName`, `TotalItemCount` (across pages), `FindPage(idOrName)`.
|
|
71
|
+
|
|
72
|
+
Each page operation raises the package's standard **cancelable before / after event pair**: `PageAdding$`/`PageAdded$`, `PageSwitching$`/`PageSwitched$`, `PageRenaming$`/`PageRenamed$`, `PageRemoving$`/`PageRemoved$`. The board surface renders a compact bottom-left **page strip** (`RealtimeWhiteboardPagesComponent`): click switches, double-click renames inline (Enter commits, Esc cancels), hover "×" deletes, "+" adds. In `ReadOnly` mode the chips still switch pages (navigation) but add/rename/delete disappear.
|
|
73
|
+
|
|
74
|
+
### The engine
|
|
75
|
+
|
|
76
|
+
`WhiteboardState` owns items, single selection, and Z-order. Callers never stamp identity — `AddItem` assigns `ID`/`Z`/`Author`. Undo/redo is snapshot-based: one entry per user gesture, or one per agent tool call via `RunBatch` (so one toast-Undo reverts a whole tool effect).
|
|
77
|
+
|
|
78
|
+
### Journal & perception deltas
|
|
79
|
+
|
|
80
|
+
Every mutation appends to a compact change journal and emits on `Changed$`. A programmatic co-author "sees" the board through:
|
|
81
|
+
|
|
82
|
+
- **`BuildSceneDelta(sinceToken)`** — coalesces everything since a previously observed `CurrentSeq` into ONE delta with *replace-current-state* semantics: N moves of one item → one `moved` entry at the current position; add+remove → nothing; undo/redo/load/page-switch (or a too-old token) → `reset: true` plus the full compact scene. Never an append-only log.
|
|
83
|
+
- **`BuildSceneSummary()`** — the full compact scene + per-kind counts (what the "What the agent sees" popover renders).
|
|
84
|
+
|
|
85
|
+
Both payloads describe the **active page's** items and carry a `pages` array (`{ id, name, active, items }`) plus a summary line naming the active page — so the agent always knows pages exist and which one it is looking at.
|
|
86
|
+
|
|
87
|
+
### Persistence
|
|
88
|
+
|
|
89
|
+
`ToJSON()` serializes the state of record; `WhiteboardState.FromJSON(json)` rehydrates a new engine (throws on malformed input); `LoadFromJSON(json)` rehydrates **in place** — tolerant (returns `false`, never throws), preserves existing subscriptions, clears undo/journal, and emits one `'replace'` change. `ParseBoardStateJson` (from the snapshot component file) is the tolerant parse-or-null helper for viewers.
|
|
90
|
+
|
|
91
|
+
The serialized shape is **version 2 (paged)**:
|
|
92
|
+
|
|
93
|
+
```jsonc
|
|
94
|
+
{
|
|
95
|
+
"version": 2,
|
|
96
|
+
"seq": 12, "idCounter": 5, "zCounter": 5,
|
|
97
|
+
"pageCounter": 2, // monotonic — mints page ids / "Page N" auto-names
|
|
98
|
+
"activePageId": "page-2",
|
|
99
|
+
"pages": [
|
|
100
|
+
{ "id": "page-1", "name": "Page 1", "items": [ /* WhiteboardItem[] */ ] },
|
|
101
|
+
{ "id": "page-2", "name": "Drafts", "items": [ /* … */ ] }
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Backward compatibility:** the legacy flat shape (`version: 1` — `items` at the root, no pages) is still accepted by `FromJSON` / `LoadFromJSON` / `ParseBoardStateJson` and migrates to a single page named "Page 1". Serialization always emits the paged shape.
|
|
107
|
+
|
|
108
|
+
## Quickstart
|
|
109
|
+
|
|
110
|
+
### Full surface — the host component
|
|
111
|
+
|
|
112
|
+
The host renders the complete experience: header (title, saved chip, ownership legend, "What [Agent] sees" popover, Focus toggle, export menu), the canvas with floating toolbar, page strip + zoom cluster, the agent-action toast with Undo, and the status footer.
|
|
113
|
+
|
|
114
|
+
The zoom cluster supports **hold-to-zoom**: a plain click on + / − steps through the usual presets, while holding the button down zooms continuously in small smooth increments (~3.5% every 50 ms) until release — same 25%–200% clamp.
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import { Component } from '@angular/core';
|
|
118
|
+
import { RealtimeWhiteboardHostComponent, WhiteboardState } from '@memberjunction/ng-whiteboard';
|
|
119
|
+
|
|
120
|
+
@Component({
|
|
121
|
+
standalone: true,
|
|
122
|
+
imports: [RealtimeWhiteboardHostComponent],
|
|
123
|
+
template: `
|
|
124
|
+
<mj-realtime-whiteboard-host
|
|
125
|
+
[State]="Board"
|
|
126
|
+
[AgentName]="'Sage'"
|
|
127
|
+
[BoardTitle]="'Planning board'"
|
|
128
|
+
(SceneDelta)="onSceneDelta($event)"
|
|
129
|
+
(WidgetSubmitted)="onWidgetInput($event)"
|
|
130
|
+
(SaveToArtifactsRequested)="saveBoard()" />
|
|
131
|
+
`
|
|
132
|
+
})
|
|
133
|
+
export class MyPageComponent {
|
|
134
|
+
/** You own the engine — create it, persist it, hand it to the surface. */
|
|
135
|
+
public readonly Board = new WhiteboardState();
|
|
136
|
+
|
|
137
|
+
onSceneDelta(deltaJson: string): void {
|
|
138
|
+
// debounced (750 ms), coalesced — feed it to your agent's live context
|
|
139
|
+
}
|
|
140
|
+
onWidgetInput(e: { ItemID: string; Title: string; DataJson: string }): void {
|
|
141
|
+
// a sandboxed widget called MJWhiteboard.submit(data)
|
|
142
|
+
}
|
|
143
|
+
saveBoard(): void {
|
|
144
|
+
const json = this.Board.ToJSON(); // persist wherever you like
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Standalone board — just the canvas
|
|
150
|
+
|
|
151
|
+
`RealtimeWhiteboardBoardComponent` (`<mj-realtime-whiteboard>`) is the canvas alone — pan/zoom, per-tool pointer interactions, inline editing, context menu. You supply the tool state (the host normally does this):
|
|
152
|
+
|
|
153
|
+
```html
|
|
154
|
+
<mj-realtime-whiteboard
|
|
155
|
+
[State]="Board"
|
|
156
|
+
[Tool]="'select'"
|
|
157
|
+
[AgentName]="'Sage'"
|
|
158
|
+
[ReadOnly]="false" />
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Read-only snapshot
|
|
162
|
+
|
|
163
|
+
`WhiteboardSnapshotComponent` (`<mj-whiteboard-snapshot>`) renders a persisted payload through the real board in `ReadOnly` mode — identical visuals, pan/zoom for navigation, every mutation disabled:
|
|
164
|
+
|
|
165
|
+
```html
|
|
166
|
+
<mj-whiteboard-snapshot [StateJson]="savedBoardJson" [AgentName]="'Sage'" />
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### NgModule apps
|
|
170
|
+
|
|
171
|
+
`WhiteboardModule` re-exports all the standalone components for module-organized apps. There is also a `LoadWhiteboardComponents()` no-op for the rare all-dynamic-resolution bundling case.
|
|
172
|
+
|
|
173
|
+
## The Programmatic Tool API (agent integration recipe)
|
|
174
|
+
|
|
175
|
+
The agent-facing surface is intentionally transport-agnostic — it is just *names, JSON schemas, and one pure function*:
|
|
176
|
+
|
|
177
|
+
1. **Register the tools.** `WHITEBOARD_TOOL_DEFINITIONS` is an array of `{ Name, Description, ParametersSchema }` (structurally identical to `RealtimeToolDefinition` from `@memberjunction/ai`, so it drops straight into MJ realtime sessions — but any function-calling runtime works). All names share the `WHITEBOARD_TOOL_PREFIX` (`Whiteboard_`), so a single prefix route covers the set.
|
|
178
|
+
2. **Route tool calls back.** When your runtime receives a `Whiteboard_*` call, execute it locally:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { ApplyWhiteboardAgentTool } from '@memberjunction/ng-whiteboard';
|
|
182
|
+
|
|
183
|
+
const resultJson = ApplyWhiteboardAgentTool(state, toolName, argsJson);
|
|
184
|
+
// → feed resultJson back to the model as the tool response
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
`ApplyWhiteboardAgentTool` never throws — malformed args, unknown tools, unknown item IDs, and host-canceled operations all return `{ success: false, error }` so the model can self-correct conversationally. Each call runs as ONE undo batch (author `'agent'`).
|
|
188
|
+
|
|
189
|
+
Prefer the host's `ApplyAgentTool(toolName, argsJson)` when a surface is bound — same round-trip plus the UI garnish (violet pop-in, action toast with Undo, gliding presence cursor).
|
|
190
|
+
3. **Feed perception.** Subscribe the host's `SceneDelta` output (debounced 750 ms, coalesced) and pipe each JSON delta into the agent's context as background information. Use `CurrentSeq` / `BuildSceneDelta(sinceToken)` directly if you manage your own cadence.
|
|
191
|
+
|
|
192
|
+
The tool set: `AddNote`, `AddShape`, `AddText`, `AddMarkdown`, `AddHtml`, `UpdateContent`, `DrawConnector`, `Highlight`, `MoveItem`, `RemoveItem`, `StyleItem`, plus the page tools `AddPage` (`{ name? }` — create + switch), `SwitchPage` (`{ name }` — page name, case-insensitive, or id) and `RenamePage` (`{ name, newName }`) — see `WHITEBOARD_TOOL_NAMES` and the per-tool schemas in `whiteboard-tools.ts`. Item tools always target the **active page**; page-tool results carry `pageId` instead of `itemId`. Content sizes are capped (`WHITEBOARD_MARKDOWN_MAX_CHARS` = 32 000, `WHITEBOARD_HTML_MAX_CHARS` = 64 000).
|
|
193
|
+
|
|
194
|
+
> **Reference integration:** `@memberjunction/ng-conversations` wires this package into MJ realtime voice sessions as a pluggable channel (`RealtimeWhiteboardChannel`) and ships a saved-board artifact viewer — both are thin consumers of the APIs above.
|
|
195
|
+
|
|
196
|
+
## Before / After Events
|
|
197
|
+
|
|
198
|
+
Every major mutation raises a **cancelable BEFORE** event and a matching **AFTER** event. Handlers run synchronously during the emit; setting `Cancel = true` on the args aborts the operation cleanly — no undo snapshot, no journal entry, no `Changed$` emission, and the AFTER event never fires. These events layer *alongside* the existing `Changed$`/journal/perception machinery.
|
|
199
|
+
|
|
200
|
+
### Engine level (`WhiteboardState` observables)
|
|
201
|
+
|
|
202
|
+
| Before (cancelable) | After | Raised by | Args highlights |
|
|
203
|
+
|---|---|---|---|
|
|
204
|
+
| `ItemAdding$` | `ItemAdded$` | `AddItem` (incl. `Highlight`, `DuplicateItem`) | `Input` (mutable pre-stamp item), `Author` |
|
|
205
|
+
| `ItemUpdating$` | `ItemUpdated$` | `UpdateItem` (`Operation: 'update'`, mutable `Patch`), `MoveItem` (`'move'`, `Position`), `BringToFront`/`SendToBack` (`'reorder'`) | `Item` (live), `Operation`, `Author` |
|
|
206
|
+
| `ItemRemoving$` | `ItemRemoved$` | `RemoveItem` | `Item`, `Author` |
|
|
207
|
+
| `PageAdding$` | `PageAdded$` | `AddPage` | mutable `Name`, `Author` |
|
|
208
|
+
| `PageSwitching$` | `PageSwitched$` | `SwitchPage` | `FromPage`, `ToPage`, `Author` |
|
|
209
|
+
| `PageRenaming$` | `PageRenamed$` | `RenamePage` | `Page`, mutable `NewName`, `Author` |
|
|
210
|
+
| `PageRemoving$` | `PageRemoved$` | `RemovePage` | `Page` (+ `ActivatedPage` on the after event), `Author` |
|
|
211
|
+
|
|
212
|
+
Cancellation surfaces to callers: `AddItem` returns `null`; `UpdateItem`/`MoveItem`/`RemoveItem`/reorders return `false`; agent tools return a `{ success: false, error: '… canceled …' }` payload. BEFORE handlers may also *rewrite* the operation (`Input` on adds, `Patch` on updates) — moderation and clamping without cancel-and-replay.
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
// Example: block the agent from removing the user's items
|
|
216
|
+
board.ItemRemoving$.subscribe((e) => {
|
|
217
|
+
if (e.Author === 'agent' && e.Item.Author === 'user') {
|
|
218
|
+
e.Cancel = true;
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Component level
|
|
224
|
+
|
|
225
|
+
`RealtimeWhiteboardBoardComponent` adds two pairs of `@Output`s of its own:
|
|
226
|
+
|
|
227
|
+
| Before (cancelable) | After | Fired when |
|
|
228
|
+
|---|---|---|
|
|
229
|
+
| `ContentApplying` | `ContentApplied` | An in-board editor commit — inline sticky/text/shape edit, or the markdown/HTML rich editor's Apply/Done — is about to write to the engine. Args: `ItemID`, `Kind`, mutable `Content` (+ `Title` for HTML widgets). |
|
|
230
|
+
| `WidgetSubmitting` | `WidgetSubmitted` | A sandboxed widget's `MJWhiteboard.submit(data)` passed validation. Cancel to drop the submission. |
|
|
231
|
+
|
|
232
|
+
`RealtimeWhiteboardHostComponent` **mirrors** all engine pairs as outputs (`ItemAdding`/`ItemAdded`, `ItemUpdating`/`ItemUpdated`, `ItemRemoving`/`ItemRemoved`) and **forwards** the board pairs — so template-driven consumers get the entire surface in one place. Mirrors re-emit synchronously, so `Cancel = true` set in a template handler still vetoes the engine mutation.
|
|
233
|
+
|
|
234
|
+
```html
|
|
235
|
+
<mj-realtime-whiteboard-host
|
|
236
|
+
[State]="Board"
|
|
237
|
+
(ItemAdding)="$event.Cancel = !allowAdds"
|
|
238
|
+
(ItemAdded)="audit('add', $event.Item)"
|
|
239
|
+
(ContentApplying)="moderate($event)"
|
|
240
|
+
(WidgetSubmitted)="forwardToAgent($event)" />
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
## Sandboxed HTML Widgets & the Input Bridge
|
|
244
|
+
|
|
245
|
+
HTML widgets are self-contained documents (inline CSS/JS) rendered inside an iframe whose `sandbox` attribute is **`allow-scripts` only**:
|
|
246
|
+
|
|
247
|
+
- The frame runs in a unique **opaque origin** — its scripts cannot reach the parent document, the app session, cookies, or any storage. `allow-same-origin` is deliberately ruled out (it would let the frame script remove its own sandbox). By design there is **no DOM sanitization inside the sandbox** — the sandbox is the boundary, so SVG/script-heavy widget documents pass through byte-for-byte.
|
|
248
|
+
- Angular's sanitizer is bypassed *only* for the iframe `srcdoc` (the payload never touches the app's DOM). The trusted value is produced by the view-scoped `wbWidgetSrcdoc` pure pipe (`WhiteboardWidgetSrcdocPipe`), which fixes the lifecycle contract:
|
|
249
|
+
- **Widget documents are rebuilt per mount.** Every time a frame is (re)created — switching whiteboard pages away and back, the viewport-lazy toggle, panel collapse/expand, whole-board re-creation — the new view gets a new pipe instance and a freshly built document, so a re-mounted widget always renders exactly what the first mount rendered. (Do not reintroduce a component-level per-item `SafeHtml` cache: a stale instance handed to a re-created frame means no `srcdoc` write and a blank widget.)
|
|
250
|
+
- **Still-mounted widgets never reload on unrelated board activity.** For an unchanged `Html` source the pipe returns the identical `SafeHtml` instance, so whole-scene `'replace'` ops (page rename, undo/redo of other items, `LoadFromJSON` restores) cause no `srcdoc` rewrite and no iframe reload — widget runtime state survives. Only an actual content edit reloads the frame.
|
|
251
|
+
- Off-screen widgets render as placeholders — live frames are only instantiated near the viewport. Re-entering the viewport re-creates the frame from a freshly built document (internal widget state resets — acceptable for board decorations).
|
|
252
|
+
|
|
253
|
+
The only way data flows out is `postMessage`, governed by the **input bridge**:
|
|
254
|
+
|
|
255
|
+
1. `InjectWhiteboardSubmitHelper` prepends (idempotently) a tiny helper defining `window.MJWhiteboard.submit(data)` to every widget's srcdoc.
|
|
256
|
+
2. The board owns a single `window` `'message'` listener and validates each event with `EvaluateWidgetSubmitMessage`: the bridge marker must be present, `event.source` must resolve to a *tracked* widget iframe (anything else is dropped), and the JSON-serialized payload is capped at `WHITEBOARD_WIDGET_SUBMIT_MAX_CHARS` (8 000 chars).
|
|
257
|
+
3. Accepted submissions flow through `WidgetSubmitting` (cancelable) → `WidgetSubmitted`.
|
|
258
|
+
|
|
259
|
+
Markdown panels are the *non-executing* sibling: rendered through the shared `mj-markdown` component on the live board (sanitized, no raw-HTML passthrough) and through an inert escape-first renderer in exports.
|
|
260
|
+
|
|
261
|
+
## Export & Print
|
|
262
|
+
|
|
263
|
+
`whiteboard-export.ts` provides pure, deterministic builders (all user/agent text HTML-escaped). Exports render the **active page** — switch pages first to export another one:
|
|
264
|
+
|
|
265
|
+
- **`BuildWhiteboardExportHtml(state, { Title, AgentName, GeneratedAt })`** — ONE fully self-contained HTML document: inline CSS only, light paper palette, ownership chips, print-friendly `@media print` rules. HTML widgets export as placeholder cards with their escaped source in a `<details>` — live widget HTML is **never** inlined (no iframe sandbox exists in the export, so it would be XSS).
|
|
266
|
+
- **`BuildWhiteboardExportSvg(state)`** — a standalone SVG document of the same snapshot.
|
|
267
|
+
|
|
268
|
+
The host's export menu wires these to **Download HTML / Download SVG / Print** (print opens a blank window, writes the document, and invokes the dialog), plus a `SaveToArtifactsRequested` output for integrations that persist boards as first-class artifacts.
|
|
269
|
+
|
|
270
|
+
> The export palette is intentionally literal (not design tokens): self-contained documents must stand alone. This is the documented exception to the token rule.
|
|
271
|
+
|
|
272
|
+
## Theming
|
|
273
|
+
|
|
274
|
+
The live components style exclusively with MJ design tokens (`--mj-text-*`, `--mj-bg-surface-*`, `--mj-border-*`, …), so the board adapts to light/dark themes and white-labeling automatically. Two deliberate accents ride on top of the tokens:
|
|
275
|
+
|
|
276
|
+
- **Violet is the agent's** — agent stickies/ink/connectors/chips use the reserved violet treatment; the user palette (`WHITEBOARD_PEN_COLORS`) never offers it.
|
|
277
|
+
- **Amber is the user's sticky default** (two alternating tints).
|
|
278
|
+
|
|
279
|
+
Icons are Font Awesome throughout.
|
|
280
|
+
|
|
281
|
+
## Extension Points
|
|
282
|
+
|
|
283
|
+
- **Before/after events** — veto or rewrite any mutation, audit changes, forward submissions (see above). This is the primary extensibility seam.
|
|
284
|
+
- **Your own surface** — the engine is Angular-free; build alternate renderers or headless automations against `WhiteboardState` directly and they share undo/journal/perception with everything else.
|
|
285
|
+
- **Custom tool routing** — `ApplyWhiteboardAgentTool` is pure; wrap it to pre-process args, post-process results, or expose a different tool vocabulary that lowers onto the same engine calls.
|
|
286
|
+
- **Context menu model** — `BuildWhiteboardContextMenu(item)` is a pure model builder; reuse it in your own chrome if you replace the board's menu rendering.
|
|
287
|
+
- **Snapshot/viewer reuse** — `ParseBoardStateJson` + `WhiteboardSnapshotComponent` make read-only board rendering a two-liner in any review/preview surface.
|
|
288
|
+
|
|
289
|
+
## Public API Surface (selected)
|
|
290
|
+
|
|
291
|
+
```typescript
|
|
292
|
+
// Engine
|
|
293
|
+
WhiteboardState, WhiteboardItem (union), WhiteboardItemInput, WhiteboardItemPatch,
|
|
294
|
+
WhiteboardChange, WhiteboardSceneDelta, WhiteboardSceneSummary, WHITEBOARD_DEFAULTS,
|
|
295
|
+
WhiteboardItemAddingEventArgs / Added / Updating / Updated / Removing / Removed,
|
|
296
|
+
WhiteboardPageInfo, WhiteboardScenePage,
|
|
297
|
+
WhiteboardPageAddingEventArgs / Added / Switching / Switched / Renaming / Renamed / Removing / Removed
|
|
298
|
+
|
|
299
|
+
// Tools
|
|
300
|
+
WHITEBOARD_TOOL_DEFINITIONS, WHITEBOARD_TOOL_NAMES, WHITEBOARD_TOOL_PREFIX,
|
|
301
|
+
WhiteboardToolDefinition, WhiteboardToolResult, ApplyWhiteboardAgentTool
|
|
302
|
+
|
|
303
|
+
// Components
|
|
304
|
+
RealtimeWhiteboardHostComponent, RealtimeWhiteboardBoardComponent,
|
|
305
|
+
RealtimeWhiteboardToolbarComponent, RealtimeWhiteboardZoomComponent,
|
|
306
|
+
RealtimeWhiteboardPagesComponent, RealtimeWhiteboardAgentSeesPopoverComponent,
|
|
307
|
+
WhiteboardSnapshotComponent, WhiteboardModule
|
|
308
|
+
|
|
309
|
+
// Export / bridge / menu
|
|
310
|
+
BuildWhiteboardExportHtml, BuildWhiteboardExportSvg, RenderMarkdownInert,
|
|
311
|
+
InjectWhiteboardSubmitHelper, EvaluateWidgetSubmitMessage, ParseBoardStateJson,
|
|
312
|
+
BuildWhiteboardContextMenu
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Everything exported carries full JSDoc — IDE hover docs are the authoritative per-member reference.
|
|
316
|
+
|
|
317
|
+
## Conventions
|
|
318
|
+
|
|
319
|
+
This package follows the MemberJunction Generic-component rules: PascalCase public members (inputs/outputs/methods), camelCase private members, standalone components with modern `@if`/`@for` templates, `inject()` DI, no Router imports, and design-token-only styling.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
/**
|
|
3
|
+
* The "What [Agent] sees" perception popover: shows the most recent coalesced scene
|
|
4
|
+
* delta JSON (syntax-tinted) plus the perception-feed stats footer (debounce, element
|
|
5
|
+
* count, last-delta age, state of record). Anchored under the header button by the host;
|
|
6
|
+
* visibility is the host's concern.
|
|
7
|
+
*/
|
|
8
|
+
export declare class RealtimeWhiteboardAgentSeesPopoverComponent {
|
|
9
|
+
/** Display name of the session's agent. */
|
|
10
|
+
AgentName: string;
|
|
11
|
+
/** Perception debounce, in ms (the host's SceneDelta debounce). */
|
|
12
|
+
DebounceMs: number;
|
|
13
|
+
/** Current element count on the board. */
|
|
14
|
+
ElementCount: number;
|
|
15
|
+
/** Friendly age of the last emitted delta ("2 s ago" / "—"). */
|
|
16
|
+
LastDeltaLabel: string;
|
|
17
|
+
/** The latest scene-delta JSON (pretty-printed). Setter pre-tints for display. */
|
|
18
|
+
set DeltaJson(value: string);
|
|
19
|
+
get DeltaJson(): string;
|
|
20
|
+
private _deltaJson;
|
|
21
|
+
/** HTML-escaped JSON wrapped in token-tint spans (keys/strings/numbers/comments). */
|
|
22
|
+
TintedHtml: string;
|
|
23
|
+
/**
|
|
24
|
+
* Minimal JSON syntax tinting matching the mockup's `.codeblock` token classes.
|
|
25
|
+
* Input is fully HTML-escaped first, so the produced markup contains only our spans.
|
|
26
|
+
*/
|
|
27
|
+
private static tint;
|
|
28
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<RealtimeWhiteboardAgentSeesPopoverComponent, never>;
|
|
29
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<RealtimeWhiteboardAgentSeesPopoverComponent, "mj-realtime-whiteboard-agent-sees-popover", never, { "AgentName": { "alias": "AgentName"; "required": false; }; "DebounceMs": { "alias": "DebounceMs"; "required": false; }; "ElementCount": { "alias": "ElementCount"; "required": false; }; "LastDeltaLabel": { "alias": "LastDeltaLabel"; "required": false; }; "DeltaJson": { "alias": "DeltaJson"; "required": false; }; }, {}, never, never, true, never>;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=whiteboard-agent-sees-popover.component.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whiteboard-agent-sees-popover.component.d.ts","sourceRoot":"","sources":["../../src/lib/whiteboard-agent-sees-popover.component.ts"],"names":[],"mappings":";AAGA;;;;;GAKG;AACH,qBAOa,2CAA2C;IACtD,2CAA2C;IAClC,SAAS,SAAW;IAC7B,mEAAmE;IAC1D,UAAU,SAAO;IAC1B,0CAA0C;IACjC,YAAY,SAAK;IAC1B,gEAAgE;IACvD,cAAc,SAAO;IAE9B,kFAAkF;IAClF,IACI,SAAS,CAAC,KAAK,EAAE,MAAM,EAK1B;IACD,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,OAAO,CAAC,UAAU,CAAM;IAExB,qFAAqF;IAC9E,UAAU,SAAM;IAEvB;;;OAGG;IACH,OAAO,CAAC,MAAM,CAAC,IAAI;yCA/BR,2CAA2C;2CAA3C,2CAA2C;CA2CvD"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Component, Input } from '@angular/core';
|
|
2
|
+
import { CommonModule } from '@angular/common';
|
|
3
|
+
import * as i0 from "@angular/core";
|
|
4
|
+
function RealtimeWhiteboardAgentSeesPopoverComponent_Conditional_9_Template(rf, ctx) { if (rf & 1) {
|
|
5
|
+
i0.ɵɵdomElementStart(0, "pre", 6);
|
|
6
|
+
i0.ɵɵdomElement(1, "span", 8);
|
|
7
|
+
i0.ɵɵtext(2, "\n");
|
|
8
|
+
i0.ɵɵdomElementStart(3, "span", 9);
|
|
9
|
+
i0.ɵɵtext(4, "// replace-current-state semantics \u2014 not an append-only log");
|
|
10
|
+
i0.ɵɵdomElementEnd()();
|
|
11
|
+
} if (rf & 2) {
|
|
12
|
+
const ctx_r0 = i0.ɵɵnextContext();
|
|
13
|
+
i0.ɵɵadvance();
|
|
14
|
+
i0.ɵɵdomProperty("innerHTML", ctx_r0.TintedHtml, i0.ɵɵsanitizeHtml);
|
|
15
|
+
} }
|
|
16
|
+
function RealtimeWhiteboardAgentSeesPopoverComponent_Conditional_10_Template(rf, ctx) { if (rf & 1) {
|
|
17
|
+
i0.ɵɵdomElementStart(0, "pre", 6)(1, "span", 9);
|
|
18
|
+
i0.ɵɵtext(2, "// no deltas yet \u2014 start sketching");
|
|
19
|
+
i0.ɵɵdomElementEnd()();
|
|
20
|
+
} }
|
|
21
|
+
/**
|
|
22
|
+
* The "What [Agent] sees" perception popover: shows the most recent coalesced scene
|
|
23
|
+
* delta JSON (syntax-tinted) plus the perception-feed stats footer (debounce, element
|
|
24
|
+
* count, last-delta age, state of record). Anchored under the header button by the host;
|
|
25
|
+
* visibility is the host's concern.
|
|
26
|
+
*/
|
|
27
|
+
export class RealtimeWhiteboardAgentSeesPopoverComponent {
|
|
28
|
+
constructor() {
|
|
29
|
+
/** Display name of the session's agent. */
|
|
30
|
+
this.AgentName = 'Agent';
|
|
31
|
+
/** Perception debounce, in ms (the host's SceneDelta debounce). */
|
|
32
|
+
this.DebounceMs = 750;
|
|
33
|
+
/** Current element count on the board. */
|
|
34
|
+
this.ElementCount = 0;
|
|
35
|
+
/** Friendly age of the last emitted delta ("2 s ago" / "—"). */
|
|
36
|
+
this.LastDeltaLabel = '—';
|
|
37
|
+
this._deltaJson = '';
|
|
38
|
+
/** HTML-escaped JSON wrapped in token-tint spans (keys/strings/numbers/comments). */
|
|
39
|
+
this.TintedHtml = '';
|
|
40
|
+
}
|
|
41
|
+
/** The latest scene-delta JSON (pretty-printed). Setter pre-tints for display. */
|
|
42
|
+
set DeltaJson(value) {
|
|
43
|
+
if (value !== this._deltaJson) {
|
|
44
|
+
this._deltaJson = value;
|
|
45
|
+
this.TintedHtml = RealtimeWhiteboardAgentSeesPopoverComponent.tint(value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
get DeltaJson() {
|
|
49
|
+
return this._deltaJson;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Minimal JSON syntax tinting matching the mockup's `.codeblock` token classes.
|
|
53
|
+
* Input is fully HTML-escaped first, so the produced markup contains only our spans.
|
|
54
|
+
*/
|
|
55
|
+
static tint(json) {
|
|
56
|
+
const escaped = json
|
|
57
|
+
.replace(/&/g, '&')
|
|
58
|
+
.replace(/</g, '<')
|
|
59
|
+
.replace(/>/g, '>');
|
|
60
|
+
return escaped
|
|
61
|
+
.replace(/"([^"]+)"(\s*):/g, '<span class="ck">"$1"</span>$2:')
|
|
62
|
+
.replace(/:(\s*)"([^"]*)"/g, ':$1<span class="cs">"$2"</span>')
|
|
63
|
+
.replace(/:(\s*)(-?\d+(?:\.\d+)?)/g, ':$1<span class="cn">$2</span>')
|
|
64
|
+
.replace(/(\[\s*)"([^"]*)"/g, '$1<span class="cs">"$2"</span>')
|
|
65
|
+
.replace(/(,\s*)"([^"]*)"(\s*[,\]])/g, '$1<span class="cs">"$2"</span>$3');
|
|
66
|
+
}
|
|
67
|
+
static { this.ɵfac = function RealtimeWhiteboardAgentSeesPopoverComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || RealtimeWhiteboardAgentSeesPopoverComponent)(); }; }
|
|
68
|
+
static { this.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: RealtimeWhiteboardAgentSeesPopoverComponent, selectors: [["mj-realtime-whiteboard-agent-sees-popover"]], inputs: { AgentName: "AgentName", DebounceMs: "DebounceMs", ElementCount: "ElementCount", LastDeltaLabel: "LastDeltaLabel", DeltaJson: "DeltaJson" }, decls: 28, vars: 6, consts: [[1, "sees-pop", 3, "click"], [1, "sees-pop__head"], [1, "fa-solid", "fa-feather"], [1, "pill", "pill--sage"], [1, "sees-pop__body"], [1, "sees-pop__cap"], [1, "codeblock"], [1, "sees-pop__foot"], [3, "innerHTML"], [1, "cc"]], template: function RealtimeWhiteboardAgentSeesPopoverComponent_Template(rf, ctx) { if (rf & 1) {
|
|
69
|
+
i0.ɵɵdomElementStart(0, "div", 0);
|
|
70
|
+
i0.ɵɵdomListener("click", function RealtimeWhiteboardAgentSeesPopoverComponent_Template_div_click_0_listener($event) { return $event.stopPropagation(); });
|
|
71
|
+
i0.ɵɵdomElementStart(1, "div", 1);
|
|
72
|
+
i0.ɵɵdomElement(2, "i", 2);
|
|
73
|
+
i0.ɵɵtext(3);
|
|
74
|
+
i0.ɵɵdomElementStart(4, "span", 3);
|
|
75
|
+
i0.ɵɵtext(5, "coalesced delta");
|
|
76
|
+
i0.ɵɵdomElementEnd()();
|
|
77
|
+
i0.ɵɵdomElementStart(6, "div", 4)(7, "p", 5);
|
|
78
|
+
i0.ɵɵtext(8);
|
|
79
|
+
i0.ɵɵdomElementEnd();
|
|
80
|
+
i0.ɵɵconditionalCreate(9, RealtimeWhiteboardAgentSeesPopoverComponent_Conditional_9_Template, 5, 1, "pre", 6)(10, RealtimeWhiteboardAgentSeesPopoverComponent_Conditional_10_Template, 3, 0, "pre", 6);
|
|
81
|
+
i0.ɵɵdomElementEnd();
|
|
82
|
+
i0.ɵɵdomElementStart(11, "div", 7)(12, "span");
|
|
83
|
+
i0.ɵɵtext(13, "debounce ");
|
|
84
|
+
i0.ɵɵdomElementStart(14, "b");
|
|
85
|
+
i0.ɵɵtext(15);
|
|
86
|
+
i0.ɵɵdomElementEnd()();
|
|
87
|
+
i0.ɵɵdomElementStart(16, "span");
|
|
88
|
+
i0.ɵɵtext(17, "elements ");
|
|
89
|
+
i0.ɵɵdomElementStart(18, "b");
|
|
90
|
+
i0.ɵɵtext(19);
|
|
91
|
+
i0.ɵɵdomElementEnd()();
|
|
92
|
+
i0.ɵɵdomElementStart(20, "span");
|
|
93
|
+
i0.ɵɵtext(21, "last delta ");
|
|
94
|
+
i0.ɵɵdomElementStart(22, "b");
|
|
95
|
+
i0.ɵɵtext(23);
|
|
96
|
+
i0.ɵɵdomElementEnd()();
|
|
97
|
+
i0.ɵɵdomElementStart(24, "span");
|
|
98
|
+
i0.ɵɵtext(25, "state of record ");
|
|
99
|
+
i0.ɵɵdomElementStart(26, "b");
|
|
100
|
+
i0.ɵɵtext(27, "session artifact");
|
|
101
|
+
i0.ɵɵdomElementEnd()()()();
|
|
102
|
+
} if (rf & 2) {
|
|
103
|
+
i0.ɵɵadvance(3);
|
|
104
|
+
i0.ɵɵtextInterpolate1(" Live perception feed \u2192 ", ctx.AgentName, "'s context ");
|
|
105
|
+
i0.ɵɵadvance(5);
|
|
106
|
+
i0.ɵɵtextInterpolate1(" ", ctx.AgentName, " doesn't see pixels \u2014 every change you make is serialized into a compact, debounced scene delta and fed into the live agent context. This is the most recent one: ");
|
|
107
|
+
i0.ɵɵadvance();
|
|
108
|
+
i0.ɵɵconditional(ctx.TintedHtml ? 9 : 10);
|
|
109
|
+
i0.ɵɵadvance(6);
|
|
110
|
+
i0.ɵɵtextInterpolate1("", ctx.DebounceMs, " ms");
|
|
111
|
+
i0.ɵɵadvance(4);
|
|
112
|
+
i0.ɵɵtextInterpolate(ctx.ElementCount);
|
|
113
|
+
i0.ɵɵadvance(4);
|
|
114
|
+
i0.ɵɵtextInterpolate(ctx.LastDeltaLabel);
|
|
115
|
+
} }, dependencies: [CommonModule], styles: ["\n\n\n\n[_nghost-%COMP%] {\n \n\n --sage: var(--mj-color-violet-400, #a78bfa);\n --sage-bg: color-mix(in srgb, var(--mj-color-violet-400, #a78bfa) 14%, transparent);\n --sage-border: color-mix(in srgb, var(--mj-color-violet-400, #a78bfa) 45%, transparent);\n\n display: block;\n position: absolute;\n top: calc(100% + 10px);\n right: 0;\n z-index: 60;\n width: 420px;\n cursor: default;\n text-align: left;\n}\n\n.sees-pop[_ngcontent-%COMP%] {\n background: var(--mj-bg-surface-card);\n border: 1px solid var(--sage-border);\n border-radius: var(--mj-radius-lg, 12px);\n box-shadow: var(--mj-shadow-2xl, 0 25px 50px -12px rgba(0, 0, 0, 0.45));\n animation: _ngcontent-%COMP%_wb-pop-fade 160ms ease;\n}\n@keyframes _ngcontent-%COMP%_wb-pop-fade {\n from { opacity: 0; transform: translateY(-4px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n.sees-pop__head[_ngcontent-%COMP%] {\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 11px 14px;\n border-bottom: 1px solid var(--mj-border-default);\n font-size: 12px;\n font-weight: 700;\n color: var(--mj-color-violet-300, var(--mj-text-primary));\n}\n\n.pill[_ngcontent-%COMP%] {\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 2px 9px;\n border-radius: var(--mj-radius-full, 9999px);\n font-size: 9.5px;\n font-weight: 700;\n border: 1px solid transparent;\n line-height: 1.6;\n white-space: nowrap;\n}\n.pill--sage[_ngcontent-%COMP%] {\n background: var(--sage-bg);\n color: var(--mj-color-violet-300, var(--mj-text-secondary));\n border-color: var(--sage-border);\n}\n\n.sees-pop__body[_ngcontent-%COMP%] {\n padding: 12px 14px;\n}\n.sees-pop__cap[_ngcontent-%COMP%] {\n font-size: 11.5px;\n color: var(--mj-text-secondary);\n line-height: 1.5;\n margin: 0 0 10px;\n font-weight: 400;\n}\n\n.codeblock[_ngcontent-%COMP%] {\n margin: 0;\n padding: 10px 12px;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md, 8px);\n background: var(--mj-bg-surface-sunken);\n font-family: var(--mj-font-mono, ui-monospace, monospace);\n font-size: 10.5px;\n line-height: 1.55;\n color: var(--mj-text-secondary);\n max-height: 190px;\n overflow: auto;\n white-space: pre-wrap;\n word-break: break-word;\n}\n\n\n\n.codeblock[_ngcontent-%COMP%] .ck { color: #9cdcfe; }\n.codeblock[_ngcontent-%COMP%] .cs { color: #ce9178; }\n.codeblock[_ngcontent-%COMP%] .cn { color: #b5cea8; }\n.codeblock[_ngcontent-%COMP%] .cc[_ngcontent-%COMP%], \n.codeblock[_ngcontent-%COMP%] .cc { color: #6a9955; }\n\n.sees-pop__foot[_ngcontent-%COMP%] {\n display: flex;\n gap: 14px;\n padding: 9px 14px;\n border-top: 1px solid var(--mj-border-subtle, var(--mj-border-default));\n font-size: 10.5px;\n color: var(--mj-text-muted);\n font-weight: 500;\n flex-wrap: wrap;\n}\n.sees-pop__foot[_ngcontent-%COMP%] b[_ngcontent-%COMP%] {\n color: var(--mj-text-secondary);\n}"] }); }
|
|
116
|
+
}
|
|
117
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(RealtimeWhiteboardAgentSeesPopoverComponent, [{
|
|
118
|
+
type: Component,
|
|
119
|
+
args: [{ standalone: true, selector: 'mj-realtime-whiteboard-agent-sees-popover', imports: [CommonModule], template: "<div class=\"sees-pop\" (click)=\"$event.stopPropagation()\">\n <div class=\"sees-pop__head\">\n <i class=\"fa-solid fa-feather\"></i> Live perception feed \u2192 {{ AgentName }}'s context\n <span class=\"pill pill--sage\">coalesced delta</span>\n </div>\n <div class=\"sees-pop__body\">\n <p class=\"sees-pop__cap\">\n {{ AgentName }} doesn't see pixels \u2014 every change you make is serialized into a compact,\n debounced scene delta and fed into the live agent context. This is the most recent one:\n </p>\n @if (TintedHtml) {\n <pre class=\"codeblock\"><span [innerHTML]=\"TintedHtml\"></span>\n<span class=\"cc\">// replace-current-state semantics \u2014 not an append-only log</span></pre>\n } @else {\n <pre class=\"codeblock\"><span class=\"cc\">// no deltas yet \u2014 start sketching</span></pre>\n }\n </div>\n <div class=\"sees-pop__foot\">\n <span>debounce <b>{{ DebounceMs }} ms</b></span>\n <span>elements <b>{{ ElementCount }}</b></span>\n <span>last delta <b>{{ LastDeltaLabel }}</b></span>\n <span>state of record <b>session artifact</b></span>\n </div>\n</div>\n", styles: ["/* \"What the agent sees\" perception popover \u2014 ported pixel-faithfully from\n plans/realtime/mockups/whiteboard.html (.sees-pop) + mockups.css (.codeblock, .pill--sage). */\n\n:host {\n /* agent (violet) accent \u2014 derived the same way as the other realtime components */\n --sage: var(--mj-color-violet-400, #a78bfa);\n --sage-bg: color-mix(in srgb, var(--mj-color-violet-400, #a78bfa) 14%, transparent);\n --sage-border: color-mix(in srgb, var(--mj-color-violet-400, #a78bfa) 45%, transparent);\n\n display: block;\n position: absolute;\n top: calc(100% + 10px);\n right: 0;\n z-index: 60;\n width: 420px;\n cursor: default;\n text-align: left;\n}\n\n.sees-pop {\n background: var(--mj-bg-surface-card);\n border: 1px solid var(--sage-border);\n border-radius: var(--mj-radius-lg, 12px);\n box-shadow: var(--mj-shadow-2xl, 0 25px 50px -12px rgba(0, 0, 0, 0.45));\n animation: wb-pop-fade 160ms ease;\n}\n@keyframes wb-pop-fade {\n from { opacity: 0; transform: translateY(-4px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n.sees-pop__head {\n display: flex;\n align-items: center;\n gap: 9px;\n padding: 11px 14px;\n border-bottom: 1px solid var(--mj-border-default);\n font-size: 12px;\n font-weight: 700;\n color: var(--mj-color-violet-300, var(--mj-text-primary));\n}\n\n.pill {\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 2px 9px;\n border-radius: var(--mj-radius-full, 9999px);\n font-size: 9.5px;\n font-weight: 700;\n border: 1px solid transparent;\n line-height: 1.6;\n white-space: nowrap;\n}\n.pill--sage {\n background: var(--sage-bg);\n color: var(--mj-color-violet-300, var(--mj-text-secondary));\n border-color: var(--sage-border);\n}\n\n.sees-pop__body {\n padding: 12px 14px;\n}\n.sees-pop__cap {\n font-size: 11.5px;\n color: var(--mj-text-secondary);\n line-height: 1.5;\n margin: 0 0 10px;\n font-weight: 400;\n}\n\n.codeblock {\n margin: 0;\n padding: 10px 12px;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md, 8px);\n background: var(--mj-bg-surface-sunken);\n font-family: var(--mj-font-mono, ui-monospace, monospace);\n font-size: 10.5px;\n line-height: 1.55;\n color: var(--mj-text-secondary);\n max-height: 190px;\n overflow: auto;\n white-space: pre-wrap;\n word-break: break-word;\n}\n/* token tints \u2014 matching the mockup's static code-editor palette (intentionally literal;\n these mirror VS Code's dark JSON colors and stay constant across themes) */\n.codeblock ::ng-deep .ck { color: #9cdcfe; }\n.codeblock ::ng-deep .cs { color: #ce9178; }\n.codeblock ::ng-deep .cn { color: #b5cea8; }\n.codeblock .cc,\n.codeblock ::ng-deep .cc { color: #6a9955; }\n\n.sees-pop__foot {\n display: flex;\n gap: 14px;\n padding: 9px 14px;\n border-top: 1px solid var(--mj-border-subtle, var(--mj-border-default));\n font-size: 10.5px;\n color: var(--mj-text-muted);\n font-weight: 500;\n flex-wrap: wrap;\n}\n.sees-pop__foot b {\n color: var(--mj-text-secondary);\n}\n"] }]
|
|
120
|
+
}], null, { AgentName: [{
|
|
121
|
+
type: Input
|
|
122
|
+
}], DebounceMs: [{
|
|
123
|
+
type: Input
|
|
124
|
+
}], ElementCount: [{
|
|
125
|
+
type: Input
|
|
126
|
+
}], LastDeltaLabel: [{
|
|
127
|
+
type: Input
|
|
128
|
+
}], DeltaJson: [{
|
|
129
|
+
type: Input
|
|
130
|
+
}] }); })();
|
|
131
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(RealtimeWhiteboardAgentSeesPopoverComponent, { className: "RealtimeWhiteboardAgentSeesPopoverComponent", filePath: "src/lib/whiteboard-agent-sees-popover.component.ts", lineNumber: 17 }); })();
|
|
132
|
+
//# sourceMappingURL=whiteboard-agent-sees-popover.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"whiteboard-agent-sees-popover.component.js","sourceRoot":"","sources":["../../src/lib/whiteboard-agent-sees-popover.component.ts","../../src/lib/whiteboard-agent-sees-popover.component.html"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;;;ICUzC,iCAAuB;IAAA,6BAAsC;IACnE,kBAAA;IAAA,kCAAiB;IAAA,gFAA2D;IAAO,AAAP,oBAAO,EAAM;;;IADtD,cAAwB;IAAxB,mEAAwB;;;IAG9B,AAAvB,iCAAuB,cAAiB;IAAA,uDAAkC;IAAO,AAAP,oBAAO,EAAM;;ADX7F;;;;;GAKG;AAQH,MAAM,OAAO,2CAA2C;IAPxD;QAQE,2CAA2C;QAClC,cAAS,GAAG,OAAO,CAAC;QAC7B,mEAAmE;QAC1D,eAAU,GAAG,GAAG,CAAC;QAC1B,0CAA0C;QACjC,iBAAY,GAAG,CAAC,CAAC;QAC1B,gEAAgE;QACvD,mBAAc,GAAG,GAAG,CAAC;QActB,eAAU,GAAG,EAAE,CAAC;QAExB,qFAAqF;QAC9E,eAAU,GAAG,EAAE,CAAC;KAkBxB;IAjCC,kFAAkF;IAClF,IACI,SAAS,CAAC,KAAa;QACzB,IAAI,KAAK,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;YAC9B,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;YACxB,IAAI,CAAC,UAAU,GAAG,2CAA2C,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC5E,CAAC;IACH,CAAC;IACD,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;IAOD;;;OAGG;IACK,MAAM,CAAC,IAAI,CAAC,IAAY;QAC9B,MAAM,OAAO,GAAG,IAAI;aACjB,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;aACtB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;aACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACzB,OAAO,OAAO;aACX,OAAO,CAAC,kBAAkB,EAAE,iCAAiC,CAAC;aAC9D,OAAO,CAAC,kBAAkB,EAAE,iCAAiC,CAAC;aAC9D,OAAO,CAAC,0BAA0B,EAAE,+BAA+B,CAAC;aACpE,OAAO,CAAC,mBAAmB,EAAE,gCAAgC,CAAC;aAC9D,OAAO,CAAC,4BAA4B,EAAE,kCAAkC,CAAC,CAAC;IAC/E,CAAC;4IA1CU,2CAA2C;oEAA3C,2CAA2C;YChBxD,iCAAyD;YAAnC,8HAAS,wBAAwB,IAAC;YACtD,iCAA4B;YAC1B,0BAAmC;YAAC,YACpC;YAAA,kCAA8B;YAAA,+BAAe;YAC/C,AAD+C,oBAAO,EAChD;YAEJ,AADF,iCAA4B,WACD;YACvB,YAEF;YAAA,oBAAI;YAIF,AAHF,6GAAkB,yFAGT;YAGX,oBAAM;YAEJ,AADF,kCAA4B,YACpB;YAAA,0BAAS;YAAA,6BAAG;YAAA,aAAmB;YAAI,AAAJ,oBAAI,EAAO;YAChD,gCAAM;YAAA,0BAAS;YAAA,6BAAG;YAAA,aAAkB;YAAI,AAAJ,oBAAI,EAAO;YAC/C,gCAAM;YAAA,4BAAW;YAAA,6BAAG;YAAA,aAAoB;YAAI,AAAJ,oBAAI,EAAO;YACnD,gCAAM;YAAA,iCAAgB;YAAA,6BAAG;YAAA,iCAAgB;YAE7C,AADE,AAD+C,AAAJ,oBAAI,EAAO,EAChD,EACF;;YArBkC,eACpC;YADoC,oFACpC;YAIE,eAEF;YAFE,oNAEF;YACA,cAKC;YALD,yCAKC;YAGiB,eAAmB;YAAnB,gDAAmB;YACnB,eAAkB;YAAlB,sCAAkB;YAChB,eAAoB;YAApB,wCAAoB;4BDRhC,YAAY;;iFAIX,2CAA2C;cAPvD,SAAS;6BACI,IAAI,YACN,2CAA2C,WAC5C,CAAC,YAAY,CAAC;;kBAMtB,KAAK;;kBAEL,KAAK;;kBAEL,KAAK;;kBAEL,KAAK;;kBAGL,KAAK;;kFAXK,2CAA2C","sourcesContent":["import { Component, Input } from '@angular/core';\nimport { CommonModule } from '@angular/common';\n\n/**\n * The \"What [Agent] sees\" perception popover: shows the most recent coalesced scene\n * delta JSON (syntax-tinted) plus the perception-feed stats footer (debounce, element\n * count, last-delta age, state of record). Anchored under the header button by the host;\n * visibility is the host's concern.\n */\n@Component({\n standalone: true,\n selector: 'mj-realtime-whiteboard-agent-sees-popover',\n imports: [CommonModule],\n templateUrl: './whiteboard-agent-sees-popover.component.html',\n styleUrl: './whiteboard-agent-sees-popover.component.css'\n})\nexport class RealtimeWhiteboardAgentSeesPopoverComponent {\n /** Display name of the session's agent. */\n @Input() AgentName = 'Agent';\n /** Perception debounce, in ms (the host's SceneDelta debounce). */\n @Input() DebounceMs = 750;\n /** Current element count on the board. */\n @Input() ElementCount = 0;\n /** Friendly age of the last emitted delta (\"2 s ago\" / \"—\"). */\n @Input() LastDeltaLabel = '—';\n\n /** The latest scene-delta JSON (pretty-printed). Setter pre-tints for display. */\n @Input()\n set DeltaJson(value: string) {\n if (value !== this._deltaJson) {\n this._deltaJson = value;\n this.TintedHtml = RealtimeWhiteboardAgentSeesPopoverComponent.tint(value);\n }\n }\n get DeltaJson(): string {\n return this._deltaJson;\n }\n\n private _deltaJson = '';\n\n /** HTML-escaped JSON wrapped in token-tint spans (keys/strings/numbers/comments). */\n public TintedHtml = '';\n\n /**\n * Minimal JSON syntax tinting matching the mockup's `.codeblock` token classes.\n * Input is fully HTML-escaped first, so the produced markup contains only our spans.\n */\n private static tint(json: string): string {\n const escaped = json\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>');\n return escaped\n .replace(/\"([^\"]+)\"(\\s*):/g, '<span class=\"ck\">\"$1\"</span>$2:')\n .replace(/:(\\s*)\"([^\"]*)\"/g, ':$1<span class=\"cs\">\"$2\"</span>')\n .replace(/:(\\s*)(-?\\d+(?:\\.\\d+)?)/g, ':$1<span class=\"cn\">$2</span>')\n .replace(/(\\[\\s*)\"([^\"]*)\"/g, '$1<span class=\"cs\">\"$2\"</span>')\n .replace(/(,\\s*)\"([^\"]*)\"(\\s*[,\\]])/g, '$1<span class=\"cs\">\"$2\"</span>$3');\n }\n}\n","<div class=\"sees-pop\" (click)=\"$event.stopPropagation()\">\n <div class=\"sees-pop__head\">\n <i class=\"fa-solid fa-feather\"></i> Live perception feed → {{ AgentName }}'s context\n <span class=\"pill pill--sage\">coalesced delta</span>\n </div>\n <div class=\"sees-pop__body\">\n <p class=\"sees-pop__cap\">\n {{ AgentName }} doesn't see pixels — every change you make is serialized into a compact,\n debounced scene delta and fed into the live agent context. This is the most recent one:\n </p>\n @if (TintedHtml) {\n <pre class=\"codeblock\"><span [innerHTML]=\"TintedHtml\"></span>\n<span class=\"cc\">// replace-current-state semantics — not an append-only log</span></pre>\n } @else {\n <pre class=\"codeblock\"><span class=\"cc\">// no deltas yet — start sketching</span></pre>\n }\n </div>\n <div class=\"sees-pop__foot\">\n <span>debounce <b>{{ DebounceMs }} ms</b></span>\n <span>elements <b>{{ ElementCount }}</b></span>\n <span>last delta <b>{{ LastDeltaLabel }}</b></span>\n <span>state of record <b>session artifact</b></span>\n </div>\n</div>\n"]}
|