@rho-workspace/rho 0.5.6 → 0.5.7
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/dist/agent-connect.d.ts +36 -2
- package/dist/agent-connect.js +64 -11
- package/dist/answer-board.js +3 -3
- package/dist/bridge.js +6 -1
- package/dist/browser-access.d.ts +1 -0
- package/dist/browser-access.js +5 -3
- package/dist/capabilities.d.ts +21 -5
- package/dist/capabilities.js +6 -4
- package/dist/cli.js +76 -11
- package/dist/composition.d.ts +1 -1
- package/dist/composition.js +13 -3
- package/dist/connect-cli.d.ts +50 -0
- package/dist/connect-cli.js +128 -0
- package/dist/contract.generated.d.ts +1191 -278
- package/dist/contract.generated.js +1 -1
- package/dist/contracts/owners.generated.js +845 -47
- package/dist/conversation-presentation.d.ts +9 -2
- package/dist/conversation-presentation.js +3 -1
- package/dist/conversations.d.ts +154 -8
- package/dist/conversations.js +147 -24
- package/dist/discovery.js +2 -2
- package/dist/extension-contract.d.ts +19 -0
- package/dist/extension-contract.js +15 -6
- package/dist/extension-markets.d.ts +74 -0
- package/dist/extension-markets.js +166 -0
- package/dist/extensions.d.ts +29 -1
- package/dist/extensions.js +13 -4
- package/dist/external-work.d.ts +22 -0
- package/dist/external-work.js +91 -0
- package/dist/plugin-cli.js +75 -3
- package/dist/plugin-inventory.d.ts +56 -0
- package/dist/plugin-inventory.js +41 -0
- package/dist/plugin-inventory.json +1581 -102
- package/dist/plugin-presentation.d.ts +105 -2
- package/dist/plugin-presentation.js +59 -5
- package/dist/plugins.d.ts +14 -3
- package/dist/plugins.js +12 -6
- package/dist/project-notes.js +1 -1
- package/dist/protocol.d.ts +6 -0
- package/dist/protocol.js +30 -12
- package/dist/replay.js +4 -0
- package/dist/response-contracts.js +26 -11
- package/dist/running.d.ts +10 -0
- package/dist/running.js +45 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +71 -8
- package/dist/state.d.ts +8 -0
- package/dist/state.js +8 -0
- package/dist/studio-layout.js +3 -3
- package/dist/task-frame.d.ts +1 -1
- package/dist/timeline-events.d.ts +1 -0
- package/dist/timeline-events.js +8 -1
- package/dist/ui-context.d.ts +73 -0
- package/dist/ui-context.js +98 -0
- package/dist/ui-contract.d.ts +23 -0
- package/dist/web/.vite/manifest.json +133 -74
- package/dist/web/assets/answer-board-CeYS8k8y.js +1 -0
- package/dist/web/assets/choices-D8vGwsl2.js +1 -0
- package/dist/web/assets/{console-Trqc0swD.js → console-Dyuo1q4N.js} +4 -4
- package/dist/web/assets/{dist-pF902jHA.js → dist-CSiCyBml.js} +1 -1
- package/dist/web/assets/{dist-DpIHRk3B.js → dist-D9FLb9Hm.js} +1 -1
- package/dist/web/assets/{dist-eMx6b8N1.js → dist-FXbTBwML.js} +1 -1
- package/dist/web/assets/{dist-DZmszrjG.js → dist-l3DUhbw5.js} +1 -1
- package/dist/web/assets/{explore-in-BCHqjJ7B.js → explore-in-BfSFGRh2.js} +1 -1
- package/dist/web/assets/graph-BU2_o5uA.js +2 -0
- package/dist/web/assets/index-C16mXVvk.css +1 -0
- package/dist/web/assets/index-CWg9XYXx.js +952 -0
- package/dist/web/assets/markdown.reader-Ct2-e2Zt.png +0 -0
- package/dist/web/assets/{notes-meaning-DcVGUBBY.js → notes-meaning-T2m5zQfv.js} +1 -1
- package/dist/web/assets/overview-PzfLJCo-.js +6 -0
- package/dist/web/assets/preview-BguEdsza.js +1 -0
- package/dist/web/assets/previews-pWNik-2g.js +3 -0
- package/dist/web/assets/{review-DYP0aCXb.js → review-Dy_85lcH.js} +1 -1
- package/dist/web/assets/rho.editor-CCJeyHma.png +0 -0
- package/dist/web/assets/rho.flow-WkMj6fRS.png +0 -0
- package/dist/web/assets/rho.media-Cc6amodO.png +0 -0
- package/dist/web/assets/rho.notebook-6vsZKGDq.png +0 -0
- package/dist/web/assets/rho.object-BGoeXjte.png +0 -0
- package/dist/web/assets/rho.plots-yIiISCkb.png +0 -0
- package/dist/web/assets/rho.scatter-DwHxesCs.png +0 -0
- package/dist/web/assets/rho.servers-DKf_mnNq.png +0 -0
- package/dist/web/assets/rho.table-DWOGaWDa.png +0 -0
- package/dist/web/assets/rho.variables-CKcWczhQ.png +0 -0
- package/dist/web/assets/{skills-1ec0Z-jx.js → skills-Dyz7mBlc.js} +1 -1
- package/dist/web/assets/{skills-work-DNexV4OJ.js → skills-work-BEGqlCkx.js} +1 -1
- package/dist/web/assets/studio-process-D-ykbC4D.js +2 -0
- package/dist/web/assets/timeline-events-DKTho9fU.js +1 -0
- package/dist/web/assets/work-turns-DSabCogR.js +1 -0
- package/dist/web/assets/{workspace-COmCbGV3.js → workspace-DfjBL--r.js} +2 -2
- package/dist/web/composition.json +138 -83
- package/dist/web/index.html +3 -3
- package/dist/web/renderer.json +1 -1
- package/dist/web-assets.js +2 -2
- package/dist/work-graph.d.ts +11 -1
- package/dist/work-graph.js +16 -5
- package/dist/workspace.d.ts +2 -2
- package/dist/workspace.js +8 -7
- package/docs/FRONTEND.md +53 -11
- package/docs/HEADLESS.md +104 -11
- package/docs/THIRD-PARTY-PLUGINS.md +75 -3
- package/node_modules/@rho-workspace/plugin-agent/README.md +5 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.d.ts +31 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-command.js +45 -10
- package/node_modules/@rho-workspace/plugin-agent/dist/acp-conversation.js +19 -3
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session-contract.js +3 -2
- package/node_modules/@rho-workspace/plugin-agent/dist/external-session.d.ts +1 -1
- package/node_modules/@rho-workspace/plugin-agent/dist/index.d.ts +8 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/index.js +3 -2
- package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.d.ts +13 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/mcp-entry.js +13 -0
- package/node_modules/@rho-workspace/plugin-agent/dist/mcp.js +16 -8
- package/node_modules/@rho-workspace/plugin-agent/dist/native-sessions.d.ts +1 -1
- package/node_modules/@rho-workspace/plugin-agent/package.json +1 -1
- package/node_modules/@rho-workspace/plugin-figures/LICENSE +661 -0
- package/node_modules/@rho-workspace/plugin-figures/README.md +191 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/index.d.ts +236 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/index.js +165 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/operations.d.ts +44 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/operations.js +39 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/presentation.d.ts +19 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/presentation.js +26 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/record.d.ts +188 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/record.js +143 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui-styles.js +68 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui.d.ts +56 -0
- package/node_modules/@rho-workspace/plugin-figures/dist/ui.js +363 -0
- package/node_modules/@rho-workspace/plugin-figures/package.json +41 -0
- package/node_modules/@rho-workspace/plugin-figures/py/figures.py +856 -0
- package/node_modules/@rho-workspace/plugin-figures/r/figures.R +775 -0
- package/node_modules/@rho-workspace/plugin-kernel/README.md +27 -3
- package/node_modules/@rho-workspace/plugin-kernel/dist/contract.d.ts +12 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/contract.js +8 -1
- package/node_modules/@rho-workspace/plugin-kernel/dist/index.d.ts +61 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/index.js +125 -9
- package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.d.ts +14 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/session-scope.js +9 -0
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-base.d.ts +23 -5
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook-work.js +1 -1
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.d.ts +6 -4
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-notebook.js +44 -17
- package/node_modules/@rho-workspace/plugin-kernel/dist/ui-styles.js +6 -0
- package/node_modules/@rho-workspace/plugin-kernel/package.json +1 -1
- package/node_modules/@rho-workspace/plugin-kernel/runners/runner.R +144 -4
- package/node_modules/@rho-workspace/plugin-kernel/runners/runner.py +106 -1
- package/node_modules/@rho-workspace/plugin-tables/README.md +5 -0
- package/node_modules/@rho-workspace/plugin-tables/dist/index.d.ts +1 -0
- package/node_modules/@rho-workspace/plugin-tables/dist/index.js +19 -3
- package/node_modules/@rho-workspace/plugin-tables/dist/ui.js +1 -1
- package/node_modules/@rho-workspace/plugin-tables/package.json +1 -1
- package/node_modules/jose/dist/webapi/jwe/general/decrypt.js +3 -2
- package/node_modules/jose/dist/webapi/jwks/remote.js +1 -1
- package/node_modules/jose/dist/webapi/jws/general/verify.js +2 -8
- package/node_modules/jose/dist/webapi/lib/jwe_decrypt.js +11 -8
- package/node_modules/jose/dist/webapi/lib/key_management.js +0 -1
- package/node_modules/jose/package.json +1 -1
- package/package.json +6 -4
- package/protocol/index.json +91 -55
- package/protocol/operations/agent.sessions.list.json +13 -5
- package/protocol/operations/agent.sessions.read.json +13 -5
- package/protocol/operations/capability.list.json +382 -6
- package/protocol/operations/composition.apply.json +10 -2
- package/protocol/operations/composition.delete.json +845 -0
- package/protocol/operations/composition.read.json +10 -2
- package/protocol/operations/composition.undo.json +10 -2
- package/protocol/operations/conversation.agents.add.json +346 -0
- package/protocol/operations/conversation.agents.remove.json +340 -0
- package/protocol/operations/conversation.decide.json +196 -0
- package/protocol/operations/conversation.permission.json +196 -0
- package/protocol/operations/conversation.read.json +196 -0
- package/protocol/operations/conversation.send.json +265 -0
- package/protocol/operations/conversation.settings.check.json +213 -495
- package/protocol/operations/conversation.settings.read.json +211 -490
- package/protocol/operations/conversation.settings.update.json +235 -554
- package/protocol/operations/conversation.start.json +267 -5
- package/protocol/operations/conversation.stop.json +196 -0
- package/protocol/operations/extension.describe.json +39 -0
- package/protocol/operations/extension.install.json +62 -0
- package/protocol/operations/extension.list.json +164 -1
- package/protocol/operations/extension.market.change.json +210 -0
- package/protocol/operations/figure.read.json +265 -0
- package/protocol/operations/figure.select.json +157 -0
- package/protocol/operations/kernel.interrupt.json +119 -0
- package/protocol/operations/kernel.read.json +119 -0
- package/protocol/operations/kernel.run.json +119 -0
- package/protocol/operations/observation.snapshot.json +416 -8
- package/protocol/operations/project.openFile.json +10 -2
- package/protocol/operations/session.grant.json +4 -0
- package/protocol/operations/shared.json +1 -0
- package/protocol/operations/view.close.json +10 -2
- package/protocol/operations/view.protection.set.json +10 -2
- package/vendor/composition.json +177 -112
- package/vendor/rho-workspace-plugin-agent-0.4.33.tgz +0 -0
- package/vendor/rho-workspace-plugin-figures-0.5.1.tgz +0 -0
- package/vendor/rho-workspace-plugin-kernel-0.5.13.tgz +0 -0
- package/vendor/rho-workspace-plugin-tables-0.5.2.tgz +0 -0
- package/web/theme.css +5 -0
- package/dist/web/assets/answer-board-CBkGbSLD.js +0 -1
- package/dist/web/assets/canvas-D8rDdfDz.js +0 -1
- package/dist/web/assets/choices-D-YfnFP6.js +0 -1
- package/dist/web/assets/graph-lENgfQNH.js +0 -2
- package/dist/web/assets/index-B3_OVy91.css +0 -1
- package/dist/web/assets/index-BpL_Kl_s.js +0 -892
- package/dist/web/assets/overview-CDnVKlOt.js +0 -6
- package/dist/web/assets/preview-BEC1E9MK.js +0 -1
- package/dist/web/assets/studio-process-EYQgikxq.js +0 -2
- package/dist/web/assets/timeline-events-Chj2Gbx7.js +0 -1
- package/dist/web/assets/work-turns-CT9RVcWY.js +0 -1
- package/vendor/rho-workspace-plugin-agent-0.4.30.tgz +0 -0
- package/vendor/rho-workspace-plugin-kernel-0.5.5.tgz +0 -0
- package/vendor/rho-workspace-plugin-tables-0.5.0.tgz +0 -0
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
// The browser entry: a plot whose marks are bound to their rows. Pointing at a mark reads its
|
|
3
|
+
// row; a click selects it (Shift or ⌘/Ctrl adds), a drag selects the marks it crosses, Esc clears.
|
|
4
|
+
// The selected rows show below the plot, and Ask carries them to the Agent. Marks an Agent pointed
|
|
5
|
+
// to show with its note. The page that shows a figure supplies the client and, optionally, how to
|
|
6
|
+
// name a principal and what Ask does.
|
|
7
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
8
|
+
import { elementMarks, markName, markRow, parseMark, parseRecord } from './record.js';
|
|
9
|
+
import { ensureFigureStyles } from './ui-styles.js';
|
|
10
|
+
export const figureViewLimits = { tableRows: 50, tableColumns: 8, tipColumns: 6, brushPx: 4, cachedRecords: 12 };
|
|
11
|
+
const records = new Map();
|
|
12
|
+
const refOf = (cellId, plot, file) => file ? { path: file.path, sha256: file.sha256 } : { cellId, plot };
|
|
13
|
+
const refKey = (ref) => ref.sha256 ? 'file:' + ref.sha256 : ref.cellId + ':' + ref.plot;
|
|
14
|
+
function readView(client, projectId, ref) {
|
|
15
|
+
const key = projectId + ':' + refKey(ref);
|
|
16
|
+
let pending = records.get(key);
|
|
17
|
+
if (!pending) {
|
|
18
|
+
pending = client.call('figure.read', { projectId, ...ref, form: 'view' }).then(v => ({ record: parseRecord(JSON.stringify(v.record)), selections: v.selections ?? [] }));
|
|
19
|
+
records.set(key, pending);
|
|
20
|
+
pending.catch(() => records.delete(key));
|
|
21
|
+
while (records.size > figureViewLimits.cachedRecords)
|
|
22
|
+
records.delete(records.keys().next().value);
|
|
23
|
+
}
|
|
24
|
+
return pending;
|
|
25
|
+
}
|
|
26
|
+
// The R device's SVG, as elements this page can hold: no scripts, handlers or outside links.
|
|
27
|
+
function svgElement(text) {
|
|
28
|
+
const svgNs = 'http://www.w3.org/2000/svg';
|
|
29
|
+
const head = text.slice(0, text.indexOf('>') + 1);
|
|
30
|
+
const doc = new DOMParser().parseFromString(/xmlns=/.test(head) ? text : text.replace(/<svg\b/, `<svg xmlns="${svgNs}"`), 'image/svg+xml');
|
|
31
|
+
const svg = doc.documentElement;
|
|
32
|
+
if (svg.localName !== 'svg' || svg.namespaceURI !== svgNs || doc.querySelector('parsererror'))
|
|
33
|
+
throw new Error('The figure\'s picture could not be read.');
|
|
34
|
+
doc.querySelectorAll('script, foreignObject').forEach(e => e.remove());
|
|
35
|
+
doc.querySelectorAll('*').forEach(e => {
|
|
36
|
+
for (const a of [...e.attributes]) {
|
|
37
|
+
if (/^on/i.test(a.name))
|
|
38
|
+
e.removeAttribute(a.name);
|
|
39
|
+
if (/href$/i.test(a.name) && !/^(#|data:image\/)/.test(a.value))
|
|
40
|
+
e.removeAttribute(a.name);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
svg.removeAttribute('width');
|
|
44
|
+
svg.removeAttribute('height');
|
|
45
|
+
svg.setAttribute('role', 'img');
|
|
46
|
+
return document.importNode(svg, true);
|
|
47
|
+
}
|
|
48
|
+
const fmt = (v) => {
|
|
49
|
+
if (v === null || v === undefined)
|
|
50
|
+
return 'NA';
|
|
51
|
+
if (typeof v === 'number') {
|
|
52
|
+
const a = Math.abs(v);
|
|
53
|
+
return a !== 0 && (a >= 1e5 || a < 1e-3) ? v.toExponential(2) : Number.isInteger(v) ? String(v) : String(Number(v.toPrecision(4)));
|
|
54
|
+
}
|
|
55
|
+
return String(v);
|
|
56
|
+
};
|
|
57
|
+
// The source columns a layer's mapping names, in mapping order: what a reader looks for first.
|
|
58
|
+
function mappedColumns(record, layer, columns) {
|
|
59
|
+
const mapping = Object.values(record.figure.layers[layer]?.mapping ?? {});
|
|
60
|
+
return columns.filter(c => mapping.some(expr => new RegExp(`(^|[^A-Za-z0-9_.])${c.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}($|[^A-Za-z0-9_.])`).test(expr)));
|
|
61
|
+
}
|
|
62
|
+
function rowFields(record, row, max) {
|
|
63
|
+
if (row.values) {
|
|
64
|
+
// Columns a package adds for its own drawing (.ggtangle_id) are left to the Agent. A value the
|
|
65
|
+
// layer maps that its row does not hold (a heatmap cell's column and value) follows the key.
|
|
66
|
+
const names = Object.keys(row.values).filter(n => n === '.rownames' || !n.startsWith('.')), first = mappedColumns(record, row.layer, names);
|
|
67
|
+
const own = Object.values(record.figure.layers[row.layer]?.mapping ?? {}).filter(n => n in row.built && !(n in row.values) && row.built[n] !== row.key?.value);
|
|
68
|
+
// A row's other names (a gene symbol beside its Ensembl key) follow the mapped columns, before the
|
|
69
|
+
// rest of its numbers.
|
|
70
|
+
const textual = new Set(record.sources.find(s => s.id === row.source)?.columns.filter(c => c.type === 'text').map(c => c.name) ?? []);
|
|
71
|
+
const order = [...(row.key ? [row.key.column] : []), ...first, ...names.filter(n => textual.has(n)), ...names].filter((n, i, a) => a.indexOf(n) === i);
|
|
72
|
+
const held = (n) => [n === '.rownames' ? 'row' : n, row.values[n] ?? null];
|
|
73
|
+
return [...order.slice(0, row.key ? 1 : 0).map(held), ...own.map((n) => [n, row.built[n] ?? null]), ...order.slice(row.key ? 1 : 0).map(held)].slice(0, max);
|
|
74
|
+
}
|
|
75
|
+
const preferred = ['label', 'x', 'y', 'count', 'density', 'ymin', 'lower', 'middle', 'upper', 'ymax', 'xmin', 'xmax', 'fill', 'colour'];
|
|
76
|
+
const names = Object.keys(row.built).sort((a, b) => (preferred.indexOf(a) + 1 || 99) - (preferred.indexOf(b) + 1 || 99));
|
|
77
|
+
return names.slice(0, max).map(n => [n, row.built[n] ?? null]);
|
|
78
|
+
}
|
|
79
|
+
// A mark is titled by its row's key (a gene, a barcode; a number key with its column's name), else
|
|
80
|
+
// by its label; a row with neither is told by its fields alone.
|
|
81
|
+
const title = (row) => row.key ? (typeof row.key.value === 'number' ? `${row.key.column} ${fmt(row.key.value)}` : fmt(row.key.value))
|
|
82
|
+
: row.built.label !== undefined && row.built.label !== null ? fmt(row.built.label) : null;
|
|
83
|
+
const tileGeom = /^(heatmap|tile|raster|image)$/i;
|
|
84
|
+
const plural = (n, word) => `${n} ${word}${n === 1 ? '' : /(s|x|sh|ch)$/.test(word) ? 'es' : 's'}`;
|
|
85
|
+
const markWord = (geom) => /text|label|repel/i.test(geom) ? 'label' : /point|circle/i.test(geom) ? 'point' : /heatmap|cell/i.test(geom) ? 'cell' : /bar|col|histogram|rect|tile/i.test(geom) ? 'bar' : /box/i.test(geom) ? 'box' : /line|path|smooth|step/i.test(geom) ? 'line' : /violin|density|area|ribbon|polygon/i.test(geom) ? 'shape' : 'mark';
|
|
86
|
+
function groupsOf(record, marks) {
|
|
87
|
+
const byLayer = new Map();
|
|
88
|
+
for (const m of marks) {
|
|
89
|
+
const r = markRow(record, parseMark(m));
|
|
90
|
+
if (r)
|
|
91
|
+
byLayer.set(r.layer, [...(byLayer.get(r.layer) ?? []), r]);
|
|
92
|
+
}
|
|
93
|
+
const groups = [];
|
|
94
|
+
for (const [layer, rows] of [...byLayer].sort((a, b) => b[1].length - a[1].length)) {
|
|
95
|
+
const keys = rows.every(r => r.key) ? rows.map(r => String(r.key.value)).sort().join('\u0000') : null;
|
|
96
|
+
const same = keys !== null && groups.find(g => g.keys === keys);
|
|
97
|
+
if (same)
|
|
98
|
+
same.also.push(layer);
|
|
99
|
+
else
|
|
100
|
+
groups.push({ layer, rows, also: [], keys });
|
|
101
|
+
}
|
|
102
|
+
return groups;
|
|
103
|
+
}
|
|
104
|
+
// Groups of different rows drawn the same way are counted together: "7 points", not "5 points, 2 points".
|
|
105
|
+
function groupsText(record, groups) {
|
|
106
|
+
const counts = new Map();
|
|
107
|
+
for (const g of groups) {
|
|
108
|
+
const word = markWord(record.figure.layers[g.layer].geom);
|
|
109
|
+
counts.set(word, (counts.get(word) ?? 0) + g.rows.length);
|
|
110
|
+
}
|
|
111
|
+
return [...counts].map(([word, n]) => plural(n, word)).join(', ');
|
|
112
|
+
}
|
|
113
|
+
export function SemanticFigure({ client, projectId, cellId, plot, file, width = file ? undefined : 1008, label, name, fit, fallback, cursor, author, onAsk }) {
|
|
114
|
+
ensureFigureStyles();
|
|
115
|
+
const ref = useMemo(() => refOf(cellId, plot, file), [cellId, plot, file?.path, file?.sha256]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
116
|
+
const key = refKey(ref);
|
|
117
|
+
const [loaded, setLoaded] = useState(null);
|
|
118
|
+
const [problem, setProblem] = useState('');
|
|
119
|
+
const [selections, setSelections] = useState([]);
|
|
120
|
+
const [selected, setSelected] = useState(new Set());
|
|
121
|
+
const [from, setFrom] = useState(null);
|
|
122
|
+
const [tip, setTip] = useState(null);
|
|
123
|
+
const [brush, setBrush] = useState(null);
|
|
124
|
+
const [asking, setAsking] = useState(false);
|
|
125
|
+
const [arrived, setArrived] = useState(false);
|
|
126
|
+
const canvas = useRef(null), host = useRef(null), waiting = useRef(null);
|
|
127
|
+
// A record is read once the figure comes near the screen: a long Notebook reads only those seen.
|
|
128
|
+
const [near, setNear] = useState(typeof IntersectionObserver === 'undefined');
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (near || !waiting.current)
|
|
131
|
+
return;
|
|
132
|
+
const observer = new IntersectionObserver(entries => { if (entries.some(e => e.isIntersecting))
|
|
133
|
+
setNear(true); }, { rootMargin: '400px' });
|
|
134
|
+
observer.observe(waiting.current);
|
|
135
|
+
return () => observer.disconnect();
|
|
136
|
+
}, [near]);
|
|
137
|
+
const seen = useRef(null);
|
|
138
|
+
const drag = useRef(null);
|
|
139
|
+
const show = (s) => { setSelected(new Set(s.marks ?? [])); setFrom(s); };
|
|
140
|
+
// A file a cell has just written is shown before the cell has described it: no cell names those
|
|
141
|
+
// bytes yet. Such a file is asked again when the page's events move on (the cell ending is one).
|
|
142
|
+
const missedAt = useRef(undefined), latest = useRef(cursor);
|
|
143
|
+
latest.current = cursor;
|
|
144
|
+
const [attempt, setAttempt] = useState(0);
|
|
145
|
+
useEffect(() => { if (missedAt.current !== undefined && cursor !== undefined && cursor !== missedAt.current) {
|
|
146
|
+
missedAt.current = undefined;
|
|
147
|
+
setAttempt(a => a + 1);
|
|
148
|
+
} }, [cursor]);
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
if (!near)
|
|
151
|
+
return;
|
|
152
|
+
let live = true;
|
|
153
|
+
const asked = cursor;
|
|
154
|
+
setLoaded(null);
|
|
155
|
+
setProblem('');
|
|
156
|
+
setSelected(new Set());
|
|
157
|
+
setFrom(null);
|
|
158
|
+
seen.current = null;
|
|
159
|
+
readView(client, projectId, ref).then(v => {
|
|
160
|
+
if (!live)
|
|
161
|
+
return;
|
|
162
|
+
missedAt.current = undefined;
|
|
163
|
+
try {
|
|
164
|
+
setLoaded({ record: v.record, selections: v.selections, svg: svgElement(v.record.svg) });
|
|
165
|
+
setSelections(v.selections);
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
setProblem(e.message);
|
|
169
|
+
}
|
|
170
|
+
}, e => {
|
|
171
|
+
if (!live)
|
|
172
|
+
return;
|
|
173
|
+
// Events that arrived while it was asked are already past: ask again now.
|
|
174
|
+
if (ref.sha256 && e?.error?.kind === 'not_found' && !e?.error?.details?.saved) {
|
|
175
|
+
if (asked !== undefined && latest.current !== asked)
|
|
176
|
+
setAttempt(a => a + 1);
|
|
177
|
+
else
|
|
178
|
+
missedAt.current = asked;
|
|
179
|
+
}
|
|
180
|
+
else
|
|
181
|
+
setProblem(e?.error?.message ?? e?.message ?? 'The figure could not be read.');
|
|
182
|
+
});
|
|
183
|
+
return () => { live = false; };
|
|
184
|
+
}, [client, projectId, key, near, attempt]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
185
|
+
// Marks people and Agents pointed to arrive with the page's events.
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
if (!loaded || cursor === undefined)
|
|
188
|
+
return;
|
|
189
|
+
let live = true;
|
|
190
|
+
client.call('figure.read', { projectId, ...ref, form: 'selections' }).then(v => { if (live)
|
|
191
|
+
setSelections(v.selections ?? []); }, () => { });
|
|
192
|
+
return () => { live = false; };
|
|
193
|
+
}, [cursor, loaded, client, projectId, key]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
194
|
+
// Each drawn element and the marks it stands for.
|
|
195
|
+
const elements = useMemo(() => {
|
|
196
|
+
if (!loaded)
|
|
197
|
+
return [];
|
|
198
|
+
// A cell of a heatmap or a tile says its value by its colour: it keeps it while others are
|
|
199
|
+
// selected, and a selection outlines it.
|
|
200
|
+
const tiles = new Set(loaded.record.figure.layers.flatMap((l, i) => tileGeom.test(l.geom) ? [i] : []));
|
|
201
|
+
return [...loaded.svg.querySelectorAll('[data-rho]')].map(el => {
|
|
202
|
+
const refs = elementMarks(loaded.record, el.getAttribute('data-rho'));
|
|
203
|
+
if (refs.some(r => tiles.has(r.layer)))
|
|
204
|
+
el.classList.add('fig-tile');
|
|
205
|
+
return { el, marks: refs.map(markName) };
|
|
206
|
+
});
|
|
207
|
+
}, [loaded]);
|
|
208
|
+
const byElement = useMemo(() => new Map(elements.map(e => [e.el, e.marks])), [elements]);
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
if (!loaded || !host.current)
|
|
211
|
+
return;
|
|
212
|
+
host.current.replaceChildren(loaded.svg);
|
|
213
|
+
loaded.svg.setAttribute('aria-label', label);
|
|
214
|
+
}, [loaded, label]);
|
|
215
|
+
// An Agent's new marks show at once when the person has nothing selected.
|
|
216
|
+
const agentMarks = selections.filter(s => s.marks?.length && !(author?.(s.by).mine ?? false));
|
|
217
|
+
useEffect(() => {
|
|
218
|
+
if (!loaded)
|
|
219
|
+
return;
|
|
220
|
+
const ids = new Set(agentMarks.map(s => s.selectionId));
|
|
221
|
+
if (seen.current) {
|
|
222
|
+
const fresh = agentMarks.find(s => !seen.current.has(s.selectionId));
|
|
223
|
+
if (fresh && selected.size === 0) {
|
|
224
|
+
show(fresh);
|
|
225
|
+
setArrived(true);
|
|
226
|
+
setTimeout(() => setArrived(false), 1000);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
seen.current = ids;
|
|
230
|
+
}, [selections, loaded]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
231
|
+
useEffect(() => {
|
|
232
|
+
for (const { el, marks } of elements)
|
|
233
|
+
el.classList.toggle('fig-on', marks.some(m => selected.has(m)));
|
|
234
|
+
}, [elements, selected]);
|
|
235
|
+
if (problem || !loaded)
|
|
236
|
+
return _jsxs("figure", { ref: waiting, className: 'fig' + (fit ? ' fit' : ''), children: [fallback, problem && _jsxs("p", { className: "fig-note", children: ["The plot shows as a picture: ", problem] })] });
|
|
237
|
+
const { record } = loaded;
|
|
238
|
+
const choose = (marks, add) => {
|
|
239
|
+
setFrom(null);
|
|
240
|
+
setSelected(previous => {
|
|
241
|
+
if (!add)
|
|
242
|
+
return new Set(marks);
|
|
243
|
+
const next = new Set(previous), all = marks.every(m => next.has(m));
|
|
244
|
+
for (const m of marks)
|
|
245
|
+
if (all)
|
|
246
|
+
next.delete(m);
|
|
247
|
+
else
|
|
248
|
+
next.add(m);
|
|
249
|
+
return next;
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
const at = (event) => { const box = canvas.current.getBoundingClientRect(); return { x: event.clientX - box.left, y: event.clientY - box.top }; };
|
|
253
|
+
const markAt = (target) => target?.closest?.('[data-rho]') ?? null;
|
|
254
|
+
const onPointerDown = (event) => {
|
|
255
|
+
if (event.button !== 0)
|
|
256
|
+
return;
|
|
257
|
+
const p = at(event);
|
|
258
|
+
drag.current = { ...p, moved: false, add: event.shiftKey || event.metaKey || event.ctrlKey, on: markAt(event.target) };
|
|
259
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
260
|
+
};
|
|
261
|
+
const onPointerMove = (event) => {
|
|
262
|
+
const p = at(event), d = drag.current;
|
|
263
|
+
if (d) {
|
|
264
|
+
if (!d.moved && Math.hypot(p.x - d.x, p.y - d.y) < figureViewLimits.brushPx)
|
|
265
|
+
return;
|
|
266
|
+
d.moved = true;
|
|
267
|
+
setTip(null);
|
|
268
|
+
setBrush({ x: Math.min(d.x, p.x), y: Math.min(d.y, p.y), w: Math.abs(p.x - d.x), h: Math.abs(p.y - d.y) });
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const el = markAt(document.elementFromPoint(event.clientX, event.clientY));
|
|
272
|
+
canvas.current?.querySelector('.fig-hover')?.classList.remove('fig-hover');
|
|
273
|
+
if (!el) {
|
|
274
|
+
setTip(null);
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
el.classList.add('fig-hover');
|
|
278
|
+
const marks = byElement.get(el) ?? [];
|
|
279
|
+
const rows = marks.slice(0, 1).map(m => markRow(record, parseMark(m))).filter((r) => !!r);
|
|
280
|
+
setTip({ x: p.x, y: p.y, rows, extra: marks.length - rows.length });
|
|
281
|
+
};
|
|
282
|
+
const onPointerUp = (event) => {
|
|
283
|
+
const d = drag.current;
|
|
284
|
+
drag.current = null;
|
|
285
|
+
setBrush(null);
|
|
286
|
+
if (!d)
|
|
287
|
+
return;
|
|
288
|
+
if (d.moved) {
|
|
289
|
+
const box = canvas.current.getBoundingClientRect(), p = at(event);
|
|
290
|
+
const left = box.left + Math.min(d.x, p.x), right = box.left + Math.max(d.x, p.x), top = box.top + Math.min(d.y, p.y), bottom = box.top + Math.max(d.y, p.y);
|
|
291
|
+
const hit = [];
|
|
292
|
+
for (const { el, marks } of elements) {
|
|
293
|
+
const r = el.getBoundingClientRect();
|
|
294
|
+
if (r.right >= left && r.left <= right && r.bottom >= top && r.top <= bottom)
|
|
295
|
+
hit.push(...marks);
|
|
296
|
+
}
|
|
297
|
+
choose([...new Set(hit)], d.add);
|
|
298
|
+
}
|
|
299
|
+
else if (d.on)
|
|
300
|
+
choose(byElement.get(d.on) ?? [], d.add);
|
|
301
|
+
else if (!d.add) {
|
|
302
|
+
setSelected(new Set());
|
|
303
|
+
setFrom(null);
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
const onLeave = () => { canvas.current?.querySelector('.fig-hover')?.classList.remove('fig-hover'); setTip(null); };
|
|
307
|
+
const ask = async () => {
|
|
308
|
+
if (!onAsk || !selected.size)
|
|
309
|
+
return;
|
|
310
|
+
setAsking(true);
|
|
311
|
+
try {
|
|
312
|
+
const { selection } = await client.call('figure.select', { projectId, ...ref, marks: [...selected].slice(0, 5000), changeId: 'ask-' + crypto.randomUUID() });
|
|
313
|
+
onAsk({ selectionId: selection.selectionId, ...(file ? { file } : { cellId, plot }), count: selection.count, label: `${groupsText(record, groupsOf(record, selection.marks ?? [...selected]))} · ${name ?? label}` });
|
|
314
|
+
}
|
|
315
|
+
catch (e) {
|
|
316
|
+
setProblem(e?.error?.message ?? e?.message ?? 'The selection could not be kept.');
|
|
317
|
+
}
|
|
318
|
+
finally {
|
|
319
|
+
setAsking(false);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
const bound = record.figure.layers.filter(l => l.bound !== 'none').length;
|
|
323
|
+
// What the record could not do, and what it noticed: marks drawn elsewhere than their values,
|
|
324
|
+
// labels that name other rows than the values beside them.
|
|
325
|
+
const notes = [...record.problems, ...new Set(record.figure.layers.flatMap(l => l.notes ?? []))];
|
|
326
|
+
const agentShown = !!from && !(author?.(from.by).mine ?? false);
|
|
327
|
+
return _jsxs("figure", { className: 'fig' + (fit ? ' fit' : ''), style: fit || !width ? undefined : { maxWidth: Math.round(width / 2) }, onKeyDown: event => { if (event.key === 'Escape' && selected.size) {
|
|
328
|
+
event.stopPropagation();
|
|
329
|
+
setSelected(new Set());
|
|
330
|
+
setFrom(null);
|
|
331
|
+
} }, children: [agentMarks.length > 0 && _jsx("div", { className: "fig-marks", role: "group", "aria-label": "Marks the Agent pointed to", children: agentMarks.slice(0, 4).map(s => _jsxs("button", { type: "button", className: "fig-mark-chip", "aria-pressed": from?.selectionId === s.selectionId, title: s.note || undefined, onClick: () => from?.selectionId === s.selectionId ? (setSelected(new Set()), setFrom(null)) : show(s), children: [_jsx("strong", { children: author?.(s.by).name ?? 'Agent' }), _jsx("span", { children: s.note || plural(s.count, 'mark') })] }, s.selectionId)) }), _jsxs("div", { ref: canvas, className: 'fig-canvas' + (selected.size ? ' has-sel' : '') + (agentShown ? ' agent' : '') + (arrived ? ' arrived' : ''), tabIndex: 0, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: () => { drag.current = null; setBrush(null); }, onPointerLeave: onLeave, "aria-describedby": undefined, children: [_jsx("div", { ref: host, style: fit ? { width: '100%', height: '100%' } : undefined }), brush && _jsx("div", { className: "fig-brush", style: { left: brush.x, top: brush.y, width: brush.w, height: brush.h } }), tip && tip.rows[0] && _jsx(Tip, { record: record, tip: tip, box: canvas.current?.getBoundingClientRect() })] }), selected.size > 0 ? _jsx(Selected, { record: record, marks: [...selected], from: from, who: from ? author?.(from.by).name ?? 'Agent' : undefined, agent: agentShown, asking: asking, onAsk: onAsk ? ask : undefined, onClear: () => { setSelected(new Set()); setFrom(null); } })
|
|
332
|
+
: _jsxs("p", { className: "fig-hint", children: [bound ? _jsxs(_Fragment, { children: ["Point at a mark to read its row. ", _jsx("b", { children: "Click" }), " or ", _jsx("b", { children: "drag" }), " to select", onAsk ? ', then ask the Agent about the selection' : '', "."] }) : 'Nothing in this plot could be bound to its rows.', notes.length > 0 && _jsxs(_Fragment, { children: [" ", notes.join(' ')] })] })] });
|
|
333
|
+
}
|
|
334
|
+
function Tip({ record, tip, box }) {
|
|
335
|
+
const row = tip.rows[0], facets = Object.entries(row.facets).map(([k, v]) => `${k} ${v}`).join(', ');
|
|
336
|
+
const heading = title(row);
|
|
337
|
+
const fields = rowFields(record, row, figureViewLimits.tipColumns + (row.key ? 1 : 0)).filter(([name, value]) => value !== null && !(row.key && name === (row.key.column === '.rownames' ? 'row' : row.key.column)));
|
|
338
|
+
// Beside the pointer where the tip fits, to its right or else to its left; in a figure narrower than
|
|
339
|
+
// that (a phone), across the figure's width, above or below the pointer.
|
|
340
|
+
const wide = box ? Math.min(280, box.width) : 280, below = box && tip.y < 120;
|
|
341
|
+
const side = !box || tip.x + 14 + wide <= box.width ? 'right' : tip.x - 14 - wide >= 0 ? 'left' : 'across';
|
|
342
|
+
const across = side === 'across' ? Math.max(0, Math.min(tip.x - wide / 2, box.width - wide)) : undefined;
|
|
343
|
+
return _jsxs("div", { className: "fig-tip", style: { left: side === 'right' ? tip.x + 14 : across, right: side === 'left' ? box.width - tip.x + 14 : undefined, width: side === 'across' ? wide : undefined, maxWidth: wide,
|
|
344
|
+
top: below ? tip.y + 16 : undefined, bottom: below ? undefined : (box ? box.height - tip.y + 12 : undefined) }, children: [_jsxs("small", { children: [row.geom, facets ? ' · ' + facets : '', tip.extra > 0 ? ` · one of ${tip.extra + 1} rows` : ''] }), heading && _jsx("b", { children: heading }), fields.length > 0 && _jsx("dl", { children: fields.map(([name, value]) => _jsxs("div", { style: { display: 'contents' }, children: [_jsx("dt", { children: name }), _jsx("dd", { children: fmt(value) })] }, name)) })] });
|
|
345
|
+
}
|
|
346
|
+
function Selected({ record, marks, from, who, agent, asking, onAsk, onClear }) {
|
|
347
|
+
const groups = useMemo(() => groupsOf(record, marks), [record, marks]);
|
|
348
|
+
const what = groupsText(record, groups);
|
|
349
|
+
const origin = (layer) => { const l = record.figure.layers[layer]; return l.source ? `rows of ${l.source === 'data' ? 'the plot\'s data' : l.source}` : `computed by ${l.stat.toLowerCase()}`; };
|
|
350
|
+
const drawnAgain = groups.filter(g => g.also.length).map(g => `the ${plural(g.rows.length, markWord(record.figure.layers[g.layer].geom))} ${g.rows.length === 1 ? 'is' : 'are'} also drawn as ${g.also.map(l => markWord(record.figure.layers[l].geom) + 's').join(' and ')}`);
|
|
351
|
+
return _jsxs("section", { className: 'fig-sel' + (agent ? ' agent' : ''), "aria-label": "Selected marks", children: [_jsxs("div", { className: "fig-sel-head", children: [_jsx("h4", { children: agent ? `${who} marked ${what}` : `${what} selected` }), _jsx("p", { children: [...groups.map(g => origin(g.layer)).filter((t, i, a) => a.indexOf(t) === i), ...drawnAgain].join(' · ') }), _jsxs("div", { className: "fig-sel-actions", children: [onAsk && _jsx("button", { type: "button", className: "primary", disabled: asking, onClick: onAsk, children: asking ? 'Keeping…' : 'Ask the Agent' }), _jsx("button", { type: "button", className: "secondary", onClick: onClear, children: "Clear" })] })] }), from?.note && _jsx("p", { className: "fig-sel-note", children: from.note }), groups.slice(0, 3).map(g => _jsx(Rows, { record: record, layer: g.layer, rows: g.rows }, g.layer))] });
|
|
352
|
+
}
|
|
353
|
+
function Rows({ record, layer, rows }) {
|
|
354
|
+
const shown = rows.slice(0, figureViewLimits.tableRows);
|
|
355
|
+
const fields = rowFields(record, shown[0], figureViewLimits.tableColumns).map(([name]) => name);
|
|
356
|
+
const facets = [...new Set(shown.flatMap(r => Object.keys(r.facets)))].filter(f => !fields.includes(f));
|
|
357
|
+
const columns = [...facets, ...fields].slice(0, Math.max(figureViewLimits.tableColumns, facets.length + 1));
|
|
358
|
+
const value = (r, name) => {
|
|
359
|
+
const held = name === 'row' ? '.rownames' : name;
|
|
360
|
+
return facets.includes(name) ? r.facets[name] ?? null : r.values && held in r.values ? r.values[held] ?? null : r.built[name] ?? null;
|
|
361
|
+
};
|
|
362
|
+
return _jsxs(_Fragment, { children: [_jsx("div", { className: "fig-table-wrap", children: _jsxs("table", { className: "fig-table", children: [_jsx("thead", { children: _jsx("tr", { children: columns.map(c => _jsx("th", { scope: "col", children: c }, c)) }) }), _jsx("tbody", { children: shown.map(r => _jsx("tr", { children: columns.map(c => { const v = value(r, c); return _jsx("td", { className: typeof v === 'number' ? 'num' : undefined, title: v === null ? undefined : String(v), children: fmt(v) }, c); }) }, r.mark)) })] }) }), rows.length > shown.length && _jsxs("p", { className: "fig-more", children: [rows.length - shown.length, " more ", plural(rows.length - shown.length, markWord(record.figure.layers[layer].geom)).replace(/^\d+ /, ''), " ", rows.length - shown.length === 1 ? 'is' : 'are', " selected; the Agent reads them all."] })] });
|
|
363
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rho-workspace/plugin-figures",
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Rho figures plugin: plots drawn in the live R or Python session kept with their marks bound to the data rows they were drawn from, for the person to select and the Agent to read",
|
|
6
|
+
"license": "AGPL-3.0-only",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=22.22.0 <23"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.js",
|
|
12
|
+
"./operations": "./dist/operations.js",
|
|
13
|
+
"./record": "./dist/record.js",
|
|
14
|
+
"./ui": "./dist/ui.js"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"r",
|
|
19
|
+
"py",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc -p tsconfig.json",
|
|
25
|
+
"prepack": "npm run build"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@rho-workspace/plugin-contracts": "0.4.0"
|
|
29
|
+
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"react": "19.3.1"
|
|
32
|
+
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"react": {
|
|
35
|
+
"optional": true
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/react": "19.3.0"
|
|
40
|
+
}
|
|
41
|
+
}
|