aac-board-viewer 0.1.1 → 0.1.3
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 +2 -28
- package/dist/index.js +241 -61
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +241 -61
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -40,24 +40,10 @@ yarn add aac-board-viewer
|
|
|
40
40
|
|
|
41
41
|
## Quick Start
|
|
42
42
|
|
|
43
|
-
### Client-Side Usage
|
|
44
|
-
|
|
45
|
-
```tsx
|
|
46
|
-
import { BoardViewer, useAACFile } from 'aac-board-viewer';
|
|
47
|
-
import 'aac-board-viewer/styles';
|
|
48
|
-
|
|
49
|
-
function MyViewer() {
|
|
50
|
-
const { tree, loading, error } = useAACFile('/path/to/file.sps');
|
|
51
|
-
|
|
52
|
-
if (loading) return <div>Loading...</div>;
|
|
53
|
-
if (error) return <div>Error: {error.message}</div>;
|
|
54
|
-
|
|
55
|
-
return <BoardViewer tree={tree} />;
|
|
56
|
-
}
|
|
57
|
-
```
|
|
58
|
-
|
|
59
43
|
### Server-Side / API Usage
|
|
60
44
|
|
|
45
|
+
> Important: `@willwade/aac-processors` is a Node-only dependency (uses native modules like `better-sqlite3`). File parsing must happen server-side. For client apps, call an API that returns the parsed tree and pass that to `BoardViewer`.
|
|
46
|
+
|
|
61
47
|
```tsx
|
|
62
48
|
import { BoardViewer } from 'aac-board-viewer';
|
|
63
49
|
import 'aac-board-viewer/styles';
|
|
@@ -179,18 +165,6 @@ interface AACTree {
|
|
|
179
165
|
|
|
180
166
|
The library provides utilities for loading AAC files:
|
|
181
167
|
|
|
182
|
-
### Client-Side Loading
|
|
183
|
-
|
|
184
|
-
```tsx
|
|
185
|
-
import { useAACFile } from 'aac-board-viewer';
|
|
186
|
-
|
|
187
|
-
function Viewer({ fileUrl }) {
|
|
188
|
-
const { tree, loading, error } = useAACFile(fileUrl);
|
|
189
|
-
|
|
190
|
-
// ...
|
|
191
|
-
}
|
|
192
|
-
```
|
|
193
|
-
|
|
194
168
|
### Programmatic Loading
|
|
195
169
|
|
|
196
170
|
```tsx
|
package/dist/index.js
CHANGED
|
@@ -47,6 +47,53 @@ module.exports = __toCommonJS(index_exports);
|
|
|
47
47
|
// src/components/BoardViewer.tsx
|
|
48
48
|
var import_react = __toESM(require("react"));
|
|
49
49
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
50
|
+
function PredictionsTooltip({ predictions, label, position, onClose }) {
|
|
51
|
+
import_react.default.useEffect(() => {
|
|
52
|
+
const handleClickOutside = (e) => {
|
|
53
|
+
if (e.target instanceof HTMLElement && !e.target.closest(".predictions-tooltip")) {
|
|
54
|
+
onClose();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
58
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
59
|
+
}, [onClose]);
|
|
60
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
61
|
+
"div",
|
|
62
|
+
{
|
|
63
|
+
className: "predictions-tooltip fixed z-50 bg-white dark:bg-gray-800 rounded-lg shadow-xl border-2 border-purple-500 p-3 max-w-xs",
|
|
64
|
+
style: {
|
|
65
|
+
left: `${Math.min(position.x, window.innerWidth - 200)}px`,
|
|
66
|
+
top: `${Math.min(position.y, window.innerHeight - 150)}px`
|
|
67
|
+
},
|
|
68
|
+
children: [
|
|
69
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between mb-2", children: [
|
|
70
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h4", { className: "text-sm font-semibold text-gray-900 dark:text-white", children: [
|
|
71
|
+
'Word forms for "',
|
|
72
|
+
label,
|
|
73
|
+
'"'
|
|
74
|
+
] }),
|
|
75
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
76
|
+
"button",
|
|
77
|
+
{
|
|
78
|
+
onClick: onClose,
|
|
79
|
+
className: "p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded",
|
|
80
|
+
"aria-label": "Close",
|
|
81
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { className: "h-4 w-4 text-gray-600 dark:text-gray-400", fill: "none", stroke: "currentColor", viewBox: "0 0 24 24", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })
|
|
82
|
+
}
|
|
83
|
+
)
|
|
84
|
+
] }),
|
|
85
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-wrap gap-1", children: predictions.map((word, idx) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
86
|
+
"span",
|
|
87
|
+
{
|
|
88
|
+
className: "px-2 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded text-xs font-medium",
|
|
89
|
+
children: word
|
|
90
|
+
},
|
|
91
|
+
idx
|
|
92
|
+
)) })
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
}
|
|
50
97
|
function BoardViewer({
|
|
51
98
|
tree,
|
|
52
99
|
buttonMetrics,
|
|
@@ -72,6 +119,12 @@ function BoardViewer({
|
|
|
72
119
|
return tree.rootId;
|
|
73
120
|
}
|
|
74
121
|
}
|
|
122
|
+
const startPage = Object.values(tree.pages).find(
|
|
123
|
+
(p) => p.name.toLowerCase() === "start"
|
|
124
|
+
);
|
|
125
|
+
if (startPage) {
|
|
126
|
+
return startPage.id;
|
|
127
|
+
}
|
|
75
128
|
const nonToolbarPage = Object.values(tree.pages).find(
|
|
76
129
|
(p) => !p.name.toLowerCase().includes("toolbar") && !p.name.toLowerCase().includes("tool bar")
|
|
77
130
|
);
|
|
@@ -86,6 +139,7 @@ function BoardViewer({
|
|
|
86
139
|
const [message, setMessage] = (0, import_react.useState)("");
|
|
87
140
|
const [currentWordCount, setCurrentWordCount] = (0, import_react.useState)(0);
|
|
88
141
|
const [currentEffort, setCurrentEffort] = (0, import_react.useState)(0);
|
|
142
|
+
const [predictionsTooltip, setPredictionsTooltip] = (0, import_react.useState)(null);
|
|
89
143
|
const buttonMetricsLookup = (0, import_react.useMemo)(() => {
|
|
90
144
|
if (!buttonMetrics) return {};
|
|
91
145
|
const lookup = {};
|
|
@@ -95,6 +149,17 @@ function BoardViewer({
|
|
|
95
149
|
return lookup;
|
|
96
150
|
}, [buttonMetrics]);
|
|
97
151
|
const currentPage = currentPageId ? tree.pages[currentPageId] : null;
|
|
152
|
+
const goToPage = (targetPageId) => {
|
|
153
|
+
if (!targetPageId || !tree.pages[targetPageId]) return false;
|
|
154
|
+
if (currentPage) {
|
|
155
|
+
setPageHistory((prev) => [...prev, currentPage]);
|
|
156
|
+
}
|
|
157
|
+
setCurrentPageId(targetPageId);
|
|
158
|
+
if (onPageChange) {
|
|
159
|
+
onPageChange(targetPageId);
|
|
160
|
+
}
|
|
161
|
+
return true;
|
|
162
|
+
};
|
|
98
163
|
import_react.default.useEffect(() => {
|
|
99
164
|
setCurrentPageId(resolveInitialPageId());
|
|
100
165
|
setPageHistory([]);
|
|
@@ -107,24 +172,62 @@ function BoardViewer({
|
|
|
107
172
|
if (onButtonClick) {
|
|
108
173
|
onButtonClick(button);
|
|
109
174
|
}
|
|
175
|
+
const intent = button.semanticAction?.intent ? String(button.semanticAction.intent) : void 0;
|
|
110
176
|
const targetPageId = button.targetPageId || button.semanticAction?.targetId;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
177
|
+
const effort = buttonMetricsLookup[button.id]?.effort || 1;
|
|
178
|
+
const textValue = button.semanticAction?.text || button.message || button.label || "";
|
|
179
|
+
const deleteLastWord = () => {
|
|
180
|
+
setMessage((prev) => {
|
|
181
|
+
const parts = prev.trim().split(/\s+/);
|
|
182
|
+
parts.pop();
|
|
183
|
+
const newMsg = parts.join(" ");
|
|
184
|
+
return newMsg;
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
const deleteLastCharacter = () => {
|
|
188
|
+
setMessage((prev) => prev.slice(0, -1));
|
|
189
|
+
};
|
|
190
|
+
const appendText = (word) => {
|
|
191
|
+
const trimmed = word || button.label || "";
|
|
192
|
+
setMessage((prev) => {
|
|
193
|
+
const newMessage = trimmed ? prev + (prev ? " " : "") + trimmed : prev;
|
|
194
|
+
if (trimmed) {
|
|
195
|
+
updateStats(trimmed, effort);
|
|
196
|
+
}
|
|
197
|
+
return newMessage;
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
if (intent === "NAVIGATE_TO" && goToPage(targetPageId)) {
|
|
119
201
|
return;
|
|
120
202
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
203
|
+
switch (intent) {
|
|
204
|
+
case "GO_BACK":
|
|
205
|
+
handleBack();
|
|
206
|
+
return;
|
|
207
|
+
case "GO_HOME":
|
|
208
|
+
if (tree.rootId && goToPage(tree.rootId)) return;
|
|
209
|
+
break;
|
|
210
|
+
case "DELETE_WORD":
|
|
211
|
+
deleteLastWord();
|
|
212
|
+
return;
|
|
213
|
+
case "DELETE_CHARACTER":
|
|
214
|
+
deleteLastCharacter();
|
|
215
|
+
return;
|
|
216
|
+
case "CLEAR_TEXT":
|
|
217
|
+
clearMessage();
|
|
218
|
+
return;
|
|
219
|
+
case "SPEAK_IMMEDIATE":
|
|
220
|
+
case "SPEAK_TEXT":
|
|
221
|
+
case "INSERT_TEXT":
|
|
222
|
+
appendText(textValue);
|
|
223
|
+
return;
|
|
224
|
+
default:
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
if (targetPageId && goToPage(targetPageId)) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
appendText(textValue);
|
|
128
231
|
};
|
|
129
232
|
const handleBack = () => {
|
|
130
233
|
if (pageHistory.length > 0) {
|
|
@@ -141,6 +244,17 @@ function BoardViewer({
|
|
|
141
244
|
setCurrentWordCount(0);
|
|
142
245
|
setCurrentEffort(0);
|
|
143
246
|
};
|
|
247
|
+
const handleShowPredictions = (button, event) => {
|
|
248
|
+
event.stopPropagation();
|
|
249
|
+
const predictions = button.predictions || button.parameters?.predictions;
|
|
250
|
+
if (predictions && predictions.length > 0) {
|
|
251
|
+
setPredictionsTooltip({
|
|
252
|
+
predictions,
|
|
253
|
+
label: button.label,
|
|
254
|
+
position: { x: event.clientX, y: event.clientY }
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
};
|
|
144
258
|
const getTextColor = (backgroundColor) => {
|
|
145
259
|
if (!backgroundColor) return "text-gray-900 dark:text-gray-100";
|
|
146
260
|
const hex = backgroundColor.replace("#", "");
|
|
@@ -292,52 +406,109 @@ ${button.message || ""}`,
|
|
|
292
406
|
display: "grid",
|
|
293
407
|
gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`
|
|
294
408
|
},
|
|
295
|
-
children:
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
409
|
+
children: (() => {
|
|
410
|
+
const rendered = /* @__PURE__ */ new Set();
|
|
411
|
+
return currentPage.grid.flatMap(
|
|
412
|
+
(row, rowIndex) => row.map((button, colIndex) => {
|
|
413
|
+
if (!button) {
|
|
414
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "aspect-square" }, `empty-${rowIndex}-${colIndex}`);
|
|
415
|
+
}
|
|
416
|
+
if (rendered.has(button.id)) {
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
rendered.add(button.id);
|
|
420
|
+
const buttonMetric = buttonMetricsLookup[button.id];
|
|
421
|
+
const effort = buttonMetric?.effort || 0;
|
|
422
|
+
const targetPageId = button.targetPageId || button.semanticAction?.targetId;
|
|
423
|
+
const hasLink = targetPageId && tree.pages[targetPageId];
|
|
424
|
+
const colSpan = button.columnSpan || 1;
|
|
425
|
+
const rowSpan = button.rowSpan || 1;
|
|
426
|
+
const predictions = button.predictions || button.parameters?.predictions;
|
|
427
|
+
const hasPredictions = predictions && predictions.length > 0;
|
|
428
|
+
const isPredictionCell = button.contentType === "AutoContent" && (button.contentSubType || "").toLowerCase() === "prediction";
|
|
429
|
+
const isWorkspace = button.contentType === "Workspace";
|
|
430
|
+
const imageSrc = (button.resolvedImageEntry && !String(button.resolvedImageEntry).startsWith("[") ? button.resolvedImageEntry : null) || (button.image && !String(button.image).startsWith("[") ? button.image : null);
|
|
431
|
+
if (isWorkspace) {
|
|
432
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
433
|
+
"div",
|
|
434
|
+
{
|
|
435
|
+
className: "relative p-3 rounded-lg border-2 bg-gray-50 text-gray-800 flex items-center gap-2",
|
|
436
|
+
style: {
|
|
437
|
+
borderColor: button.style?.borderColor || "#e5e7eb",
|
|
438
|
+
gridColumn: `${colIndex + 1} / span ${colSpan}`,
|
|
439
|
+
gridRow: `${rowIndex + 1} / span ${rowSpan}`
|
|
440
|
+
},
|
|
441
|
+
children: [
|
|
442
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "font-semibold text-sm", children: button.label || "Workspace" }),
|
|
443
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "text-xs text-gray-500 truncate", children: message || "Chat writing area" })
|
|
444
|
+
]
|
|
445
|
+
},
|
|
446
|
+
button.id
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
450
|
+
"button",
|
|
451
|
+
{
|
|
452
|
+
onClick: () => handleButtonClick(button),
|
|
453
|
+
className: "relative aspect-square p-2 rounded-lg border-2 transition flex flex-col items-center justify-center gap-1 hover:opacity-80 hover:scale-105 active:scale-95",
|
|
454
|
+
style: {
|
|
455
|
+
backgroundColor: button.style?.backgroundColor || "#f3f4f6",
|
|
456
|
+
borderColor: button.style?.borderColor || "#e5e7eb",
|
|
457
|
+
color: button.style?.fontColor || void 0,
|
|
458
|
+
gridColumn: `${colIndex + 1} / span ${colSpan}`,
|
|
459
|
+
gridRow: `${rowIndex + 1} / span ${rowSpan}`
|
|
460
|
+
},
|
|
461
|
+
children: [
|
|
462
|
+
buttonMetric && showEffortBadges && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "absolute top-1 right-1 px-1.5 py-0.5 text-xs font-semibold rounded bg-blue-600 text-white shadow-sm", children: effort.toFixed(1) }),
|
|
463
|
+
hasLink && showLinkIndicators && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "absolute top-1 left-1 w-2 h-2 bg-green-500 rounded-full shadow-sm" }),
|
|
464
|
+
hasPredictions && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
465
|
+
"div",
|
|
466
|
+
{
|
|
467
|
+
onClick: (e) => handleShowPredictions(button, e),
|
|
468
|
+
className: "absolute bottom-1 right-1 px-1.5 py-0.5 text-xs font-semibold rounded bg-purple-600 text-white shadow-sm cursor-pointer hover:bg-purple-700 transition",
|
|
469
|
+
title: `Has ${predictions?.length} word form${predictions && predictions.length > 1 ? "s" : ""}`,
|
|
470
|
+
children: predictions?.length
|
|
471
|
+
}
|
|
472
|
+
),
|
|
473
|
+
imageSrc && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
474
|
+
"img",
|
|
475
|
+
{
|
|
476
|
+
src: imageSrc,
|
|
477
|
+
alt: button.label,
|
|
478
|
+
className: "max-h-12 object-contain"
|
|
479
|
+
}
|
|
480
|
+
),
|
|
481
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col items-center justify-center", children: [
|
|
482
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
483
|
+
"span",
|
|
484
|
+
{
|
|
485
|
+
className: `text-xs sm:text-sm text-center font-medium leading-tight line-clamp-3 ${getTextColor(
|
|
486
|
+
button.style?.backgroundColor
|
|
487
|
+
)}`,
|
|
488
|
+
children: button.label
|
|
489
|
+
}
|
|
490
|
+
),
|
|
491
|
+
isPredictionCell && predictions && predictions.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "mt-1 text-[10px] sm:text-xs text-center opacity-80 space-y-0.5", children: [
|
|
492
|
+
predictions.slice(0, 3).map((p, idx) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: p }, `${button.id}-pred-${idx}`)),
|
|
493
|
+
predictions.length > 3 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: "\u2026" })
|
|
494
|
+
] })
|
|
495
|
+
] }),
|
|
496
|
+
button.message && button.message !== button.label && !isPredictionCell && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
497
|
+
"span",
|
|
498
|
+
{
|
|
499
|
+
className: `text-[10px] sm:text-xs text-center opacity-75 line-clamp-2 ${getTextColor(
|
|
500
|
+
button.style?.backgroundColor
|
|
501
|
+
)}`,
|
|
502
|
+
children: button.message
|
|
503
|
+
}
|
|
504
|
+
)
|
|
505
|
+
]
|
|
313
506
|
},
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
{
|
|
320
|
-
className: `text-xs sm:text-sm text-center font-medium leading-tight line-clamp-3 ${getTextColor(
|
|
321
|
-
button.style?.backgroundColor
|
|
322
|
-
)}`,
|
|
323
|
-
children: button.label
|
|
324
|
-
}
|
|
325
|
-
),
|
|
326
|
-
button.message && button.message !== button.label && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
327
|
-
"span",
|
|
328
|
-
{
|
|
329
|
-
className: `text-[10px] sm:text-xs text-center opacity-75 line-clamp-2 ${getTextColor(
|
|
330
|
-
button.style?.backgroundColor
|
|
331
|
-
)}`,
|
|
332
|
-
children: button.message
|
|
333
|
-
}
|
|
334
|
-
)
|
|
335
|
-
]
|
|
336
|
-
},
|
|
337
|
-
button.id
|
|
338
|
-
);
|
|
339
|
-
})
|
|
340
|
-
)
|
|
507
|
+
button.id
|
|
508
|
+
);
|
|
509
|
+
})
|
|
510
|
+
);
|
|
511
|
+
})()
|
|
341
512
|
}
|
|
342
513
|
),
|
|
343
514
|
Object.keys(tree.pages).length > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "p-4 border-t border-gray-200 dark:border-gray-700", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { className: "text-sm text-gray-600 dark:text-gray-400 mb-2", children: [
|
|
@@ -345,7 +516,16 @@ ${button.message || ""}`,
|
|
|
345
516
|
" pages in this vocabulary"
|
|
346
517
|
] }) })
|
|
347
518
|
] })
|
|
348
|
-
] })
|
|
519
|
+
] }),
|
|
520
|
+
predictionsTooltip && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
521
|
+
PredictionsTooltip,
|
|
522
|
+
{
|
|
523
|
+
predictions: predictionsTooltip.predictions,
|
|
524
|
+
label: predictionsTooltip.label,
|
|
525
|
+
position: predictionsTooltip.position,
|
|
526
|
+
onClose: () => setPredictionsTooltip(null)
|
|
527
|
+
}
|
|
528
|
+
)
|
|
349
529
|
] });
|
|
350
530
|
}
|
|
351
531
|
|