@robosystems/core 0.11.5 → 0.13.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.
@@ -6,6 +6,7 @@ import { useCallback, useEffect, useRef, useState } from 'react';
6
6
  import { HiTerminal } from 'react-icons/hi';
7
7
  import { useGraphContext } from '../../contexts';
8
8
  import { useStreamingQuery } from '../../hooks';
9
+ import { emitGraphWrites, readGraphWrites, } from '../../lib/graph-writes';
9
10
  import { ConsoleMarkdown } from './ConsoleMarkdown';
10
11
  import { ProgressiveText } from './ProgressiveText';
11
12
  function generateMessageId() {
@@ -53,6 +54,23 @@ function downloadText(filename, text, mime) {
53
54
  anchor.click();
54
55
  URL.revokeObjectURL(url);
55
56
  }
57
+ const WRITE_LABELS = {
58
+ 'create-taxonomy-block': 'Created taxonomy block',
59
+ 'create-information-block': 'Created block',
60
+ 'update-information-block': 'Updated block',
61
+ 'assert-metrics': 'Asserted metrics',
62
+ 'compute-metrics': 'Computed metrics',
63
+ 'compute-forecast': 'Computed forecast',
64
+ 'create-agent': 'Created counterparty',
65
+ 'update-agent': 'Updated counterparty',
66
+ remember: 'Saved memory',
67
+ };
68
+ function describeWrite(write) {
69
+ var _a, _b;
70
+ const label = (_a = WRITE_LABELS[write.operation]) !== null && _a !== void 0 ? _a : write.operation;
71
+ const subject = (_b = write.name) !== null && _b !== void 0 ? _b : write.id;
72
+ return subject ? `${label}: ${subject}` : label;
73
+ }
56
74
  function copyRowsJson(rows) {
57
75
  var _a;
58
76
  (_a = navigator.clipboard) === null || _a === void 0 ? void 0 : _a.writeText(JSON.stringify(rows, null, 2));
@@ -102,6 +120,9 @@ export function ConsoleContent({ config }) {
102
120
  (config.enableRecall
103
121
  ? ` /recall - Recall semantic memories\n`
104
122
  : '') +
123
+ (config.enableAuthor
124
+ ? ` /do - Make a change (a block, a counterparty, a memory)\n`
125
+ : '') +
105
126
  ` /mcp - Connect an MCP client (sign in — no key)\n` +
106
127
  ` /mcp key - Mint a graph-scoped API key for header-only clients\n` +
107
128
  ` /help - Show this help message\n` +
@@ -145,6 +166,7 @@ export function ConsoleContent({ config }) {
145
166
  setTerminalMessages((prev) => [...prev, message]);
146
167
  }, []);
147
168
  const addResultMessage = useCallback((content, data, cypher, opts) => {
169
+ var _a;
148
170
  const message = {
149
171
  id: generateMessageId(),
150
172
  type: 'system',
@@ -154,6 +176,7 @@ export function ConsoleContent({ config }) {
154
176
  cypher,
155
177
  markdown: opts === null || opts === void 0 ? void 0 : opts.markdown,
156
178
  footer: opts === null || opts === void 0 ? void 0 : opts.footer,
179
+ writes: ((_a = opts === null || opts === void 0 ? void 0 : opts.writes) === null || _a === void 0 ? void 0 : _a.length) ? opts.writes : undefined,
157
180
  };
158
181
  setTerminalMessages((prev) => [...prev, message]);
159
182
  }, []);
@@ -328,7 +351,9 @@ export function ConsoleContent({ config }) {
328
351
  setCurrentQueryStartTime(null);
329
352
  }
330
353
  };
331
- const executeOperatorQuery = async (userQuery) => {
354
+ // `operatorType` names an operator (`author` for /do); unset, the API
355
+ // chooses one for a question.
356
+ const executeOperatorQuery = async (userQuery, operatorType) => {
332
357
  var _a;
333
358
  if (!graphId) {
334
359
  addErrorMessage(config.noSelectionError);
@@ -342,10 +367,11 @@ export function ConsoleContent({ config }) {
342
367
  try {
343
368
  const { clients } = await import('@robosystems/client/clients');
344
369
  const history = conversationRef.current;
345
- const result = await clients.operator.executeQuery(graphId, Object.assign({ message: userQuery,
370
+ const request = Object.assign({ message: userQuery,
346
371
  // `standard` gives the operator loop six tool turns; `quick` (three)
347
372
  // was exhausted by schema + examples before the first query ran.
348
- mode: 'standard' }, (history.length > 0 ? { history } : {})), {
373
+ mode: 'standard' }, (history.length > 0 ? { history } : {}));
374
+ const options = {
349
375
  mode: 'auto',
350
376
  onProgress: (message, percentage) => {
351
377
  if (isStale())
@@ -356,7 +382,10 @@ export function ConsoleContent({ config }) {
356
382
  percentage,
357
383
  });
358
384
  },
359
- });
385
+ };
386
+ const result = operatorType
387
+ ? await clients.operator.executeOperator(graphId, operatorType, request, options)
388
+ : await clients.operator.executeQuery(graphId, request, options);
360
389
  // The user switched graphs while the operator ran: the reset already
361
390
  // cleared the console, so drop the answer rather than print it (and
362
391
  // record it as history) under the new graph.
@@ -378,6 +407,9 @@ export function ConsoleContent({ config }) {
378
407
  }
379
408
  const duration = Date.now() - startTime;
380
409
  const metadata = (result.metadata || {});
410
+ const writes = readGraphWrites(metadata);
411
+ // Pages showing this graph reload what the console just changed.
412
+ emitGraphWrites(requestGraphId, writes);
381
413
  const creditsUsed = metadata.credits_consumed;
382
414
  const resultCount = metadata.result_count;
383
415
  // Prefer the structured outputs the operator loop now returns
@@ -429,7 +461,11 @@ export function ConsoleContent({ config }) {
429
461
  // otherwise collapse under markdown). With no narrative, fall back to the
430
462
  // plain footer, matching the direct-query result style.
431
463
  if (narrative) {
432
- addResultMessage(narrative, rows, cypher, { markdown: true, footer });
464
+ addResultMessage(narrative, rows, cypher, {
465
+ markdown: true,
466
+ footer,
467
+ writes,
468
+ });
433
469
  // Record the exchange for follow-ups. Only a narrative answer is
434
470
  // worth carrying; the answer is clipped so history stays cheap.
435
471
  conversationRef.current = [
@@ -439,7 +475,7 @@ export function ConsoleContent({ config }) {
439
475
  ].slice(-6);
440
476
  }
441
477
  else {
442
- addResultMessage(footer, rows, cypher);
478
+ addResultMessage(footer, rows, cypher, { writes });
443
479
  }
444
480
  }
445
481
  catch (error) {
@@ -596,6 +632,17 @@ export function ConsoleContent({ config }) {
596
632
  }
597
633
  return;
598
634
  }
635
+ // Handle /do — the author operator, which may change the graph. Writes
636
+ // are never inferred from a question; they need this command.
637
+ if (config.enableAuthor && /^\/do(\s|$)/i.test(command)) {
638
+ const request = command.slice(3).trim();
639
+ if (!request) {
640
+ addErrorMessage('Usage: /do <change>\n\nExamples:\n /do add Notion Labs as a vendor\n /do remember that we report ARPC on subscription revenue only');
641
+ return;
642
+ }
643
+ await executeOperatorQuery(request, 'author');
644
+ return;
645
+ }
599
646
  // Handle /recall command — built-in only when the target supports
600
647
  // semantic memory (user graphs; shared repositories reject it). When
601
648
  // disabled it falls through to the unknown-command branch below.
@@ -702,7 +749,10 @@ export function ConsoleContent({ config }) {
702
749
  ? 'text-green-400'
703
750
  : message.type === 'error'
704
751
  ? 'text-red-400'
705
- : 'text-gray-300'}`, children: [message.type === 'user' && (_jsx("span", { className: "mr-2 text-green-500", children: "$" })), message.isAnimating ? (_jsx(ProgressiveText, { text: message.content, onComplete: () => handleAnimationComplete(message.id), onUpdate: scrollToBottom })) : message.markdown ? (_jsx(ConsoleMarkdown, { content: message.content })) : (message.content)] }), message.footer && (_jsx("div", { className: "mt-3 text-xs whitespace-pre-wrap text-gray-500", children: message.footer })), message.cypher && (_jsxs("div", { className: "mt-3 overflow-hidden rounded border border-gray-800 bg-gray-900/40", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-800 px-3 py-1.5", children: [_jsx("span", { className: "text-xs tracking-wider text-gray-500 uppercase", children: "Generated Cypher" }), _jsx("button", { onClick: () => { var _a; return handleCommand(`/query ${(_a = message.cypher) !== null && _a !== void 0 ? _a : ''}`); }, className: "rounded bg-cyan-600/90 px-2 py-0.5 text-xs font-medium text-white transition-colors hover:bg-cyan-600", children: "Run" })] }), _jsx("pre", { className: "overflow-x-auto px-3 py-2 font-mono text-xs whitespace-pre-wrap text-cyan-300", children: message.cypher })] })), message.data && message.data.length > 0 && (_jsxs("div", { className: "mt-3 overflow-hidden rounded border border-gray-800", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-800 bg-gray-900 px-4 py-2", children: [_jsxs("span", { className: "text-xs text-gray-500", children: [message.data.length, " row", message.data.length === 1 ? '' : 's'] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { onClick: () => copyRowsJson(message.data), className: "rounded border border-gray-700 px-2 py-0.5 text-xs text-gray-300 transition-colors hover:bg-gray-800", children: "Copy JSON" }), _jsx("button", { onClick: () => downloadText('results.csv', rowsToCsv(message.data), 'text/csv'), className: "rounded border border-gray-700 px-2 py-0.5 text-xs text-gray-300 transition-colors hover:bg-gray-800", children: "Download CSV" })] })] }), _jsx("div", { className: "max-h-96 overflow-auto", children: _jsxs("table", { className: "w-full border-collapse text-xs", children: [_jsx("thead", { className: "sticky top-0", children: _jsx("tr", { className: "border-b border-gray-800 bg-gray-900", children: Object.keys(message.data[0]).map((key) => (_jsx("th", { className: "px-4 py-2 text-left font-semibold whitespace-nowrap text-cyan-400", children: key }, key))) }) }), _jsx("tbody", { children: message.data
752
+ : 'text-gray-300'}`, children: [message.type === 'user' && (_jsx("span", { className: "mr-2 text-green-500", children: "$" })), message.isAnimating ? (_jsx(ProgressiveText, { text: message.content, onComplete: () => handleAnimationComplete(message.id), onUpdate: scrollToBottom })) : message.markdown ? (_jsx(ConsoleMarkdown, { content: message.content })) : (message.content)] }), message.writes && (_jsxs("div", { className: "mt-3 rounded border border-emerald-900/60 bg-emerald-950/30 px-3 py-2", children: [_jsx("div", { className: "text-xs tracking-wider text-emerald-400 uppercase", children: "Changes made" }), _jsx("ul", { className: "mt-1 space-y-0.5 text-sm text-emerald-200", children: message.writes.map((write, idx) => {
753
+ var _a;
754
+ return (_jsxs("li", { children: ["\u2713 ", describeWrite(write)] }, `${write.operation}-${(_a = write.id) !== null && _a !== void 0 ? _a : idx}`));
755
+ }) })] })), message.footer && (_jsx("div", { className: "mt-3 text-xs whitespace-pre-wrap text-gray-500", children: message.footer })), message.cypher && (_jsxs("div", { className: "mt-3 overflow-hidden rounded border border-gray-800 bg-gray-900/40", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-800 px-3 py-1.5", children: [_jsx("span", { className: "text-xs tracking-wider text-gray-500 uppercase", children: "Generated Cypher" }), _jsx("button", { onClick: () => { var _a; return handleCommand(`/query ${(_a = message.cypher) !== null && _a !== void 0 ? _a : ''}`); }, className: "rounded bg-cyan-600/90 px-2 py-0.5 text-xs font-medium text-white transition-colors hover:bg-cyan-600", children: "Run" })] }), _jsx("pre", { className: "overflow-x-auto px-3 py-2 font-mono text-xs whitespace-pre-wrap text-cyan-300", children: message.cypher })] })), message.data && message.data.length > 0 && (_jsxs("div", { className: "mt-3 overflow-hidden rounded border border-gray-800", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-gray-800 bg-gray-900 px-4 py-2", children: [_jsxs("span", { className: "text-xs text-gray-500", children: [message.data.length, " row", message.data.length === 1 ? '' : 's'] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { onClick: () => copyRowsJson(message.data), className: "rounded border border-gray-700 px-2 py-0.5 text-xs text-gray-300 transition-colors hover:bg-gray-800", children: "Copy JSON" }), _jsx("button", { onClick: () => downloadText('results.csv', rowsToCsv(message.data), 'text/csv'), className: "rounded border border-gray-700 px-2 py-0.5 text-xs text-gray-300 transition-colors hover:bg-gray-800", children: "Download CSV" })] })] }), _jsx("div", { className: "max-h-96 overflow-auto", children: _jsxs("table", { className: "w-full border-collapse text-xs", children: [_jsx("thead", { className: "sticky top-0", children: _jsx("tr", { className: "border-b border-gray-800 bg-gray-900", children: Object.keys(message.data[0]).map((key) => (_jsx("th", { className: "px-4 py-2 text-left font-semibold whitespace-nowrap text-cyan-400", children: key }, key))) }) }), _jsx("tbody", { children: message.data
706
756
  .slice(0, MAX_TABLE_ROWS)
707
757
  .map((row, idx) => (_jsx("tr", { className: "border-b border-gray-900 hover:bg-gray-900/50", children: Object.keys(message.data[0]).map((key) => {
708
758
  const value = row[key];
@@ -404,6 +404,8 @@ export function buildGraphAwareConsoleConfig(graph, branding) {
404
404
  noSelectionError: (_c = branding.noSelectionError) !== null && _c !== void 0 ? _c : 'No graph selected. Please select a graph first.',
405
405
  // Semantic memory is per-user-graph; shared repositories reject it.
406
406
  enableRecall: !isRepository,
407
+ // Writes need a user graph; the API also checks the write role.
408
+ enableAuthor: !isRepository,
407
409
  };
408
410
  }
409
411
  /**
@@ -1,3 +1,4 @@
1
+ import type { GraphWrite } from '../../lib/graph-writes';
1
2
  export interface SampleQuery {
2
3
  name: string;
3
4
  query: string;
@@ -65,6 +66,9 @@ export interface ConsoleConfig {
65
66
  * shared repositories don't support memory, so the graph-aware builder
66
67
  * enables it only for user graphs. */
67
68
  enableRecall?: boolean;
69
+ /** Enable `/do`, which runs the write-capable author operator. The API
70
+ * gates it on the graph write role; off for shared repositories. */
71
+ enableAuthor?: boolean;
68
72
  /** Extra slash commands beyond the built-in set */
69
73
  extraCommands?: ConsoleExtraCommand[];
70
74
  }
@@ -90,4 +94,6 @@ export interface TerminalMessage {
90
94
  * breaks survive markdown rendering.
91
95
  */
92
96
  footer?: string;
97
+ /** Changes the author operator made, shown as a receipt below the answer. */
98
+ writes?: GraphWrite[];
93
99
  }
@@ -0,0 +1,66 @@
1
+ export interface Rect {
2
+ x: number;
3
+ y: number;
4
+ w: number;
5
+ h: number;
6
+ }
7
+ export interface DemoTheme {
8
+ /** Selector for the sidebar that holds `.pill` and the `.nv[data-k]` items. */
9
+ side: string;
10
+ /** Resting item colour, as RGB. */
11
+ rest: [number, number, number];
12
+ /** Resting colour of an indented `.nv.sub` item; defaults to `rest`. */
13
+ restSub?: [number, number, number];
14
+ /** Colour of the item under the pill. */
15
+ active: [number, number, number];
16
+ }
17
+ export interface DemoContext {
18
+ $: (id: string) => HTMLElement;
19
+ root: ShadowRoot;
20
+ stage: HTMLElement;
21
+ rel: (el: Element) => Rect;
22
+ ring: (hl: HTMLElement, target: Element, p: number, pad?: number) => void;
23
+ nav: (k: string, from?: string, m?: number, scope?: ParentNode) => void;
24
+ }
25
+ export interface DemoLayout {
26
+ width: number;
27
+ height: number;
28
+ css?: string;
29
+ }
30
+ export interface DemoDefinition extends DemoLayout {
31
+ total: number;
32
+ poster?: number;
33
+ html: string;
34
+ setup: (ctx: DemoContext) => (t: number) => void;
35
+ mobile?: DemoLayout;
36
+ }
37
+ export interface MountOptions {
38
+ autoplay?: boolean;
39
+ phone?: boolean;
40
+ /** The app's base CSS: its tokens, window chrome and shared components. */
41
+ css?: string;
42
+ theme?: DemoTheme;
43
+ }
44
+ export interface DemoHandle {
45
+ seek: (t: number) => void;
46
+ destroy: () => void;
47
+ }
48
+ export declare const clamp01: (x: number) => number;
49
+ export declare const seg: (t: number, a: number, b: number) => number;
50
+ export declare const eo: (x: number) => number;
51
+ export declare const eio: (x: number) => number;
52
+ export declare const typed: (s: string, t: number, t0: number, cps?: number) => string;
53
+ export declare const spin: (t: number) => string;
54
+ export declare const money: (v: number, dp?: number) => string;
55
+ export declare function rise(el: HTMLElement, p: number, dy?: number): void;
56
+ export declare const dip: (t: number, at: number, d?: number) => number;
57
+ export declare function swap(el: HTMLElement, t: number, at: number, a?: string | null, b?: string | null, cls?: [string, string]): boolean;
58
+ export declare function steps(el: HTMLElement, t: number, times: number[], texts?: string[] | null, classes?: string[] | null): number;
59
+ export declare function blurIn(el: HTMLElement, p: number, dy?: number): void;
60
+ export declare function push(el: HTMLElement, t: number, total: number, ox: number, oy: number, amount?: number): void;
61
+ export declare function pointer(ctx: DemoContext, cur: HTMLElement, el: Element, t: number, t0: number, t1: number, { from, out, }?: {
62
+ from?: [number, number];
63
+ out?: number;
64
+ }): boolean;
65
+ export declare function variant(def: DemoDefinition, phone: boolean): DemoDefinition;
66
+ export declare function mount(host: HTMLElement, def: DemoDefinition, { autoplay, phone, css, theme }?: MountOptions): DemoHandle;
@@ -0,0 +1,278 @@
1
+ /*
2
+ * The runtime behind the landing-page demos in each app's /public/demos.
3
+ *
4
+ * A demo module default-exports { width, height, total, poster, css, html,
5
+ * setup(ctx), mobile? } where setup returns seek(t), a pure function of time.
6
+ * The same module runs live on the landing page (mount, in a shadow root,
7
+ * looping while visible) and frame by frame under the content machine's
8
+ * renderer (render.html), so the page and the social cut share one source.
9
+ *
10
+ * This file compiles to dependency-free ESM that a browser loads as-is: each
11
+ * app commits a copy at public/demos/runtime.js (npm run sync:demos) and a
12
+ * test there fails when the copy drifts from the installed package. The app's
13
+ * own kit.js wraps mount() with its window chrome, base CSS and sidebar theme.
14
+ */
15
+ export const clamp01 = (x) => Math.max(0, Math.min(1, x));
16
+ export const seg = (t, a, b) => clamp01((t - a) / (b - a));
17
+ export const eo = (x) => 1 - Math.pow(1 - clamp01(x), 3);
18
+ export const eio = (x) => {
19
+ x = clamp01(x);
20
+ return x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2;
21
+ };
22
+ export const typed = (s, t, t0, cps = 34) => s.slice(0, Math.max(0, Math.floor((t - t0) * cps)));
23
+ export const spin = (t) => '◐◓◑◒'[Math.floor(t * 8) % 4];
24
+ export const money = (v, dp = 2) => '$' +
25
+ v.toLocaleString('en-US', {
26
+ minimumFractionDigits: dp,
27
+ maximumFractionDigits: dp,
28
+ });
29
+ export function rise(el, p, dy = 40) {
30
+ el.style.opacity = String(p);
31
+ el.style.transform = `translateY(${(1 - p) * dy}px)`;
32
+ }
33
+ /* Opacity for an element whose content changes at `at`: dips through 0.15 over `d` seconds. */
34
+ export const dip = (t, at, d = 0.3) => Math.min(1, 0.15 + 0.85 * (Math.abs(t - at) / (d / 2)));
35
+ /*
36
+ * Change a label or badge without a one-frame pop: before `at` it shows `a`,
37
+ * after it `b` (text, and optional class names), with an opacity dip across
38
+ * the change. With `a` null only the dip applies, for text set elsewhere.
39
+ */
40
+ export function swap(el, t, at, a, b, cls) {
41
+ const after = t >= at;
42
+ if (a != null)
43
+ el.textContent = after ? (b !== null && b !== void 0 ? b : '') : a;
44
+ if (cls)
45
+ el.className = after ? cls[1] : cls[0];
46
+ el.style.opacity = String(dip(t, at));
47
+ return after;
48
+ }
49
+ /* A badge that moves through states at `times`: texts[k] and classes[k], dipping at each change. */
50
+ export function steps(el, t, times, texts, classes) {
51
+ let k = 0;
52
+ times.forEach((x) => (k += t >= x ? 1 : 0));
53
+ if (texts)
54
+ el.textContent = texts[k];
55
+ if (classes)
56
+ el.className = classes[k];
57
+ el.style.opacity = String(Math.min(1, ...times.map((x) => dip(t, x))));
58
+ return k;
59
+ }
60
+ /* Headline entrance: blur to sharp, rising, as p goes 0 to 1. */
61
+ export function blurIn(el, p, dy = 24) {
62
+ const e = eo(p);
63
+ el.style.opacity = String(clamp01(p * 1.4));
64
+ el.style.filter = e < 1 ? `blur(${(1 - e) * 14}px)` : '';
65
+ el.style.transform = `translateY(${(1 - e) * dy}px)`;
66
+ }
67
+ /*
68
+ * A slow camera push over the loop toward (ox, oy), in stage px, easing back
69
+ * before the loop wraps, so no screen sits still like a slide.
70
+ */
71
+ export function push(el, t, total, ox, oy, amount = 0.04) {
72
+ const k = amount *
73
+ eio(seg(t, 0.4, total - 1.2)) *
74
+ (1 - eio(seg(t, total - 1.2, total)));
75
+ el.style.transformOrigin = `${ox}px ${oy}px`;
76
+ el.style.transform = `scale(${1 + k})`;
77
+ }
78
+ /* A drawn pointer that travels from `from` to the centre of `el`, presses, and fades out. */
79
+ export function pointer(ctx, cur, el, t, t0, t1, { from = [260, 160], out = 0.6, } = {}) {
80
+ if (t < t0 - 0.15 || t > t1 + out) {
81
+ cur.style.opacity = '0';
82
+ return t > t1;
83
+ }
84
+ const r = ctx.rel(el);
85
+ const tx = r.x + r.w * 0.55;
86
+ const ty = r.y + r.h * 0.55;
87
+ const m = eio(seg(t, t0, t1));
88
+ cur.style.left = tx + (1 - m) * from[0] + 'px';
89
+ cur.style.top = ty + (1 - m) * from[1] + 'px';
90
+ cur.style.opacity = String(seg(t, t0 - 0.15, t0) * (1 - seg(t, t1 + out - 0.2, t1 + out)));
91
+ const pressed = t > t1 && t < t1 + 0.18;
92
+ cur.style.transform = `scale(${pressed ? 0.85 : 1})`;
93
+ return t > t1;
94
+ }
95
+ /* The stage's own reset; everything else, including the tokens, comes from the app. */
96
+ const STAGE_CSS = `
97
+ :host { display: block; }
98
+ .stage { position: absolute; left: 0; top: 0; transform-origin: 0 0; overflow: hidden; }
99
+ :where(.stage *) { box-sizing: border-box; margin: 0; padding: 0; }
100
+ `;
101
+ function makeCtx(root, stage, getScale, theme) {
102
+ const $ = (id) => root.getElementById(id);
103
+ const rel = (el) => {
104
+ const s = stage.getBoundingClientRect();
105
+ const r = el.getBoundingClientRect();
106
+ const k = getScale();
107
+ return {
108
+ x: (r.left - s.left) / k,
109
+ y: (r.top - s.top) / k,
110
+ w: r.width / k,
111
+ h: r.height / k,
112
+ };
113
+ };
114
+ /* ring `hl` (absolutely positioned in its offsetParent) around `target` */
115
+ const ring = (hl, target, p, pad = 6) => {
116
+ const parent = hl.offsetParent;
117
+ // a hidden scene has no layout to measure; skip rather than throw
118
+ if (!parent) {
119
+ hl.style.opacity = '0';
120
+ return;
121
+ }
122
+ const a = rel(target);
123
+ const b = rel(parent);
124
+ // a camera push scales the parent; undo it so the ring sits in its local px
125
+ const k = b.w / parent.offsetWidth || 1;
126
+ hl.style.left = (a.x - b.x) / k - pad + 'px';
127
+ hl.style.top = (a.y - b.y) / k - pad * 0.7 + 'px';
128
+ hl.style.width = a.w / k + pad * 2 + 'px';
129
+ hl.style.height = a.h / k + pad * 1.4 + 'px';
130
+ hl.style.opacity = String(p);
131
+ };
132
+ /* slide the pill of the sidebar inside `scope` to `k`, blending from `from` by m */
133
+ const nav = (k, from = k, m = 1, scope = root) => {
134
+ if (!theme)
135
+ return;
136
+ const side = scope.querySelector(theme.side);
137
+ if (!side)
138
+ return;
139
+ const item = (key) => side.querySelector(`[data-k="${key}"]`);
140
+ const y = (key) => { var _a, _b; return (_b = (_a = item(key)) === null || _a === void 0 ? void 0 : _a.offsetTop) !== null && _b !== void 0 ? _b : 0; };
141
+ const pill = side.querySelector('.pill');
142
+ if (pill)
143
+ pill.style.top = y(from) + (y(k) - y(from)) * eio(m) + 'px';
144
+ // the active item's colour blends across the move instead of switching
145
+ const w = eio(m);
146
+ side.querySelectorAll('.nv').forEach((d) => {
147
+ var _a;
148
+ const key = d.dataset.k;
149
+ const on = (key === k ? w : 0) + (key === from && from !== k ? 1 - w : 0);
150
+ const rest = d.classList.contains('sub')
151
+ ? ((_a = theme.restSub) !== null && _a !== void 0 ? _a : theme.rest)
152
+ : theme.rest;
153
+ const c = rest.map((v, i) => Math.round(v + (theme.active[i] - v) * on));
154
+ d.classList.remove('on');
155
+ d.style.color = `rgb(${c.join(',')})`;
156
+ });
157
+ };
158
+ return { $, root, stage, rel, ring, nav };
159
+ }
160
+ /* The phone layout of a demo: its `mobile` block overrides the stage size and adds CSS. */
161
+ export function variant(def, phone) {
162
+ if (!phone || !def.mobile)
163
+ return def;
164
+ return Object.assign(Object.assign(Object.assign({}, def), def.mobile), { css: (def.css || '') + (def.mobile.css || '') });
165
+ }
166
+ /* Phones: screens narrower than Tailwind's sm breakpoint get a demo's phone layout. */
167
+ const PHONE = '(max-width: 639px)';
168
+ /*
169
+ * Mount a demo into `host` (a shadow root keeps its CSS off the page). With
170
+ * autoplay it scales to the host's width, uses the phone layout on narrow
171
+ * screens, loops while on screen, and holds the poster frame for reduced
172
+ * motion; without, it sits at native size for the renderer (`phone` picks the
173
+ * phone layout there).
174
+ */
175
+ export function mount(host, def, { autoplay = true, phone = false, css = '', theme } = {}) {
176
+ var _a;
177
+ const root = host.shadowRoot || host.attachShadow({ mode: 'open' });
178
+ let scale = 1;
179
+ let v = def;
180
+ let stage;
181
+ let seek = () => { };
182
+ const build = (isPhone) => {
183
+ v = variant(def, isPhone);
184
+ root.innerHTML = `<style>${STAGE_CSS}${css}${v.css || ''}</style>
185
+ <div class="stage" style="width:${v.width}px;height:${v.height}px">${v.html}</div>`;
186
+ stage = root.querySelector('.stage');
187
+ const ctx = makeCtx(root, stage, () => scale, theme);
188
+ // each window's pill starts under the item its chrome marked active, so a
189
+ // demo that never calls nav() does not show it on the first item
190
+ if (theme)
191
+ root.querySelectorAll(theme.side).forEach((side) => {
192
+ const on = side.querySelector('.nv.on');
193
+ if ((on === null || on === void 0 ? void 0 : on.dataset.k) && side.parentNode)
194
+ ctx.nav(on.dataset.k, on.dataset.k, 1, side.parentNode);
195
+ });
196
+ const pose = v.setup(ctx);
197
+ // [data-loop] content fades in at the start of the loop and out at its end,
198
+ // so the wrap back to the first frame is a dissolve, not a jump.
199
+ const looped = [...root.querySelectorAll('[data-loop]')];
200
+ const total = v.total;
201
+ seek = (t) => {
202
+ pose(t);
203
+ const f = Math.min(seg(t, 0, 0.35), 1 - seg(t, total - 0.35, total));
204
+ looped.forEach((el) => (el.style.opacity = String(f)));
205
+ };
206
+ };
207
+ const poster = (_a = def.poster) !== null && _a !== void 0 ? _a : 0;
208
+ if (!autoplay) {
209
+ build(phone);
210
+ seek(poster);
211
+ return { seek: (t) => seek(t), destroy() { } };
212
+ }
213
+ const phoneQuery = matchMedia(PHONE);
214
+ const fit = () => {
215
+ scale = host.clientWidth / v.width || 1;
216
+ stage.style.transform = `scale(${scale})`;
217
+ };
218
+ build(phoneQuery.matches);
219
+ fit();
220
+ const ro = new ResizeObserver(fit);
221
+ ro.observe(host);
222
+ const reduce = matchMedia('(prefers-reduced-motion: reduce)');
223
+ let t = poster;
224
+ let started = false;
225
+ let last = null;
226
+ let raf = 0;
227
+ let visible = false;
228
+ seek(t);
229
+ const loop = (now) => {
230
+ if (last != null)
231
+ t = (t + Math.min(0.1, (now - last) / 1000)) % def.total;
232
+ last = now;
233
+ seek(t);
234
+ raf = requestAnimationFrame(loop);
235
+ };
236
+ const update = () => {
237
+ cancelAnimationFrame(raf);
238
+ last = null;
239
+ if (reduce.matches) {
240
+ t = poster;
241
+ seek(t);
242
+ return;
243
+ }
244
+ if (visible && !document.hidden) {
245
+ if (!started) {
246
+ started = true;
247
+ t = 0;
248
+ }
249
+ raf = requestAnimationFrame(loop);
250
+ }
251
+ else
252
+ seek(t);
253
+ };
254
+ const relayout = () => {
255
+ build(phoneQuery.matches);
256
+ fit();
257
+ update();
258
+ };
259
+ const io = new IntersectionObserver(([e]) => {
260
+ visible = e.isIntersecting;
261
+ update();
262
+ }, { threshold: 0.2 });
263
+ io.observe(host);
264
+ document.addEventListener('visibilitychange', update);
265
+ reduce.addEventListener('change', update);
266
+ phoneQuery.addEventListener('change', relayout);
267
+ return {
268
+ seek: (x) => seek(x),
269
+ destroy() {
270
+ cancelAnimationFrame(raf);
271
+ io.disconnect();
272
+ ro.disconnect();
273
+ document.removeEventListener('visibilitychange', update);
274
+ reduce.removeEventListener('change', update);
275
+ phoneQuery.removeEventListener('change', relayout);
276
+ },
277
+ };
278
+ }
package/hooks/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { useApiError } from './use-api-error';
2
2
  export { useCrossAppLink, type CrossAppTarget, type OpenCrossAppOptions, type UseCrossAppLinkResult, } from './use-cross-app-link';
3
+ export { useGraphWrites } from './use-graph-writes';
3
4
  export { useMediaQuery } from './use-media-query';
4
5
  export { useStreamingQuery } from './use-streaming-query';
5
6
  export { useToast, type ToastMessage } from './use-toast';
package/hooks/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { useApiError } from './use-api-error';
2
2
  export { useCrossAppLink, } from './use-cross-app-link';
3
+ export { useGraphWrites } from './use-graph-writes';
3
4
  export { useMediaQuery } from './use-media-query';
4
5
  export { useStreamingQuery } from './use-streaming-query';
5
6
  export { useToast } from './use-toast';
@@ -0,0 +1,6 @@
1
+ import { type GraphWrite } from '../lib/graph-writes';
2
+ /**
3
+ * Calls `onWrites` when the console changes the graph a page is showing, so
4
+ * the page can reload what it displays. Writes to other graphs are ignored.
5
+ */
6
+ export declare function useGraphWrites(graphId: string | null | undefined, onWrites: (writes: GraphWrite[]) => void): void;
@@ -0,0 +1,22 @@
1
+ 'use client';
2
+ import { useEffect, useRef } from 'react';
3
+ import { GRAPH_WRITES_EVENT, } from '../lib/graph-writes';
4
+ /**
5
+ * Calls `onWrites` when the console changes the graph a page is showing, so
6
+ * the page can reload what it displays. Writes to other graphs are ignored.
7
+ */
8
+ export function useGraphWrites(graphId, onWrites) {
9
+ const onWritesRef = useRef(onWrites);
10
+ onWritesRef.current = onWrites;
11
+ useEffect(() => {
12
+ if (!graphId)
13
+ return;
14
+ const handler = (event) => {
15
+ const detail = event.detail;
16
+ if ((detail === null || detail === void 0 ? void 0 : detail.graphId) === graphId)
17
+ onWritesRef.current(detail.writes);
18
+ };
19
+ window.addEventListener(GRAPH_WRITES_EVENT, handler);
20
+ return () => window.removeEventListener(GRAPH_WRITES_EVENT, handler);
21
+ }, [graphId]);
22
+ }
package/index.d.ts CHANGED
@@ -2,9 +2,9 @@ export * as AuthCore from './auth-core';
2
2
  export { clearToken, getAuthHeader, getRefreshToken, getToken, getValidToken, handleAuthResponse, hasValidToken, storeToken, } from './auth-core/token-storage';
3
3
  export { clearUserCookies, clearUserLocalStorage, clearUserSessionStorage, listUserData, performLogoutCleanup, resetUIState, } from './auth-core/cleanup';
4
4
  export { AppSwitcher, AuthGuard, AuthProvider, LoginRedirector, SignInForm, SignUpForm, useAuth, } from './auth-components';
5
- export { useApiError, useMediaQuery, useToast, useUser, useUserLimits, type ToastMessage, } from './hooks';
5
+ export { useApiError, useGraphWrites, useMediaQuery, useToast, useUser, useUserLimits, type ToastMessage, } from './hooks';
6
6
  export { createGraphContext, createGraphProvider, createUseGraphContext, EntityProvider, GraphContext, GraphProvider, OrgProvider, ServiceOfferingsProvider, SidebarProvider, useEntity, useGraphContext, useOrg, useServiceOfferings, useSidebarContext, type EntityContextValue, type EntityProviderProps, type GraphContextValue, type GraphProviderProps, type GraphState, type OrgContextState, type OrgContextValue, type OrgProviderProps, type ServiceOfferings, } from './contexts';
7
- export { clientGraphCookie, clientSidebarCookie, createMcpConnectorUrl, entityCookie, graphCookie, sidebarCookie, type EntityCookie, type GraphCookie, type McpConnectorUrl, type SidebarCookie, } from './lib';
7
+ export { clientGraphCookie, clientSidebarCookie, createMcpConnectorUrl, emitGraphWrites, entityCookie, GRAPH_WRITES_EVENT, graphCookie, readGraphWrites, sidebarCookie, type EntityCookie, type GraphCookie, type GraphWrite, type GraphWritesDetail, type McpConnectorUrl, type SidebarCookie, } from './lib';
8
8
  export { ApiError, errorStatus, extractErrorDetail, isApiError, isSessionRejection, isTransientError, toApiError, unwrapSdk, type SdkResult, } from './lib/sdk-errors';
9
9
  export { generateEntityUri, isUUID, UUID_REGEX } from './utils';
10
10
  export { clearGraphSelection, getGraphSelection, persistGraphSelection, } from './actions/graph-actions';
package/index.js CHANGED
@@ -8,7 +8,7 @@ export { clearUserCookies, clearUserLocalStorage, clearUserSessionStorage, listU
8
8
  // Export auth-components
9
9
  export { AppSwitcher, AuthGuard, AuthProvider, LoginRedirector, SignInForm, SignUpForm, useAuth, } from './auth-components';
10
10
  // Export hooks
11
- export { useApiError, useMediaQuery, useToast, useUser, useUserLimits, } from './hooks';
11
+ export { useApiError, useGraphWrites, useMediaQuery, useToast, useUser, useUserLimits, } from './hooks';
12
12
  // Export contexts
13
13
  export {
14
14
  // Graph context exports
@@ -20,7 +20,7 @@ OrgProvider,
20
20
  // Service offerings context
21
21
  ServiceOfferingsProvider, SidebarProvider, useEntity, useGraphContext, useOrg, useServiceOfferings, useSidebarContext, } from './contexts';
22
22
  // Export lib utilities
23
- export { clientGraphCookie, clientSidebarCookie, createMcpConnectorUrl, entityCookie, graphCookie, sidebarCookie, } from './lib';
23
+ export { clientGraphCookie, clientSidebarCookie, createMcpConnectorUrl, emitGraphWrites, entityCookie, GRAPH_WRITES_EVENT, graphCookie, readGraphWrites, sidebarCookie, } from './lib';
24
24
  // Export the SDK error seam (unwrap an SDK result or throw ApiError)
25
25
  export { ApiError, errorStatus, extractErrorDetail, isApiError, isSessionRejection, isTransientError, toApiError, unwrapSdk, } from './lib/sdk-errors';
26
26
  // Export utils
@@ -0,0 +1,20 @@
1
+ /**
2
+ * A write an operator made, as the API reports it in `metadata.writes`.
3
+ * `id` is null when the tool returned no object id (for example a memory).
4
+ */
5
+ export interface GraphWrite {
6
+ operation: string;
7
+ id: string | null;
8
+ name: string | null;
9
+ block_type?: string;
10
+ }
11
+ export interface GraphWritesDetail {
12
+ graphId: string;
13
+ writes: GraphWrite[];
14
+ }
15
+ /** Window event fired after the console changes a graph. */
16
+ export declare const GRAPH_WRITES_EVENT = "robosystems:graph-writes";
17
+ /** Tell any page showing `graphId` that it changed. No-op when nothing was written. */
18
+ export declare function emitGraphWrites(graphId: string, writes: GraphWrite[]): void;
19
+ /** Read `metadata.writes` defensively: anything malformed yields no writes. */
20
+ export declare function readGraphWrites(metadata: unknown): GraphWrite[];
@@ -0,0 +1,19 @@
1
+ /** Window event fired after the console changes a graph. */
2
+ export const GRAPH_WRITES_EVENT = 'robosystems:graph-writes';
3
+ /** Tell any page showing `graphId` that it changed. No-op when nothing was written. */
4
+ export function emitGraphWrites(graphId, writes) {
5
+ if (typeof window === 'undefined' || writes.length === 0)
6
+ return;
7
+ window.dispatchEvent(new CustomEvent(GRAPH_WRITES_EVENT, {
8
+ detail: { graphId, writes },
9
+ }));
10
+ }
11
+ /** Read `metadata.writes` defensively: anything malformed yields no writes. */
12
+ export function readGraphWrites(metadata) {
13
+ const writes = metadata === null || metadata === void 0 ? void 0 : metadata.writes;
14
+ if (!Array.isArray(writes))
15
+ return [];
16
+ return writes.filter((w) => typeof w === 'object' &&
17
+ w !== null &&
18
+ typeof w.operation === 'string');
19
+ }
package/lib/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { clientSidebarCookie, sidebarCookie, type SidebarCookie, } from './sidebar-cookie';
2
2
  export { clientGraphCookie, graphCookie, type GraphCookie, } from './graph-cookie';
3
3
  export { entityCookie, type EntityCookie } from './entity-cookie';
4
+ export { GRAPH_WRITES_EVENT, emitGraphWrites, readGraphWrites, type GraphWrite, type GraphWritesDetail, } from './graph-writes';
4
5
  export { createMcpConnectorUrl, type McpConnectorUrl } from './mcp-connector';
5
6
  export { ApiError, errorStatus, extractErrorDetail, isApiError, isSessionRejection, isTransientError, toApiError, unwrapSdk, type SdkResult, } from './sdk-errors';
package/lib/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  export { clientSidebarCookie, sidebarCookie, } from './sidebar-cookie';
2
2
  export { clientGraphCookie, graphCookie, } from './graph-cookie';
3
3
  export { entityCookie } from './entity-cookie';
4
+ export { GRAPH_WRITES_EVENT, emitGraphWrites, readGraphWrites, } from './graph-writes';
4
5
  export { createMcpConnectorUrl } from './mcp-connector';
5
6
  export { ApiError, errorStatus, extractErrorDetail, isApiError, isSessionRejection, isTransientError, toApiError, unwrapSdk, } from './sdk-errors';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robosystems/core",
3
- "version": "0.11.5",
3
+ "version": "0.13.0",
4
4
  "description": "Shared RoboSystems frontend core library (auth, contexts, task monitoring, UI components) for the RoboSystems Next.js apps",
5
5
  "license": "MIT",
6
6
  "private": false,
@@ -0,0 +1,22 @@
1
+ export interface LiveDemoProps {
2
+ /** Demo module name under /public/demos (e.g. "hero", "inbox"). */
3
+ name: string;
4
+ /** Stage aspect ratio as width / height, reserved before the demo loads. */
5
+ aspect: number;
6
+ /**
7
+ * The phone layout's aspect ratio (the demo module's `mobile` width / height),
8
+ * used below the sm breakpoint, where kit.js mounts that layout.
9
+ */
10
+ phoneAspect?: number;
11
+ /** What the animation shows, for screen readers. */
12
+ label: string;
13
+ className?: string;
14
+ }
15
+ /**
16
+ * An animated product demo from the app's /public/demos, mounted in a shadow root so its
17
+ * styles stay off the page. The demos are plain ES modules shared with the
18
+ * content machine's renderer, which shoots the same files for the social cuts;
19
+ * see demos/runtime.ts in this package. They loop only while on screen and hold one frame
20
+ * for reduced motion.
21
+ */
22
+ export declare function LiveDemo({ name, aspect, label, phoneAspect, className, }: LiveDemoProps): import("react").JSX.Element;
@@ -0,0 +1,44 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useEffect, useRef } from 'react';
4
+ /**
5
+ * An animated product demo from the app's /public/demos, mounted in a shadow root so its
6
+ * styles stay off the page. The demos are plain ES modules shared with the
7
+ * content machine's renderer, which shoots the same files for the social cuts;
8
+ * see demos/runtime.ts in this package. They loop only while on screen and hold one frame
9
+ * for reduced motion.
10
+ */
11
+ export function LiveDemo({ name, aspect, label, phoneAspect, className = '', }) {
12
+ const host = useRef(null);
13
+ useEffect(() => {
14
+ let handle;
15
+ let cancelled = false;
16
+ const load = async () => {
17
+ // Served from /public at runtime, never bundled.
18
+ const base = '/demos/';
19
+ const [kit, demo] = await Promise.all([
20
+ import(
21
+ /* webpackIgnore: true */ /* turbopackIgnore: true */ base + 'kit.js'),
22
+ import(
23
+ /* webpackIgnore: true */ /* turbopackIgnore: true */ base +
24
+ name +
25
+ '.js'),
26
+ ]);
27
+ if (cancelled || !host.current)
28
+ return;
29
+ handle = kit.mount(host.current, demo.default);
30
+ };
31
+ load().catch((err) => {
32
+ // The page stands without the demo, but a broken module should still show up.
33
+ console.error(`LiveDemo "${name}" failed to load`, err);
34
+ });
35
+ return () => {
36
+ cancelled = true;
37
+ handle === null || handle === void 0 ? void 0 : handle.destroy();
38
+ };
39
+ }, [name]);
40
+ return (_jsx("div", { ref: host, role: "img", "aria-label": label, className: `relative aspect-(--aspect) w-full overflow-hidden max-sm:aspect-(--phone-aspect) ${className}`, style: {
41
+ '--aspect': aspect,
42
+ '--phone-aspect': phoneAspect !== null && phoneAspect !== void 0 ? phoneAspect : aspect,
43
+ } }));
44
+ }
@@ -2,6 +2,7 @@ export { ACCOUNT_SETTINGS_PATH, useAccountSettingsLink, type AccountSettingsLink
2
2
  export { CoreNavbar } from './CoreNavbar';
3
3
  export { CoreSidebar, type SidebarItemData } from './CoreSidebar';
4
4
  export { LandingFooter, type FooterLink, type LandingFooterProps, } from './LandingFooter';
5
+ export { LiveDemo, type LiveDemoProps } from './LiveDemo';
5
6
  export { PageContainer } from './PageContainer';
6
7
  export { PageHeader, type PageHeaderProps } from './PageHeader';
7
8
  export { SettingsContainer } from './SettingsContainer';
@@ -2,6 +2,7 @@ export { ACCOUNT_SETTINGS_PATH, useAccountSettingsLink, } from './account-settin
2
2
  export { CoreNavbar } from './CoreNavbar';
3
3
  export { CoreSidebar } from './CoreSidebar';
4
4
  export { LandingFooter, } from './LandingFooter';
5
+ export { LiveDemo } from './LiveDemo';
5
6
  export { PageContainer } from './PageContainer';
6
7
  export { PageHeader } from './PageHeader';
7
8
  export { SettingsContainer } from './SettingsContainer';