aac-board-viewer 0.1.2 → 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/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
- if (targetPageId && tree.pages[targetPageId]) {
112
- if (currentPage) {
113
- setPageHistory((prev) => [...prev, currentPage]);
114
- }
115
- setCurrentPageId(targetPageId);
116
- if (onPageChange) {
117
- onPageChange(targetPageId);
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
- const word = button.message || button.label;
122
- const effort = buttonMetricsLookup[button.id]?.effort || 1;
123
- setMessage((prev) => {
124
- const newMessage = prev + (prev ? " " : "") + word;
125
- updateStats(word, effort);
126
- return newMessage;
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: currentPage.grid.map(
296
- (row, rowIndex) => row.map((button, colIndex) => {
297
- if (!button) {
298
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "aspect-square" }, `empty-${rowIndex}-${colIndex}`);
299
- }
300
- const buttonMetric = buttonMetricsLookup[button.id];
301
- const effort = buttonMetric?.effort || 0;
302
- const targetPageId = button.targetPageId || button.semanticAction?.targetId;
303
- const hasLink = targetPageId && tree.pages[targetPageId];
304
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
305
- "button",
306
- {
307
- onClick: () => handleButtonClick(button),
308
- 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",
309
- style: {
310
- backgroundColor: button.style?.backgroundColor || "#f3f4f6",
311
- borderColor: button.style?.borderColor || "#e5e7eb",
312
- color: button.style?.fontColor || void 0
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
- children: [
315
- 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) }),
316
- 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" }),
317
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
318
- "span",
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
 
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts","../src/components/BoardViewer.tsx","../src/hooks/useAACFile.ts","../src/utils/loaders.ts"],"sourcesContent":["/**\n * AAC Board Viewer\n *\n * Universal AAC board viewer component for React\n */\n\n// Main component\nexport { BoardViewer } from './components/BoardViewer';\n\n// Hooks\nexport {\n useAACFile,\n useAACFileWithMetrics,\n useMetrics,\n useSentenceBuilder,\n} from './hooks/useAACFile';\n\n// Utilities\nexport {\n loadAACFile,\n loadAACFileFromURL,\n loadAACFileFromFile,\n loadAACFileWithMetadata,\n calculateMetrics,\n getSupportedFormats,\n} from './utils/loaders';\n\n// Types\nexport type {\n BoardViewerProps,\n ButtonMetric,\n LoadAACFileResult,\n MetricsOptions,\n} from './types';\n\n// Re-export AAC processor types\nexport type {\n AACTree,\n AACPage,\n AACButton,\n AACSemanticAction,\n AACSemanticCategory,\n AACSemanticIntent,\n} from '@willwade/aac-processors';\n","import React, { useState, useMemo, useCallback } from 'react';\nimport type {\n AACPage,\n AACButton,\n} from '@willwade/aac-processors';\nimport type { BoardViewerProps, ButtonMetric } from '../types';\n\n/**\n * AAC Board Viewer Component\n *\n * Displays AAC boards with interactive navigation, sentence building,\n * and optional effort metrics.\n *\n * @param props - BoardViewerProps\n */\nexport function BoardViewer({\n tree,\n buttonMetrics,\n showMessageBar = true,\n showEffortBadges = true,\n showLinkIndicators = true,\n initialPageId,\n onButtonClick,\n onPageChange,\n className = '',\n}: BoardViewerProps) {\n const resolveInitialPageId = useCallback(() => {\n // Explicit override\n if (initialPageId && tree.pages[initialPageId]) {\n return initialPageId;\n }\n // If toolbar exists and rootId is different, use rootId\n if (tree.toolbarId && tree.rootId && tree.rootId !== tree.toolbarId) {\n return tree.rootId;\n }\n // If rootId exists and is not a toolbar, use it\n if (tree.rootId && tree.pages[tree.rootId]) {\n const rootPage = tree.pages[tree.rootId];\n const isToolbar =\n rootPage.name.toLowerCase().includes('toolbar') ||\n rootPage.name.toLowerCase().includes('tool bar');\n if (!isToolbar) {\n return tree.rootId;\n }\n }\n // Fall back to first page that's not a toolbar\n const nonToolbarPage = Object.values(tree.pages).find(\n (p) => !p.name.toLowerCase().includes('toolbar') && !p.name.toLowerCase().includes('tool bar')\n );\n if (nonToolbarPage) {\n return nonToolbarPage.id;\n }\n // Last resort: first page\n const pageIds = Object.keys(tree.pages);\n return pageIds.length > 0 ? pageIds[0] : null;\n }, [initialPageId, tree]);\n\n // Determine which page to show\n const [currentPageId, setCurrentPageId] = useState<string | null>(() => resolveInitialPageId());\n\n const [pageHistory, setPageHistory] = useState<AACPage[]>([]);\n const [message, setMessage] = useState('');\n const [currentWordCount, setCurrentWordCount] = useState(0);\n const [currentEffort, setCurrentEffort] = useState(0);\n\n // Convert button metrics array to lookup object for easy access\n const buttonMetricsLookup = useMemo(() => {\n if (!buttonMetrics) return {};\n const lookup: { [buttonId: string]: ButtonMetric } = {};\n buttonMetrics.forEach((metric) => {\n lookup[metric.id] = metric;\n });\n return lookup;\n }, [buttonMetrics]);\n\n const currentPage = currentPageId ? tree.pages[currentPageId] : null;\n\n // Sync when tree or initialPageId changes\n React.useEffect(() => {\n setCurrentPageId(resolveInitialPageId());\n setPageHistory([]);\n }, [resolveInitialPageId]);\n\n // Calculate total stats for current word\n const updateStats = (word: string, effort: number) => {\n setCurrentWordCount((prev) => prev + 1);\n setCurrentEffort((prev) => prev + effort);\n };\n\n const handleButtonClick = (button: AACButton) => {\n // Call external callback if provided\n if (onButtonClick) {\n onButtonClick(button);\n }\n\n // Check if button links to another page via targetPageId or semanticAction\n const targetPageId = button.targetPageId || button.semanticAction?.targetId;\n\n if (targetPageId && tree.pages[targetPageId]) {\n if (currentPage) {\n setPageHistory((prev) => [...prev, currentPage]);\n }\n setCurrentPageId(targetPageId);\n if (onPageChange) {\n onPageChange(targetPageId);\n }\n return;\n }\n\n // Otherwise add to message\n const word = button.message || button.label;\n const effort = buttonMetricsLookup[button.id]?.effort || 1;\n\n setMessage((prev) => {\n const newMessage = prev + (prev ? ' ' : '') + word;\n updateStats(word, effort);\n return newMessage;\n });\n };\n\n const handleBack = () => {\n if (pageHistory.length > 0) {\n const previousPage = pageHistory[pageHistory.length - 1];\n setPageHistory((prev) => prev.slice(0, -1));\n setCurrentPageId(previousPage.id);\n if (onPageChange) {\n onPageChange(previousPage.id);\n }\n }\n };\n\n const clearMessage = () => {\n setMessage('');\n setCurrentWordCount(0);\n setCurrentEffort(0);\n };\n\n const getTextColor = (backgroundColor?: string) => {\n if (!backgroundColor) return 'text-gray-900 dark:text-gray-100';\n\n // Convert hex to rgb for brightness calculation\n const hex = backgroundColor.replace('#', '');\n if (hex.length === 6) {\n const r = parseInt(hex.substring(0, 2), 16);\n const g = parseInt(hex.substring(2, 4), 16);\n const b = parseInt(hex.substring(4, 6), 16);\n const brightness = (r * 299 + g * 587 + b * 114) / 1000;\n return brightness >= 128 ? 'text-gray-900' : 'text-white';\n }\n\n return 'text-gray-900 dark:text-gray-100';\n };\n\n if (!currentPage) {\n return (\n <div className={`flex items-center justify-center h-96 bg-gray-100 dark:bg-gray-800 rounded-lg ${className}`}>\n <div className=\"text-center\">\n <p className=\"text-gray-600 dark:text-gray-400\">No pages available</p>\n </div>\n </div>\n );\n }\n\n // Get toolbar page if it exists\n const toolbarPage = tree.toolbarId ? tree.pages[tree.toolbarId] : null;\n\n // Get grid dimensions\n const gridRows = currentPage.grid.length;\n const gridCols = gridRows > 0 ? currentPage.grid[0].length : 0;\n\n return (\n <div className={`bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden ${className}`}>\n {/* Message Bar */}\n {showMessageBar && (\n <div className=\"p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50\">\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex-1 min-w-0\">\n {message ? (\n <div className=\"space-y-2\">\n <p className=\"text-lg text-gray-900 dark:text-white break-words\">{message}</p>\n <div className=\"flex gap-4 text-sm\">\n <div className=\"text-gray-600 dark:text-gray-400\">\n {currentWordCount} {currentWordCount === 1 ? 'word' : 'words'}\n </div>\n {buttonMetrics && (\n <>\n <div className=\"text-gray-600 dark:text-gray-400\">\n Effort: <span className=\"font-medium\">{currentEffort.toFixed(2)}</span>\n </div>\n <div className=\"text-gray-600 dark:text-gray-400\">\n Avg:{' '}\n <span className=\"font-medium\">\n {currentWordCount > 0\n ? (currentEffort / currentWordCount).toFixed(2)\n : '0.00'}\n </span>\n </div>\n </>\n )}\n </div>\n </div>\n ) : (\n <p className=\"text-gray-500 dark:text-gray-400 italic\">\n Tap buttons to build a sentence...\n </p>\n )}\n </div>\n {message && (\n <button\n onClick={clearMessage}\n className=\"p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition\"\n aria-label=\"Clear message\"\n >\n <svg\n className=\"h-5 w-5 text-gray-600 dark:text-gray-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M6 18L18 6M6 6l12 12\"\n />\n </svg>\n </button>\n )}\n </div>\n </div>\n )}\n\n {/* Main Content Area */}\n <div className=\"flex\">\n {/* Toolbar Sidebar (if exists) */}\n {toolbarPage && (\n <div className=\"w-16 sm:w-20 border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50\">\n <div className=\"p-2\">\n <p className=\"text-[10px] text-gray-500 dark:text-gray-400 text-center mb-2 font-semibold\">\n TOOLBAR\n </p>\n <div className=\"grid gap-1\">\n {toolbarPage.grid.map((row, _rowIndex) =>\n row.map((button, _colIndex) => {\n if (!button) return null;\n\n const buttonMetric = buttonMetricsLookup[button.id];\n const effort = buttonMetric?.effort || 0;\n\n return (\n <button\n key={button.id}\n onClick={() => handleButtonClick(button)}\n className=\"aspect-square p-1 rounded border border-gray-200 dark:border-gray-700 transition flex flex-col items-center justify-center gap-0.5 hover:bg-gray-200 dark:hover:bg-gray-700 relative\"\n style={{\n backgroundColor: button.style?.backgroundColor || '#f3f4f6',\n borderColor: button.style?.borderColor || '#e5e7eb',\n }}\n title={`${button.label}\\n${button.message || ''}`}\n >\n {buttonMetric && showEffortBadges && effort > 0 && (\n <div className=\"absolute top-0 right-0 px-0.5 py-0 text-[8px] font-semibold rounded bg-blue-600 text-white\">\n {effort.toFixed(1)}\n </div>\n )}\n <span\n className={`text-[8px] sm:text-[9px] text-center font-medium leading-tight line-clamp-2 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.label}\n </span>\n </button>\n );\n })\n )}\n </div>\n </div>\n </div>\n )}\n\n {/* Main Page Content */}\n <div className=\"flex-1\">\n {/* Header */}\n <div className=\"flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700\">\n <div className=\"flex items-center gap-2\">\n {pageHistory.length > 0 && (\n <button\n onClick={handleBack}\n className=\"p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition\"\n aria-label=\"Go back\"\n >\n <svg\n className=\"h-5 w-5 text-gray-600 dark:text-gray-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 19l-7-7 7-7\"\n />\n </svg>\n </button>\n )}\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white\">\n {currentPage.name}\n </h3>\n </div>\n <div className=\"text-sm text-gray-500 dark:text-gray-400\">\n {gridRows}×{gridCols} grid\n </div>\n </div>\n\n {/* Grid */}\n <div\n className=\"p-4 gap-2 overflow-auto max-h-[600px]\"\n style={{\n display: 'grid',\n gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`,\n }}\n >\n {currentPage.grid.map((row, rowIndex) =>\n row.map((button, colIndex) => {\n if (!button) {\n return <div key={`empty-${rowIndex}-${colIndex}`} className=\"aspect-square\" />;\n }\n\n const buttonMetric = buttonMetricsLookup[button.id];\n const effort = buttonMetric?.effort || 0;\n const targetPageId = button.targetPageId || button.semanticAction?.targetId;\n const hasLink = targetPageId && tree.pages[targetPageId];\n\n return (\n <button\n key={button.id}\n onClick={() => handleButtonClick(button)}\n 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\"\n style={{\n backgroundColor: button.style?.backgroundColor || '#f3f4f6',\n borderColor: button.style?.borderColor || '#e5e7eb',\n color: button.style?.fontColor || undefined,\n }}\n >\n {/* Effort Badge */}\n {buttonMetric && showEffortBadges && (\n <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\">\n {effort.toFixed(1)}\n </div>\n )}\n\n {/* Link Indicator */}\n {hasLink && showLinkIndicators && (\n <div className=\"absolute top-1 left-1 w-2 h-2 bg-green-500 rounded-full shadow-sm\" />\n )}\n\n {/* Label */}\n <span\n className={`text-xs sm:text-sm text-center font-medium leading-tight line-clamp-3 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.label}\n </span>\n\n {/* Message (if different from label) */}\n {button.message && button.message !== button.label && (\n <span\n className={`text-[10px] sm:text-xs text-center opacity-75 line-clamp-2 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.message}\n </span>\n )}\n </button>\n );\n })\n )}\n </div>\n\n {/* Page Navigation (if multiple pages) */}\n {Object.keys(tree.pages).length > 1 && (\n <div className=\"p-4 border-t border-gray-200 dark:border-gray-700\">\n <p className=\"text-sm text-gray-600 dark:text-gray-400 mb-2\">\n {Object.keys(tree.pages).length} pages in this vocabulary\n </p>\n </div>\n )}\n </div>\n </div>\n </div>\n );\n}\n","/**\n * React hooks for AAC Board Viewer\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { loadAACFileFromURL, calculateMetrics } from '../utils/loaders';\nimport type { AACTree } from '@willwade/aac-processors';\nimport type { ButtonMetric, MetricsOptions } from '../types';\n\n/**\n * Hook to load an AAC file from a URL\n *\n * @param url - URL to the AAC file\n * @param options - Processor options (e.g., pageLayoutPreference for SNAP)\n * @returns Object with tree, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer() {\n * const { tree, loading, error, reload } = useAACFile('/files/board.sps');\n *\n * if (loading) return <div>Loading...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n *\n * return <BoardViewer tree={tree} />;\n * }\n * ```\n */\nexport function useAACFile(\n url: string,\n options?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (options?.enabled === false) {\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFileFromURL(url, options?.processorOptions);\n setTree(loadedTree);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [url, options]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to load an AAC file and calculate metrics\n *\n * @param url - URL to the AAC file\n * @param metricsOptions - Metrics calculation options\n * @returns Object with tree, metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer() {\n * const { tree, metrics, loading, error } = useAACFileWithMetrics(\n * '/files/board.sps',\n * { accessMethod: 'direct' }\n * );\n *\n * if (loading) return <div>Loading...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n *\n * return <BoardViewer tree={tree} buttonMetrics={metrics} />;\n * }\n * ```\n */\nexport function useAACFileWithMetrics(\n url: string,\n metricsOptions?: MetricsOptions,\n fileOptions?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [metrics, setMetrics] = useState<ButtonMetric[] | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (fileOptions?.enabled === false) {\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFileFromURL(url, fileOptions?.processorOptions);\n setTree(loadedTree);\n\n // Calculate metrics\n const calculatedMetrics = await calculateMetrics(loadedTree, metricsOptions || {});\n setMetrics(calculatedMetrics);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [url, metricsOptions, fileOptions]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n metrics,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to calculate metrics for a tree\n *\n * @param tree - The AAC tree\n * @param options - Metrics calculation options\n * @returns Object with metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer({ tree }) {\n * const { metrics, loading } = useMetrics(tree, {\n * accessMethod: 'scanning',\n * scanningConfig: { pattern: 'row-column' }\n * });\n *\n * return <BoardViewer tree={tree} buttonMetrics={metrics} />;\n * }\n * ```\n */\nexport function useMetrics(\n tree: AACTree | null,\n options?: MetricsOptions\n) {\n const [metrics, setMetrics] = useState<ButtonMetric[] | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const calculate = useCallback(async () => {\n if (!tree) {\n setMetrics(null);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const calculatedMetrics = await calculateMetrics(tree, options || {});\n setMetrics(calculatedMetrics);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to calculate metrics'));\n } finally {\n setLoading(false);\n }\n }, [tree, options]);\n\n useEffect(() => {\n calculate();\n }, [calculate]);\n\n return {\n metrics,\n loading,\n error,\n recalculate: calculate,\n };\n}\n\n/**\n * Hook for sentence building state\n *\n * @returns Object with message state and handlers\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { message, wordCount, effort, addWord, clear } = useSentenceBuilder();\n *\n * return (\n * <div>\n * <p>{message || 'Start building...'}</p>\n * <p>{wordCount} words, {effort.toFixed(2)} effort</p>\n * <button onClick={clear}>Clear</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useSentenceBuilder() {\n const [message, setMessage] = useState('');\n const [wordCount, setWordCount] = useState(0);\n const [effort, setEffort] = useState(0);\n\n const addWord = useCallback((word: string, wordEffort: number = 1) => {\n setMessage((prev) => {\n const newMessage = prev + (prev ? ' ' : '') + word;\n setWordCount((prev) => prev + 1);\n setEffort((prev) => prev + wordEffort);\n return newMessage;\n });\n }, []);\n\n const clear = useCallback(() => {\n setMessage('');\n setWordCount(0);\n setEffort(0);\n }, []);\n\n return {\n message,\n wordCount,\n effort,\n addWord,\n clear,\n };\n}\n","/**\n * AAC File Loading Utilities\n *\n * Provides utilities for loading AAC files from various sources\n * (URLs, File objects, file paths) in both client and server contexts.\n */\n\nimport type { AACTree } from '@willwade/aac-processors';\nimport type { LoadAACFileResult } from '../types';\n\ntype ProcessorOptions = Record<string, unknown> | undefined;\n\ntype ProcessorModule = typeof import('@willwade/aac-processors');\n\n// Lazily load processors so browser bundles avoid pulling in Node APIs\nasync function importProcessors(): Promise<ProcessorModule> {\n return import('@willwade/aac-processors');\n}\n\n/**\n * Get the appropriate processor for a file based on its extension\n */\nasync function getProcessorForFile(filepath: string, options?: ProcessorOptions) {\n const {\n getProcessor,\n GridsetProcessor,\n SnapProcessor,\n TouchChatProcessor,\n ObfProcessor,\n ApplePanelsProcessor,\n AstericsGridProcessor,\n OpmlProcessor,\n ExcelProcessor,\n DotProcessor,\n } = await importProcessors();\n\n const ext = filepath.toLowerCase();\n\n // GridSet files (.gridset)\n if (ext.endsWith('.gridset')) {\n return new GridsetProcessor();\n }\n\n // SNAP files (.sps, .spb)\n if (ext.endsWith('.sps') || ext.endsWith('.spb')) {\n return options ? new SnapProcessor(null, options) : new SnapProcessor();\n }\n\n // TouchChat files (.ce)\n if (ext.endsWith('.ce')) {\n return new TouchChatProcessor();\n }\n\n // OpenBoard files (.obf, .obz)\n if (ext.endsWith('.obf')) {\n return new ObfProcessor();\n }\n if (ext.endsWith('.obz')) {\n return new ObfProcessor();\n }\n\n // Asterics Grid files (.grd)\n if (ext.endsWith('.grd')) {\n return new AstericsGridProcessor();\n }\n\n // Apple Panels files\n if (ext.endsWith('.plist')) {\n return new ApplePanelsProcessor();\n }\n\n // OPML files\n if (ext.endsWith('.opml')) {\n return new OpmlProcessor();\n }\n\n // Excel files\n if (ext.endsWith('.xlsx') || ext.endsWith('.xls')) {\n return new ExcelProcessor();\n }\n\n // DOT files\n if (ext.endsWith('.dot')) {\n return new DotProcessor();\n }\n\n // Fallback to generic processor detection\n return getProcessor(filepath);\n}\n\n/**\n * Load an AAC file from a file path (server-side only)\n *\n * @param filepath - Path to the AAC file\n * @param options - Processor options (e.g., pageLayoutPreference for SNAP)\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const tree = await loadAACFile('/path/to/file.sps');\n * ```\n */\nexport async function loadAACFile(\n filepath: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const processor = await getProcessorForFile(filepath, options);\n return processor.loadIntoTree(filepath);\n}\n\n/**\n * Load an AAC file and return extended result with format info\n *\n * @param filepath - Path to the AAC file\n * @param options - Processor options\n * @returns Promise resolving to LoadAACFileResult\n */\nexport async function loadAACFileWithMetadata(\n filepath: string,\n options?: ProcessorOptions\n): Promise<LoadAACFileResult> {\n const tree = await loadAACFile(filepath, options);\n\n // Detect format from file extension\n const ext = filepath.toLowerCase();\n let format = 'unknown';\n\n if (ext.endsWith('.gridset')) format = 'gridset';\n else if (ext.endsWith('.sps') || ext.endsWith('.spb')) format = 'snap';\n else if (ext.endsWith('.ce')) format = 'touchchat';\n else if (ext.endsWith('.obf')) format = 'openboard';\n else if (ext.endsWith('.obz')) format = 'openboard';\n else if (ext.endsWith('.grd')) format = 'asterics-grid';\n else if (ext.endsWith('.plist')) format = 'apple-panels';\n else if (ext.endsWith('.opml')) format = 'opml';\n else if (ext.endsWith('.xlsx') || ext.endsWith('.xls')) format = 'excel';\n else if (ext.endsWith('.dot')) format = 'dot';\n\n return {\n tree,\n format,\n metadata: tree.metadata,\n };\n}\n\n/**\n * Load an AAC file from a URL (client-side)\n *\n * Note: This requires the server to provide the file with appropriate CORS headers.\n * For better performance, consider server-side loading instead.\n *\n * @param url - URL to the AAC file\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const tree = await loadAACFileFromURL('https://example.com/file.sps');\n * ```\n */\nexport async function loadAACFileFromURL(\n url: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`Failed to load file: ${response.statusText}`);\n }\n\n const blob = await response.blob();\n const filename = getFilenameFromURL(url);\n\n return loadAACFileFromFile(blob, filename, options);\n}\n\n/**\n * Load an AAC file from a File object (client-side file input)\n *\n * @param file - File object from file input\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const input = document.querySelector('input[type=\"file\"]');\n * input.onchange = async (e) => {\n * const file = e.target.files[0];\n * const tree = await loadAACFileFromFile(file);\n * // Use tree...\n * };\n * ```\n */\nexport async function loadAACFileFromFile(\n _file: File | Blob,\n _filename?: string,\n _options?: unknown\n): Promise<AACTree> {\n throw new Error('Client-side file loading not yet fully implemented. Please use server-side loading or loadAACFileFromURL with proper CORS headers.');\n}\n\n/**\n * Extract filename from URL\n */\nfunction getFilenameFromURL(url: string): string {\n try {\n const urlObj = new URL(url);\n const pathname = urlObj.pathname;\n const parts = pathname.split('/');\n return parts[parts.length - 1] || 'unknown';\n } catch {\n return 'unknown';\n }\n}\n\n/**\n * Calculate cognitive effort metrics for an AAC tree\n *\n * @param tree - The AAC tree\n * @param options - Metrics calculation options\n * @returns Promise resolving to array of ButtonMetrics\n *\n * @example\n * ```ts\n * import { calculateMetrics } from 'aac-board-viewer';\n *\n * const metrics = await calculateMetrics(tree, {\n * accessMethod: 'direct',\n * });\n * ```\n */\nexport async function calculateMetrics(\n tree: AACTree,\n options: {\n accessMethod?: 'direct' | 'scanning';\n scanningConfig?: {\n pattern?: 'linear' | 'row-column' | 'block';\n selectionMethod?: string;\n errorCorrection?: boolean;\n };\n } = {}\n) {\n // Import MetricsCalculator dynamically to avoid circular dependencies\n const { MetricsCalculator } = await import('@willwade/aac-processors');\n\n const calculator = new MetricsCalculator();\n\n let metricsOptions: Record<string, unknown> = {};\n\n if (options.accessMethod === 'scanning' && options.scanningConfig) {\n // Import scanning enums\n const { CellScanningOrder, ScanningSelectionMethod } = await import('@willwade/aac-processors');\n\n let cellScanningOrder = CellScanningOrder.SimpleScan;\n let blockScanEnabled = false;\n\n switch (options.scanningConfig.pattern) {\n case 'linear':\n cellScanningOrder = CellScanningOrder.SimpleScan;\n break;\n case 'row-column':\n cellScanningOrder = CellScanningOrder.RowColumnScan;\n break;\n case 'block':\n cellScanningOrder = CellScanningOrder.RowColumnScan;\n blockScanEnabled = true;\n break;\n }\n\n metricsOptions = {\n scanningConfig: {\n cellScanningOrder,\n blockScanEnabled,\n selectionMethod: ScanningSelectionMethod.AutoScan,\n errorCorrectionEnabled: options.scanningConfig.errorCorrection || false,\n errorRate: 0.1,\n },\n };\n }\n\n const metricsResult = calculator.analyze(tree, metricsOptions);\n\n // Convert to the format expected by BoardViewer\n type MetricsButton = {\n id: string;\n label: string;\n effort: number;\n count?: number;\n level?: number;\n semantic_id?: string;\n clone_id?: string;\n };\n\n return metricsResult.buttons.map((btn: MetricsButton) => ({\n id: btn.id,\n label: btn.label,\n effort: btn.effort,\n count: btn.count ?? 0,\n is_word: true,\n level: btn.level,\n semantic_id: btn.semantic_id,\n clone_id: btn.clone_id,\n }));\n}\n\n/**\n * Get a list of supported file formats\n *\n * @returns Array of format information\n */\nexport function getSupportedFormats(): Array<{\n name: string;\n extensions: string[];\n description: string;\n}> {\n return [\n {\n name: 'Grid 3',\n extensions: ['.gridset'],\n description: 'Smartbox Grid 3 communication boards',\n },\n {\n name: 'TD Snap',\n extensions: ['.sps', '.spb'],\n description: 'Tobii Dynavox Snap files',\n },\n {\n name: 'TouchChat',\n extensions: ['.ce'],\n description: 'Saltillo TouchChat files',\n },\n {\n name: 'OpenBoard',\n extensions: ['.obf', '.obz'],\n description: 'OpenBoard Format (OBZ/OBF)',\n },\n {\n name: 'Asterics Grid',\n extensions: ['.grd'],\n description: 'Asterics Grid files (.grd)',\n },\n {\n name: 'Apple Panels',\n extensions: ['.plist'],\n description: 'Apple iOS Panels files',\n },\n {\n name: 'OPML',\n extensions: ['.opml'],\n description: 'OPML outline files',\n },\n {\n name: 'Excel',\n extensions: ['.xlsx', '.xls'],\n description: 'Excel spreadsheet boards',\n },\n {\n name: 'DOT',\n extensions: ['.dot'],\n description: 'DOT graph visualization files',\n },\n ];\n}\n"],"mappings":"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","names":["React","import_react","prev"]}
1
+ {"version":3,"sources":["../src/index.ts","../src/components/BoardViewer.tsx","../src/hooks/useAACFile.ts","../src/utils/loaders.ts"],"sourcesContent":["/**\n * AAC Board Viewer\n *\n * Universal AAC board viewer component for React\n */\n\n// Main component\nexport { BoardViewer } from './components/BoardViewer';\n\n// Hooks\nexport {\n useAACFile,\n useAACFileWithMetrics,\n useMetrics,\n useSentenceBuilder,\n} from './hooks/useAACFile';\n\n// Utilities\nexport {\n loadAACFile,\n loadAACFileFromURL,\n loadAACFileFromFile,\n loadAACFileWithMetadata,\n calculateMetrics,\n getSupportedFormats,\n} from './utils/loaders';\n\n// Types\nexport type {\n BoardViewerProps,\n ButtonMetric,\n LoadAACFileResult,\n MetricsOptions,\n} from './types';\n\n// Re-export AAC processor types\nexport type {\n AACTree,\n AACPage,\n AACButton,\n AACSemanticAction,\n AACSemanticCategory,\n AACSemanticIntent,\n} from '@willwade/aac-processors';\n","import React, { useState, useMemo, useCallback } from 'react';\nimport type {\n AACPage,\n AACButton,\n} from '@willwade/aac-processors';\nimport type { BoardViewerProps, ButtonMetric } from '../types';\n\n/**\n * Predictions Tooltip Component\n *\n * Shows a tooltip with predicted word forms when clicking the predictions indicator\n */\ninterface PredictionsTooltipProps {\n predictions: string[];\n label: string;\n position: { x: number; y: number };\n onClose: () => void;\n}\n\nfunction PredictionsTooltip({ predictions, label, position, onClose }: PredictionsTooltipProps) {\n // Close tooltip when clicking outside\n React.useEffect(() => {\n const handleClickOutside = (e: MouseEvent) => {\n if (e.target instanceof HTMLElement && !e.target.closest('.predictions-tooltip')) {\n onClose();\n }\n };\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, [onClose]);\n\n return (\n <div\n 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\"\n style={{\n left: `${Math.min(position.x, window.innerWidth - 200)}px`,\n top: `${Math.min(position.y, window.innerHeight - 150)}px`,\n }}\n >\n <div className=\"flex items-center justify-between mb-2\">\n <h4 className=\"text-sm font-semibold text-gray-900 dark:text-white\">\n Word forms for &quot;{label}&quot;\n </h4>\n <button\n onClick={onClose}\n className=\"p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded\"\n aria-label=\"Close\"\n >\n <svg className=\"h-4 w-4 text-gray-600 dark:text-gray-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n <div className=\"flex flex-wrap gap-1\">\n {predictions.map((word, idx) => (\n <span\n key={idx}\n className=\"px-2 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded text-xs font-medium\"\n >\n {word}\n </span>\n ))}\n </div>\n </div>\n );\n}\n\n/**\n * AAC Board Viewer Component\n *\n * Displays AAC boards with interactive navigation, sentence building,\n * and optional effort metrics.\n *\n * @param props - BoardViewerProps\n */\nexport function BoardViewer({\n tree,\n buttonMetrics,\n showMessageBar = true,\n showEffortBadges = true,\n showLinkIndicators = true,\n initialPageId,\n onButtonClick,\n onPageChange,\n className = '',\n}: BoardViewerProps) {\n const resolveInitialPageId = useCallback(() => {\n // Explicit override\n if (initialPageId && tree.pages[initialPageId]) {\n return initialPageId;\n }\n // If toolbar exists and rootId is different, use rootId\n if (tree.toolbarId && tree.rootId && tree.rootId !== tree.toolbarId) {\n return tree.rootId;\n }\n // If rootId exists and is not a toolbar, use it\n if (tree.rootId && tree.pages[tree.rootId]) {\n const rootPage = tree.pages[tree.rootId];\n const isToolbar =\n rootPage.name.toLowerCase().includes('toolbar') ||\n rootPage.name.toLowerCase().includes('tool bar');\n if (!isToolbar) {\n return tree.rootId;\n }\n }\n // Explicit Start page fallback if present\n const startPage = Object.values(tree.pages).find(\n (p) => p.name.toLowerCase() === 'start'\n );\n if (startPage) {\n return startPage.id;\n }\n // Fall back to first page that's not a toolbar\n const nonToolbarPage = Object.values(tree.pages).find(\n (p) => !p.name.toLowerCase().includes('toolbar') && !p.name.toLowerCase().includes('tool bar')\n );\n if (nonToolbarPage) {\n return nonToolbarPage.id;\n }\n // Last resort: first page\n const pageIds = Object.keys(tree.pages);\n return pageIds.length > 0 ? pageIds[0] : null;\n }, [initialPageId, tree]);\n\n // Determine which page to show\n const [currentPageId, setCurrentPageId] = useState<string | null>(() => resolveInitialPageId());\n\n const [pageHistory, setPageHistory] = useState<AACPage[]>([]);\n const [message, setMessage] = useState('');\n const [currentWordCount, setCurrentWordCount] = useState(0);\n const [currentEffort, setCurrentEffort] = useState(0);\n\n // Predictions tooltip state\n const [predictionsTooltip, setPredictionsTooltip] = useState<{\n predictions: string[];\n label: string;\n position: { x: number; y: number };\n } | null>(null);\n\n // Convert button metrics array to lookup object for easy access\n const buttonMetricsLookup = useMemo(() => {\n if (!buttonMetrics) return {};\n const lookup: { [buttonId: string]: ButtonMetric } = {};\n buttonMetrics.forEach((metric) => {\n lookup[metric.id] = metric;\n });\n return lookup;\n }, [buttonMetrics]);\n\n const currentPage = currentPageId ? tree.pages[currentPageId] : null;\n const goToPage = (targetPageId: string | undefined | null) => {\n if (!targetPageId || !tree.pages[targetPageId]) return false;\n if (currentPage) {\n setPageHistory((prev) => [...prev, currentPage]);\n }\n setCurrentPageId(targetPageId);\n if (onPageChange) {\n onPageChange(targetPageId);\n }\n return true;\n };\n\n // Sync when tree or initialPageId changes\n React.useEffect(() => {\n setCurrentPageId(resolveInitialPageId());\n setPageHistory([]);\n }, [resolveInitialPageId]);\n\n // Calculate total stats for current word\n const updateStats = (word: string, effort: number) => {\n setCurrentWordCount((prev) => prev + 1);\n setCurrentEffort((prev) => prev + effort);\n };\n\n const handleButtonClick = (button: AACButton) => {\n // Call external callback if provided\n if (onButtonClick) {\n onButtonClick(button);\n }\n\n const intent = button.semanticAction?.intent\n ? String(button.semanticAction.intent)\n : undefined;\n const targetPageId = button.targetPageId || button.semanticAction?.targetId;\n const effort = buttonMetricsLookup[button.id]?.effort || 1;\n const textValue =\n button.semanticAction?.text || button.message || button.label || '';\n\n const deleteLastWord = () => {\n setMessage((prev) => {\n const parts = prev.trim().split(/\\s+/);\n parts.pop();\n const newMsg = parts.join(' ');\n return newMsg;\n });\n };\n\n const deleteLastCharacter = () => {\n setMessage((prev) => prev.slice(0, -1));\n };\n\n const appendText = (word: string) => {\n const trimmed = word || button.label || '';\n setMessage((prev) => {\n const newMessage = trimmed\n ? prev + (prev ? ' ' : '') + trimmed\n : prev;\n if (trimmed) {\n updateStats(trimmed, effort);\n }\n return newMessage;\n });\n };\n\n // Navigation takes precedence\n if (intent === 'NAVIGATE_TO' && goToPage(targetPageId)) {\n return;\n }\n\n switch (intent) {\n case 'GO_BACK':\n handleBack();\n return;\n case 'GO_HOME':\n if (tree.rootId && goToPage(tree.rootId)) return;\n break;\n case 'DELETE_WORD':\n deleteLastWord();\n return;\n case 'DELETE_CHARACTER':\n deleteLastCharacter();\n return;\n case 'CLEAR_TEXT':\n clearMessage();\n return;\n case 'SPEAK_IMMEDIATE':\n case 'SPEAK_TEXT':\n case 'INSERT_TEXT':\n appendText(textValue);\n return;\n default:\n break;\n }\n\n // Fallback navigation if intent not set but target exists\n if (targetPageId && goToPage(targetPageId)) {\n return;\n }\n\n // Otherwise add to message\n appendText(textValue);\n };\n\n const handleBack = () => {\n if (pageHistory.length > 0) {\n const previousPage = pageHistory[pageHistory.length - 1];\n setPageHistory((prev) => prev.slice(0, -1));\n setCurrentPageId(previousPage.id);\n if (onPageChange) {\n onPageChange(previousPage.id);\n }\n }\n };\n\n const clearMessage = () => {\n setMessage('');\n setCurrentWordCount(0);\n setCurrentEffort(0);\n };\n\n const handleShowPredictions = (\n button: AACButton,\n event: React.MouseEvent<HTMLDivElement>\n ) => {\n event.stopPropagation(); // Prevent button click\n const predictions = button.predictions || (button.parameters as { predictions?: string[] })?.predictions;\n if (predictions && predictions.length > 0) {\n setPredictionsTooltip({\n predictions,\n label: button.label,\n position: { x: event.clientX, y: event.clientY },\n });\n }\n };\n\n const getTextColor = (backgroundColor?: string) => {\n if (!backgroundColor) return 'text-gray-900 dark:text-gray-100';\n\n // Convert hex to rgb for brightness calculation\n const hex = backgroundColor.replace('#', '');\n if (hex.length === 6) {\n const r = parseInt(hex.substring(0, 2), 16);\n const g = parseInt(hex.substring(2, 4), 16);\n const b = parseInt(hex.substring(4, 6), 16);\n const brightness = (r * 299 + g * 587 + b * 114) / 1000;\n return brightness >= 128 ? 'text-gray-900' : 'text-white';\n }\n\n return 'text-gray-900 dark:text-gray-100';\n };\n\n if (!currentPage) {\n return (\n <div className={`flex items-center justify-center h-96 bg-gray-100 dark:bg-gray-800 rounded-lg ${className}`}>\n <div className=\"text-center\">\n <p className=\"text-gray-600 dark:text-gray-400\">No pages available</p>\n </div>\n </div>\n );\n }\n\n // Get toolbar page if it exists\n const toolbarPage = tree.toolbarId ? tree.pages[tree.toolbarId] : null;\n\n // Get grid dimensions\n const gridRows = currentPage.grid.length;\n const gridCols = gridRows > 0 ? currentPage.grid[0].length : 0;\n\n return (\n <div className={`bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden ${className}`}>\n {/* Message Bar */}\n {showMessageBar && (\n <div className=\"p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50\">\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex-1 min-w-0\">\n {message ? (\n <div className=\"space-y-2\">\n <p className=\"text-lg text-gray-900 dark:text-white break-words\">{message}</p>\n <div className=\"flex gap-4 text-sm\">\n <div className=\"text-gray-600 dark:text-gray-400\">\n {currentWordCount} {currentWordCount === 1 ? 'word' : 'words'}\n </div>\n {buttonMetrics && (\n <>\n <div className=\"text-gray-600 dark:text-gray-400\">\n Effort: <span className=\"font-medium\">{currentEffort.toFixed(2)}</span>\n </div>\n <div className=\"text-gray-600 dark:text-gray-400\">\n Avg:{' '}\n <span className=\"font-medium\">\n {currentWordCount > 0\n ? (currentEffort / currentWordCount).toFixed(2)\n : '0.00'}\n </span>\n </div>\n </>\n )}\n </div>\n </div>\n ) : (\n <p className=\"text-gray-500 dark:text-gray-400 italic\">\n Tap buttons to build a sentence...\n </p>\n )}\n </div>\n {message && (\n <button\n onClick={clearMessage}\n className=\"p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition\"\n aria-label=\"Clear message\"\n >\n <svg\n className=\"h-5 w-5 text-gray-600 dark:text-gray-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M6 18L18 6M6 6l12 12\"\n />\n </svg>\n </button>\n )}\n </div>\n </div>\n )}\n\n {/* Main Content Area */}\n <div className=\"flex\">\n {/* Toolbar Sidebar (if exists) */}\n {toolbarPage && (\n <div className=\"w-16 sm:w-20 border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50\">\n <div className=\"p-2\">\n <p className=\"text-[10px] text-gray-500 dark:text-gray-400 text-center mb-2 font-semibold\">\n TOOLBAR\n </p>\n <div className=\"grid gap-1\">\n {toolbarPage.grid.map((row, _rowIndex) =>\n row.map((button, _colIndex) => {\n if (!button) return null;\n\n const buttonMetric = buttonMetricsLookup[button.id];\n const effort = buttonMetric?.effort || 0;\n\n return (\n <button\n key={button.id}\n onClick={() => handleButtonClick(button)}\n className=\"aspect-square p-1 rounded border border-gray-200 dark:border-gray-700 transition flex flex-col items-center justify-center gap-0.5 hover:bg-gray-200 dark:hover:bg-gray-700 relative\"\n style={{\n backgroundColor: button.style?.backgroundColor || '#f3f4f6',\n borderColor: button.style?.borderColor || '#e5e7eb',\n }}\n title={`${button.label}\\n${button.message || ''}`}\n >\n {buttonMetric && showEffortBadges && effort > 0 && (\n <div className=\"absolute top-0 right-0 px-0.5 py-0 text-[8px] font-semibold rounded bg-blue-600 text-white\">\n {effort.toFixed(1)}\n </div>\n )}\n <span\n className={`text-[8px] sm:text-[9px] text-center font-medium leading-tight line-clamp-2 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.label}\n </span>\n </button>\n );\n })\n )}\n </div>\n </div>\n </div>\n )}\n\n {/* Main Page Content */}\n <div className=\"flex-1\">\n {/* Header */}\n <div className=\"flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700\">\n <div className=\"flex items-center gap-2\">\n {pageHistory.length > 0 && (\n <button\n onClick={handleBack}\n className=\"p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition\"\n aria-label=\"Go back\"\n >\n <svg\n className=\"h-5 w-5 text-gray-600 dark:text-gray-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 19l-7-7 7-7\"\n />\n </svg>\n </button>\n )}\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white\">\n {currentPage.name}\n </h3>\n </div>\n <div className=\"text-sm text-gray-500 dark:text-gray-400\">\n {gridRows}×{gridCols} grid\n </div>\n </div>\n\n {/* Grid */}\n <div\n className=\"p-4 gap-2 overflow-auto max-h-[600px]\"\n style={{\n display: 'grid',\n gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`,\n }}\n >\n {(() => {\n const rendered = new Set<string>();\n return currentPage.grid.flatMap((row, rowIndex) =>\n row.map((button, colIndex) => {\n if (!button) {\n return <div key={`empty-${rowIndex}-${colIndex}`} className=\"aspect-square\" />;\n }\n\n if (rendered.has(button.id)) {\n return null;\n }\n rendered.add(button.id);\n\n const buttonMetric = buttonMetricsLookup[button.id];\n const effort = buttonMetric?.effort || 0;\n const targetPageId = button.targetPageId || button.semanticAction?.targetId;\n const hasLink = targetPageId && tree.pages[targetPageId];\n const colSpan = button.columnSpan || 1;\n const rowSpan = button.rowSpan || 1;\n const predictions =\n button.predictions || (button.parameters as { predictions?: string[] })?.predictions;\n const hasPredictions = predictions && predictions.length > 0;\n const isPredictionCell =\n button.contentType === 'AutoContent' &&\n (button.contentSubType || '').toLowerCase() === 'prediction';\n const isWorkspace = button.contentType === 'Workspace';\n\n const imageSrc =\n (button.resolvedImageEntry && !String(button.resolvedImageEntry).startsWith('[')\n ? button.resolvedImageEntry\n : null) ||\n (button.image && !String(button.image).startsWith('[') ? button.image : null);\n\n if (isWorkspace) {\n return (\n <div\n key={button.id}\n className=\"relative p-3 rounded-lg border-2 bg-gray-50 text-gray-800 flex items-center gap-2\"\n style={{\n borderColor: button.style?.borderColor || '#e5e7eb',\n gridColumn: `${colIndex + 1} / span ${colSpan}`,\n gridRow: `${rowIndex + 1} / span ${rowSpan}`,\n }}\n >\n <div className=\"font-semibold text-sm\">{button.label || 'Workspace'}</div>\n <div className=\"text-xs text-gray-500 truncate\">\n {message || 'Chat writing area'}\n </div>\n </div>\n );\n }\n\n return (\n <button\n key={button.id}\n onClick={() => handleButtonClick(button)}\n 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\"\n style={{\n backgroundColor: button.style?.backgroundColor || '#f3f4f6',\n borderColor: button.style?.borderColor || '#e5e7eb',\n color: button.style?.fontColor || undefined,\n gridColumn: `${colIndex + 1} / span ${colSpan}`,\n gridRow: `${rowIndex + 1} / span ${rowSpan}`,\n }}\n >\n {/* Effort Badge */}\n {buttonMetric && showEffortBadges && (\n <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\">\n {effort.toFixed(1)}\n </div>\n )}\n\n {/* Link Indicator */}\n {hasLink && showLinkIndicators && (\n <div className=\"absolute top-1 left-1 w-2 h-2 bg-green-500 rounded-full shadow-sm\" />\n )}\n\n {/* Predictions Indicator */}\n {hasPredictions && (\n <div\n onClick={(e) => handleShowPredictions(button, e)}\n 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\"\n title={`Has ${predictions?.length} word form${predictions && predictions.length > 1 ? 's' : ''}`}\n >\n {predictions?.length}\n </div>\n )}\n\n {/* Image */}\n {imageSrc && (\n <img\n src={imageSrc}\n alt={button.label}\n className=\"max-h-12 object-contain\"\n />\n )}\n\n {/* Label / Predictions */}\n <div className=\"flex flex-col items-center justify-center\">\n <span\n className={`text-xs sm:text-sm text-center font-medium leading-tight line-clamp-3 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.label}\n </span>\n {isPredictionCell && predictions && predictions.length > 0 && (\n <div className=\"mt-1 text-[10px] sm:text-xs text-center opacity-80 space-y-0.5\">\n {predictions.slice(0, 3).map((p, idx) => (\n <div key={`${button.id}-pred-${idx}`}>\n {p}\n </div>\n ))}\n {predictions.length > 3 && <div>…</div>}\n </div>\n )}\n </div>\n\n {/* Message (if different from label) */}\n {button.message && button.message !== button.label && !isPredictionCell && (\n <span\n className={`text-[10px] sm:text-xs text-center opacity-75 line-clamp-2 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.message}\n </span>\n )}\n </button>\n );\n })\n );\n })()}\n </div>\n\n {/* Page Navigation (if multiple pages) */}\n {Object.keys(tree.pages).length > 1 && (\n <div className=\"p-4 border-t border-gray-200 dark:border-gray-700\">\n <p className=\"text-sm text-gray-600 dark:text-gray-400 mb-2\">\n {Object.keys(tree.pages).length} pages in this vocabulary\n </p>\n </div>\n )}\n </div>\n </div>\n\n {/* Predictions Tooltip */}\n {predictionsTooltip && (\n <PredictionsTooltip\n predictions={predictionsTooltip.predictions}\n label={predictionsTooltip.label}\n position={predictionsTooltip.position}\n onClose={() => setPredictionsTooltip(null)}\n />\n )}\n </div>\n );\n}\n","/**\n * React hooks for AAC Board Viewer\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { loadAACFileFromURL, calculateMetrics } from '../utils/loaders';\nimport type { AACTree } from '@willwade/aac-processors';\nimport type { ButtonMetric, MetricsOptions } from '../types';\n\n/**\n * Hook to load an AAC file from a URL\n *\n * @param url - URL to the AAC file\n * @param options - Processor options (e.g., pageLayoutPreference for SNAP)\n * @returns Object with tree, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer() {\n * const { tree, loading, error, reload } = useAACFile('/files/board.sps');\n *\n * if (loading) return <div>Loading...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n *\n * return <BoardViewer tree={tree} />;\n * }\n * ```\n */\nexport function useAACFile(\n url: string,\n options?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (options?.enabled === false) {\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFileFromURL(url, options?.processorOptions);\n setTree(loadedTree);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [url, options]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to load an AAC file and calculate metrics\n *\n * @param url - URL to the AAC file\n * @param metricsOptions - Metrics calculation options\n * @returns Object with tree, metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer() {\n * const { tree, metrics, loading, error } = useAACFileWithMetrics(\n * '/files/board.sps',\n * { accessMethod: 'direct' }\n * );\n *\n * if (loading) return <div>Loading...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n *\n * return <BoardViewer tree={tree} buttonMetrics={metrics} />;\n * }\n * ```\n */\nexport function useAACFileWithMetrics(\n url: string,\n metricsOptions?: MetricsOptions,\n fileOptions?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [metrics, setMetrics] = useState<ButtonMetric[] | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (fileOptions?.enabled === false) {\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFileFromURL(url, fileOptions?.processorOptions);\n setTree(loadedTree);\n\n // Calculate metrics\n const calculatedMetrics = await calculateMetrics(loadedTree, metricsOptions || {});\n setMetrics(calculatedMetrics);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [url, metricsOptions, fileOptions]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n metrics,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to calculate metrics for a tree\n *\n * @param tree - The AAC tree\n * @param options - Metrics calculation options\n * @returns Object with metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer({ tree }) {\n * const { metrics, loading } = useMetrics(tree, {\n * accessMethod: 'scanning',\n * scanningConfig: { pattern: 'row-column' }\n * });\n *\n * return <BoardViewer tree={tree} buttonMetrics={metrics} />;\n * }\n * ```\n */\nexport function useMetrics(\n tree: AACTree | null,\n options?: MetricsOptions\n) {\n const [metrics, setMetrics] = useState<ButtonMetric[] | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const calculate = useCallback(async () => {\n if (!tree) {\n setMetrics(null);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const calculatedMetrics = await calculateMetrics(tree, options || {});\n setMetrics(calculatedMetrics);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to calculate metrics'));\n } finally {\n setLoading(false);\n }\n }, [tree, options]);\n\n useEffect(() => {\n calculate();\n }, [calculate]);\n\n return {\n metrics,\n loading,\n error,\n recalculate: calculate,\n };\n}\n\n/**\n * Hook for sentence building state\n *\n * @returns Object with message state and handlers\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { message, wordCount, effort, addWord, clear } = useSentenceBuilder();\n *\n * return (\n * <div>\n * <p>{message || 'Start building...'}</p>\n * <p>{wordCount} words, {effort.toFixed(2)} effort</p>\n * <button onClick={clear}>Clear</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useSentenceBuilder() {\n const [message, setMessage] = useState('');\n const [wordCount, setWordCount] = useState(0);\n const [effort, setEffort] = useState(0);\n\n const addWord = useCallback((word: string, wordEffort: number = 1) => {\n setMessage((prev) => {\n const newMessage = prev + (prev ? ' ' : '') + word;\n setWordCount((prev) => prev + 1);\n setEffort((prev) => prev + wordEffort);\n return newMessage;\n });\n }, []);\n\n const clear = useCallback(() => {\n setMessage('');\n setWordCount(0);\n setEffort(0);\n }, []);\n\n return {\n message,\n wordCount,\n effort,\n addWord,\n clear,\n };\n}\n","/**\n * AAC File Loading Utilities\n *\n * Provides utilities for loading AAC files from various sources\n * (URLs, File objects, file paths) in both client and server contexts.\n */\n\nimport type { AACTree } from '@willwade/aac-processors';\nimport type { LoadAACFileResult } from '../types';\n\ntype ProcessorOptions = Record<string, unknown> | undefined;\n\ntype ProcessorModule = typeof import('@willwade/aac-processors');\n\n// Lazily load processors so browser bundles avoid pulling in Node APIs\nasync function importProcessors(): Promise<ProcessorModule> {\n return import('@willwade/aac-processors');\n}\n\n/**\n * Get the appropriate processor for a file based on its extension\n */\nasync function getProcessorForFile(filepath: string, options?: ProcessorOptions) {\n const {\n getProcessor,\n GridsetProcessor,\n SnapProcessor,\n TouchChatProcessor,\n ObfProcessor,\n ApplePanelsProcessor,\n AstericsGridProcessor,\n OpmlProcessor,\n ExcelProcessor,\n DotProcessor,\n } = await importProcessors();\n\n const ext = filepath.toLowerCase();\n\n // GridSet files (.gridset)\n if (ext.endsWith('.gridset')) {\n return new GridsetProcessor();\n }\n\n // SNAP files (.sps, .spb)\n if (ext.endsWith('.sps') || ext.endsWith('.spb')) {\n return options ? new SnapProcessor(null, options) : new SnapProcessor();\n }\n\n // TouchChat files (.ce)\n if (ext.endsWith('.ce')) {\n return new TouchChatProcessor();\n }\n\n // OpenBoard files (.obf, .obz)\n if (ext.endsWith('.obf')) {\n return new ObfProcessor();\n }\n if (ext.endsWith('.obz')) {\n return new ObfProcessor();\n }\n\n // Asterics Grid files (.grd)\n if (ext.endsWith('.grd')) {\n return new AstericsGridProcessor();\n }\n\n // Apple Panels files\n if (ext.endsWith('.plist')) {\n return new ApplePanelsProcessor();\n }\n\n // OPML files\n if (ext.endsWith('.opml')) {\n return new OpmlProcessor();\n }\n\n // Excel files\n if (ext.endsWith('.xlsx') || ext.endsWith('.xls')) {\n return new ExcelProcessor();\n }\n\n // DOT files\n if (ext.endsWith('.dot')) {\n return new DotProcessor();\n }\n\n // Fallback to generic processor detection\n return getProcessor(filepath);\n}\n\n/**\n * Load an AAC file from a file path (server-side only)\n *\n * @param filepath - Path to the AAC file\n * @param options - Processor options (e.g., pageLayoutPreference for SNAP)\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const tree = await loadAACFile('/path/to/file.sps');\n * ```\n */\nexport async function loadAACFile(\n filepath: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const processor = await getProcessorForFile(filepath, options);\n return processor.loadIntoTree(filepath);\n}\n\n/**\n * Load an AAC file and return extended result with format info\n *\n * @param filepath - Path to the AAC file\n * @param options - Processor options\n * @returns Promise resolving to LoadAACFileResult\n */\nexport async function loadAACFileWithMetadata(\n filepath: string,\n options?: ProcessorOptions\n): Promise<LoadAACFileResult> {\n const tree = await loadAACFile(filepath, options);\n\n // Detect format from file extension\n const ext = filepath.toLowerCase();\n let format = 'unknown';\n\n if (ext.endsWith('.gridset')) format = 'gridset';\n else if (ext.endsWith('.sps') || ext.endsWith('.spb')) format = 'snap';\n else if (ext.endsWith('.ce')) format = 'touchchat';\n else if (ext.endsWith('.obf')) format = 'openboard';\n else if (ext.endsWith('.obz')) format = 'openboard';\n else if (ext.endsWith('.grd')) format = 'asterics-grid';\n else if (ext.endsWith('.plist')) format = 'apple-panels';\n else if (ext.endsWith('.opml')) format = 'opml';\n else if (ext.endsWith('.xlsx') || ext.endsWith('.xls')) format = 'excel';\n else if (ext.endsWith('.dot')) format = 'dot';\n\n return {\n tree,\n format,\n metadata: tree.metadata,\n };\n}\n\n/**\n * Load an AAC file from a URL (client-side)\n *\n * Note: This requires the server to provide the file with appropriate CORS headers.\n * For better performance, consider server-side loading instead.\n *\n * @param url - URL to the AAC file\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const tree = await loadAACFileFromURL('https://example.com/file.sps');\n * ```\n */\nexport async function loadAACFileFromURL(\n url: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`Failed to load file: ${response.statusText}`);\n }\n\n const blob = await response.blob();\n const filename = getFilenameFromURL(url);\n\n return loadAACFileFromFile(blob, filename, options);\n}\n\n/**\n * Load an AAC file from a File object (client-side file input)\n *\n * @param file - File object from file input\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const input = document.querySelector('input[type=\"file\"]');\n * input.onchange = async (e) => {\n * const file = e.target.files[0];\n * const tree = await loadAACFileFromFile(file);\n * // Use tree...\n * };\n * ```\n */\nexport async function loadAACFileFromFile(\n _file: File | Blob,\n _filename?: string,\n _options?: unknown\n): Promise<AACTree> {\n throw new Error('Client-side file loading not yet fully implemented. Please use server-side loading or loadAACFileFromURL with proper CORS headers.');\n}\n\n/**\n * Extract filename from URL\n */\nfunction getFilenameFromURL(url: string): string {\n try {\n const urlObj = new URL(url);\n const pathname = urlObj.pathname;\n const parts = pathname.split('/');\n return parts[parts.length - 1] || 'unknown';\n } catch {\n return 'unknown';\n }\n}\n\n/**\n * Calculate cognitive effort metrics for an AAC tree\n *\n * @param tree - The AAC tree\n * @param options - Metrics calculation options\n * @returns Promise resolving to array of ButtonMetrics\n *\n * @example\n * ```ts\n * import { calculateMetrics } from 'aac-board-viewer';\n *\n * const metrics = await calculateMetrics(tree, {\n * accessMethod: 'direct',\n * });\n * ```\n */\nexport async function calculateMetrics(\n tree: AACTree,\n options: {\n accessMethod?: 'direct' | 'scanning';\n scanningConfig?: {\n pattern?: 'linear' | 'row-column' | 'block';\n selectionMethod?: string;\n errorCorrection?: boolean;\n };\n } = {}\n) {\n // Import MetricsCalculator dynamically to avoid circular dependencies\n const { MetricsCalculator } = await import('@willwade/aac-processors');\n\n const calculator = new MetricsCalculator();\n\n let metricsOptions: Record<string, unknown> = {};\n\n if (options.accessMethod === 'scanning' && options.scanningConfig) {\n // Import scanning enums\n const { CellScanningOrder, ScanningSelectionMethod } = await import('@willwade/aac-processors');\n\n let cellScanningOrder = CellScanningOrder.SimpleScan;\n let blockScanEnabled = false;\n\n switch (options.scanningConfig.pattern) {\n case 'linear':\n cellScanningOrder = CellScanningOrder.SimpleScan;\n break;\n case 'row-column':\n cellScanningOrder = CellScanningOrder.RowColumnScan;\n break;\n case 'block':\n cellScanningOrder = CellScanningOrder.RowColumnScan;\n blockScanEnabled = true;\n break;\n }\n\n metricsOptions = {\n scanningConfig: {\n cellScanningOrder,\n blockScanEnabled,\n selectionMethod: ScanningSelectionMethod.AutoScan,\n errorCorrectionEnabled: options.scanningConfig.errorCorrection || false,\n errorRate: 0.1,\n },\n };\n }\n\n const metricsResult = calculator.analyze(tree, metricsOptions);\n\n // Convert to the format expected by BoardViewer\n type MetricsButton = {\n id: string;\n label: string;\n effort: number;\n count?: number;\n level?: number;\n semantic_id?: string;\n clone_id?: string;\n };\n\n return metricsResult.buttons.map((btn: MetricsButton) => ({\n id: btn.id,\n label: btn.label,\n effort: btn.effort,\n count: btn.count ?? 0,\n is_word: true,\n level: btn.level,\n semantic_id: btn.semantic_id,\n clone_id: btn.clone_id,\n }));\n}\n\n/**\n * Get a list of supported file formats\n *\n * @returns Array of format information\n */\nexport function getSupportedFormats(): Array<{\n name: string;\n extensions: string[];\n description: string;\n}> {\n return [\n {\n name: 'Grid 3',\n extensions: ['.gridset'],\n description: 'Smartbox Grid 3 communication boards',\n },\n {\n name: 'TD Snap',\n extensions: ['.sps', '.spb'],\n description: 'Tobii Dynavox Snap files',\n },\n {\n name: 'TouchChat',\n extensions: ['.ce'],\n description: 'Saltillo TouchChat files',\n },\n {\n name: 'OpenBoard',\n extensions: ['.obf', '.obz'],\n description: 'OpenBoard Format (OBZ/OBF)',\n },\n {\n name: 'Asterics Grid',\n extensions: ['.grd'],\n description: 'Asterics Grid files (.grd)',\n },\n {\n name: 'Apple Panels',\n extensions: ['.plist'],\n description: 'Apple iOS Panels files',\n },\n {\n name: 'OPML',\n extensions: ['.opml'],\n description: 'OPML outline files',\n },\n {\n name: 'Excel',\n extensions: ['.xlsx', '.xls'],\n description: 'Excel spreadsheet boards',\n },\n {\n name: 'DOT',\n extensions: ['.dot'],\n description: 'DOT graph visualization files',\n },\n ];\n}\n"],"mappings":"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","names":["React","import_react","prev"]}
package/dist/index.mjs CHANGED
@@ -1,6 +1,53 @@
1
1
  // src/components/BoardViewer.tsx
2
2
  import React, { useState, useMemo, useCallback } from "react";
3
3
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
4
+ function PredictionsTooltip({ predictions, label, position, onClose }) {
5
+ React.useEffect(() => {
6
+ const handleClickOutside = (e) => {
7
+ if (e.target instanceof HTMLElement && !e.target.closest(".predictions-tooltip")) {
8
+ onClose();
9
+ }
10
+ };
11
+ document.addEventListener("mousedown", handleClickOutside);
12
+ return () => document.removeEventListener("mousedown", handleClickOutside);
13
+ }, [onClose]);
14
+ return /* @__PURE__ */ jsxs(
15
+ "div",
16
+ {
17
+ 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",
18
+ style: {
19
+ left: `${Math.min(position.x, window.innerWidth - 200)}px`,
20
+ top: `${Math.min(position.y, window.innerHeight - 150)}px`
21
+ },
22
+ children: [
23
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
24
+ /* @__PURE__ */ jsxs("h4", { className: "text-sm font-semibold text-gray-900 dark:text-white", children: [
25
+ 'Word forms for "',
26
+ label,
27
+ '"'
28
+ ] }),
29
+ /* @__PURE__ */ jsx(
30
+ "button",
31
+ {
32
+ onClick: onClose,
33
+ className: "p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded",
34
+ "aria-label": "Close",
35
+ children: /* @__PURE__ */ 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__ */ jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) })
36
+ }
37
+ )
38
+ ] }),
39
+ /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-1", children: predictions.map((word, idx) => /* @__PURE__ */ jsx(
40
+ "span",
41
+ {
42
+ className: "px-2 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded text-xs font-medium",
43
+ children: word
44
+ },
45
+ idx
46
+ )) })
47
+ ]
48
+ }
49
+ );
50
+ }
4
51
  function BoardViewer({
5
52
  tree,
6
53
  buttonMetrics,
@@ -26,6 +73,12 @@ function BoardViewer({
26
73
  return tree.rootId;
27
74
  }
28
75
  }
76
+ const startPage = Object.values(tree.pages).find(
77
+ (p) => p.name.toLowerCase() === "start"
78
+ );
79
+ if (startPage) {
80
+ return startPage.id;
81
+ }
29
82
  const nonToolbarPage = Object.values(tree.pages).find(
30
83
  (p) => !p.name.toLowerCase().includes("toolbar") && !p.name.toLowerCase().includes("tool bar")
31
84
  );
@@ -40,6 +93,7 @@ function BoardViewer({
40
93
  const [message, setMessage] = useState("");
41
94
  const [currentWordCount, setCurrentWordCount] = useState(0);
42
95
  const [currentEffort, setCurrentEffort] = useState(0);
96
+ const [predictionsTooltip, setPredictionsTooltip] = useState(null);
43
97
  const buttonMetricsLookup = useMemo(() => {
44
98
  if (!buttonMetrics) return {};
45
99
  const lookup = {};
@@ -49,6 +103,17 @@ function BoardViewer({
49
103
  return lookup;
50
104
  }, [buttonMetrics]);
51
105
  const currentPage = currentPageId ? tree.pages[currentPageId] : null;
106
+ const goToPage = (targetPageId) => {
107
+ if (!targetPageId || !tree.pages[targetPageId]) return false;
108
+ if (currentPage) {
109
+ setPageHistory((prev) => [...prev, currentPage]);
110
+ }
111
+ setCurrentPageId(targetPageId);
112
+ if (onPageChange) {
113
+ onPageChange(targetPageId);
114
+ }
115
+ return true;
116
+ };
52
117
  React.useEffect(() => {
53
118
  setCurrentPageId(resolveInitialPageId());
54
119
  setPageHistory([]);
@@ -61,24 +126,62 @@ function BoardViewer({
61
126
  if (onButtonClick) {
62
127
  onButtonClick(button);
63
128
  }
129
+ const intent = button.semanticAction?.intent ? String(button.semanticAction.intent) : void 0;
64
130
  const targetPageId = button.targetPageId || button.semanticAction?.targetId;
65
- if (targetPageId && tree.pages[targetPageId]) {
66
- if (currentPage) {
67
- setPageHistory((prev) => [...prev, currentPage]);
68
- }
69
- setCurrentPageId(targetPageId);
70
- if (onPageChange) {
71
- onPageChange(targetPageId);
72
- }
131
+ const effort = buttonMetricsLookup[button.id]?.effort || 1;
132
+ const textValue = button.semanticAction?.text || button.message || button.label || "";
133
+ const deleteLastWord = () => {
134
+ setMessage((prev) => {
135
+ const parts = prev.trim().split(/\s+/);
136
+ parts.pop();
137
+ const newMsg = parts.join(" ");
138
+ return newMsg;
139
+ });
140
+ };
141
+ const deleteLastCharacter = () => {
142
+ setMessage((prev) => prev.slice(0, -1));
143
+ };
144
+ const appendText = (word) => {
145
+ const trimmed = word || button.label || "";
146
+ setMessage((prev) => {
147
+ const newMessage = trimmed ? prev + (prev ? " " : "") + trimmed : prev;
148
+ if (trimmed) {
149
+ updateStats(trimmed, effort);
150
+ }
151
+ return newMessage;
152
+ });
153
+ };
154
+ if (intent === "NAVIGATE_TO" && goToPage(targetPageId)) {
73
155
  return;
74
156
  }
75
- const word = button.message || button.label;
76
- const effort = buttonMetricsLookup[button.id]?.effort || 1;
77
- setMessage((prev) => {
78
- const newMessage = prev + (prev ? " " : "") + word;
79
- updateStats(word, effort);
80
- return newMessage;
81
- });
157
+ switch (intent) {
158
+ case "GO_BACK":
159
+ handleBack();
160
+ return;
161
+ case "GO_HOME":
162
+ if (tree.rootId && goToPage(tree.rootId)) return;
163
+ break;
164
+ case "DELETE_WORD":
165
+ deleteLastWord();
166
+ return;
167
+ case "DELETE_CHARACTER":
168
+ deleteLastCharacter();
169
+ return;
170
+ case "CLEAR_TEXT":
171
+ clearMessage();
172
+ return;
173
+ case "SPEAK_IMMEDIATE":
174
+ case "SPEAK_TEXT":
175
+ case "INSERT_TEXT":
176
+ appendText(textValue);
177
+ return;
178
+ default:
179
+ break;
180
+ }
181
+ if (targetPageId && goToPage(targetPageId)) {
182
+ return;
183
+ }
184
+ appendText(textValue);
82
185
  };
83
186
  const handleBack = () => {
84
187
  if (pageHistory.length > 0) {
@@ -95,6 +198,17 @@ function BoardViewer({
95
198
  setCurrentWordCount(0);
96
199
  setCurrentEffort(0);
97
200
  };
201
+ const handleShowPredictions = (button, event) => {
202
+ event.stopPropagation();
203
+ const predictions = button.predictions || button.parameters?.predictions;
204
+ if (predictions && predictions.length > 0) {
205
+ setPredictionsTooltip({
206
+ predictions,
207
+ label: button.label,
208
+ position: { x: event.clientX, y: event.clientY }
209
+ });
210
+ }
211
+ };
98
212
  const getTextColor = (backgroundColor) => {
99
213
  if (!backgroundColor) return "text-gray-900 dark:text-gray-100";
100
214
  const hex = backgroundColor.replace("#", "");
@@ -246,52 +360,109 @@ ${button.message || ""}`,
246
360
  display: "grid",
247
361
  gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`
248
362
  },
249
- children: currentPage.grid.map(
250
- (row, rowIndex) => row.map((button, colIndex) => {
251
- if (!button) {
252
- return /* @__PURE__ */ jsx("div", { className: "aspect-square" }, `empty-${rowIndex}-${colIndex}`);
253
- }
254
- const buttonMetric = buttonMetricsLookup[button.id];
255
- const effort = buttonMetric?.effort || 0;
256
- const targetPageId = button.targetPageId || button.semanticAction?.targetId;
257
- const hasLink = targetPageId && tree.pages[targetPageId];
258
- return /* @__PURE__ */ jsxs(
259
- "button",
260
- {
261
- onClick: () => handleButtonClick(button),
262
- 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",
263
- style: {
264
- backgroundColor: button.style?.backgroundColor || "#f3f4f6",
265
- borderColor: button.style?.borderColor || "#e5e7eb",
266
- color: button.style?.fontColor || void 0
363
+ children: (() => {
364
+ const rendered = /* @__PURE__ */ new Set();
365
+ return currentPage.grid.flatMap(
366
+ (row, rowIndex) => row.map((button, colIndex) => {
367
+ if (!button) {
368
+ return /* @__PURE__ */ jsx("div", { className: "aspect-square" }, `empty-${rowIndex}-${colIndex}`);
369
+ }
370
+ if (rendered.has(button.id)) {
371
+ return null;
372
+ }
373
+ rendered.add(button.id);
374
+ const buttonMetric = buttonMetricsLookup[button.id];
375
+ const effort = buttonMetric?.effort || 0;
376
+ const targetPageId = button.targetPageId || button.semanticAction?.targetId;
377
+ const hasLink = targetPageId && tree.pages[targetPageId];
378
+ const colSpan = button.columnSpan || 1;
379
+ const rowSpan = button.rowSpan || 1;
380
+ const predictions = button.predictions || button.parameters?.predictions;
381
+ const hasPredictions = predictions && predictions.length > 0;
382
+ const isPredictionCell = button.contentType === "AutoContent" && (button.contentSubType || "").toLowerCase() === "prediction";
383
+ const isWorkspace = button.contentType === "Workspace";
384
+ const imageSrc = (button.resolvedImageEntry && !String(button.resolvedImageEntry).startsWith("[") ? button.resolvedImageEntry : null) || (button.image && !String(button.image).startsWith("[") ? button.image : null);
385
+ if (isWorkspace) {
386
+ return /* @__PURE__ */ jsxs(
387
+ "div",
388
+ {
389
+ className: "relative p-3 rounded-lg border-2 bg-gray-50 text-gray-800 flex items-center gap-2",
390
+ style: {
391
+ borderColor: button.style?.borderColor || "#e5e7eb",
392
+ gridColumn: `${colIndex + 1} / span ${colSpan}`,
393
+ gridRow: `${rowIndex + 1} / span ${rowSpan}`
394
+ },
395
+ children: [
396
+ /* @__PURE__ */ jsx("div", { className: "font-semibold text-sm", children: button.label || "Workspace" }),
397
+ /* @__PURE__ */ jsx("div", { className: "text-xs text-gray-500 truncate", children: message || "Chat writing area" })
398
+ ]
399
+ },
400
+ button.id
401
+ );
402
+ }
403
+ return /* @__PURE__ */ jsxs(
404
+ "button",
405
+ {
406
+ onClick: () => handleButtonClick(button),
407
+ 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",
408
+ style: {
409
+ backgroundColor: button.style?.backgroundColor || "#f3f4f6",
410
+ borderColor: button.style?.borderColor || "#e5e7eb",
411
+ color: button.style?.fontColor || void 0,
412
+ gridColumn: `${colIndex + 1} / span ${colSpan}`,
413
+ gridRow: `${rowIndex + 1} / span ${rowSpan}`
414
+ },
415
+ children: [
416
+ buttonMetric && showEffortBadges && /* @__PURE__ */ 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) }),
417
+ hasLink && showLinkIndicators && /* @__PURE__ */ jsx("div", { className: "absolute top-1 left-1 w-2 h-2 bg-green-500 rounded-full shadow-sm" }),
418
+ hasPredictions && /* @__PURE__ */ jsx(
419
+ "div",
420
+ {
421
+ onClick: (e) => handleShowPredictions(button, e),
422
+ 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",
423
+ title: `Has ${predictions?.length} word form${predictions && predictions.length > 1 ? "s" : ""}`,
424
+ children: predictions?.length
425
+ }
426
+ ),
427
+ imageSrc && /* @__PURE__ */ jsx(
428
+ "img",
429
+ {
430
+ src: imageSrc,
431
+ alt: button.label,
432
+ className: "max-h-12 object-contain"
433
+ }
434
+ ),
435
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center justify-center", children: [
436
+ /* @__PURE__ */ jsx(
437
+ "span",
438
+ {
439
+ className: `text-xs sm:text-sm text-center font-medium leading-tight line-clamp-3 ${getTextColor(
440
+ button.style?.backgroundColor
441
+ )}`,
442
+ children: button.label
443
+ }
444
+ ),
445
+ isPredictionCell && predictions && predictions.length > 0 && /* @__PURE__ */ jsxs("div", { className: "mt-1 text-[10px] sm:text-xs text-center opacity-80 space-y-0.5", children: [
446
+ predictions.slice(0, 3).map((p, idx) => /* @__PURE__ */ jsx("div", { children: p }, `${button.id}-pred-${idx}`)),
447
+ predictions.length > 3 && /* @__PURE__ */ jsx("div", { children: "\u2026" })
448
+ ] })
449
+ ] }),
450
+ button.message && button.message !== button.label && !isPredictionCell && /* @__PURE__ */ jsx(
451
+ "span",
452
+ {
453
+ className: `text-[10px] sm:text-xs text-center opacity-75 line-clamp-2 ${getTextColor(
454
+ button.style?.backgroundColor
455
+ )}`,
456
+ children: button.message
457
+ }
458
+ )
459
+ ]
267
460
  },
268
- children: [
269
- buttonMetric && showEffortBadges && /* @__PURE__ */ 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) }),
270
- hasLink && showLinkIndicators && /* @__PURE__ */ jsx("div", { className: "absolute top-1 left-1 w-2 h-2 bg-green-500 rounded-full shadow-sm" }),
271
- /* @__PURE__ */ jsx(
272
- "span",
273
- {
274
- className: `text-xs sm:text-sm text-center font-medium leading-tight line-clamp-3 ${getTextColor(
275
- button.style?.backgroundColor
276
- )}`,
277
- children: button.label
278
- }
279
- ),
280
- button.message && button.message !== button.label && /* @__PURE__ */ jsx(
281
- "span",
282
- {
283
- className: `text-[10px] sm:text-xs text-center opacity-75 line-clamp-2 ${getTextColor(
284
- button.style?.backgroundColor
285
- )}`,
286
- children: button.message
287
- }
288
- )
289
- ]
290
- },
291
- button.id
292
- );
293
- })
294
- )
461
+ button.id
462
+ );
463
+ })
464
+ );
465
+ })()
295
466
  }
296
467
  ),
297
468
  Object.keys(tree.pages).length > 1 && /* @__PURE__ */ jsx("div", { className: "p-4 border-t border-gray-200 dark:border-gray-700", children: /* @__PURE__ */ jsxs("p", { className: "text-sm text-gray-600 dark:text-gray-400 mb-2", children: [
@@ -299,7 +470,16 @@ ${button.message || ""}`,
299
470
  " pages in this vocabulary"
300
471
  ] }) })
301
472
  ] })
302
- ] })
473
+ ] }),
474
+ predictionsTooltip && /* @__PURE__ */ jsx(
475
+ PredictionsTooltip,
476
+ {
477
+ predictions: predictionsTooltip.predictions,
478
+ label: predictionsTooltip.label,
479
+ position: predictionsTooltip.position,
480
+ onClose: () => setPredictionsTooltip(null)
481
+ }
482
+ )
303
483
  ] });
304
484
  }
305
485
 
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/BoardViewer.tsx","../src/hooks/useAACFile.ts","../src/utils/loaders.ts"],"sourcesContent":["import React, { useState, useMemo, useCallback } from 'react';\nimport type {\n AACPage,\n AACButton,\n} from '@willwade/aac-processors';\nimport type { BoardViewerProps, ButtonMetric } from '../types';\n\n/**\n * AAC Board Viewer Component\n *\n * Displays AAC boards with interactive navigation, sentence building,\n * and optional effort metrics.\n *\n * @param props - BoardViewerProps\n */\nexport function BoardViewer({\n tree,\n buttonMetrics,\n showMessageBar = true,\n showEffortBadges = true,\n showLinkIndicators = true,\n initialPageId,\n onButtonClick,\n onPageChange,\n className = '',\n}: BoardViewerProps) {\n const resolveInitialPageId = useCallback(() => {\n // Explicit override\n if (initialPageId && tree.pages[initialPageId]) {\n return initialPageId;\n }\n // If toolbar exists and rootId is different, use rootId\n if (tree.toolbarId && tree.rootId && tree.rootId !== tree.toolbarId) {\n return tree.rootId;\n }\n // If rootId exists and is not a toolbar, use it\n if (tree.rootId && tree.pages[tree.rootId]) {\n const rootPage = tree.pages[tree.rootId];\n const isToolbar =\n rootPage.name.toLowerCase().includes('toolbar') ||\n rootPage.name.toLowerCase().includes('tool bar');\n if (!isToolbar) {\n return tree.rootId;\n }\n }\n // Fall back to first page that's not a toolbar\n const nonToolbarPage = Object.values(tree.pages).find(\n (p) => !p.name.toLowerCase().includes('toolbar') && !p.name.toLowerCase().includes('tool bar')\n );\n if (nonToolbarPage) {\n return nonToolbarPage.id;\n }\n // Last resort: first page\n const pageIds = Object.keys(tree.pages);\n return pageIds.length > 0 ? pageIds[0] : null;\n }, [initialPageId, tree]);\n\n // Determine which page to show\n const [currentPageId, setCurrentPageId] = useState<string | null>(() => resolveInitialPageId());\n\n const [pageHistory, setPageHistory] = useState<AACPage[]>([]);\n const [message, setMessage] = useState('');\n const [currentWordCount, setCurrentWordCount] = useState(0);\n const [currentEffort, setCurrentEffort] = useState(0);\n\n // Convert button metrics array to lookup object for easy access\n const buttonMetricsLookup = useMemo(() => {\n if (!buttonMetrics) return {};\n const lookup: { [buttonId: string]: ButtonMetric } = {};\n buttonMetrics.forEach((metric) => {\n lookup[metric.id] = metric;\n });\n return lookup;\n }, [buttonMetrics]);\n\n const currentPage = currentPageId ? tree.pages[currentPageId] : null;\n\n // Sync when tree or initialPageId changes\n React.useEffect(() => {\n setCurrentPageId(resolveInitialPageId());\n setPageHistory([]);\n }, [resolveInitialPageId]);\n\n // Calculate total stats for current word\n const updateStats = (word: string, effort: number) => {\n setCurrentWordCount((prev) => prev + 1);\n setCurrentEffort((prev) => prev + effort);\n };\n\n const handleButtonClick = (button: AACButton) => {\n // Call external callback if provided\n if (onButtonClick) {\n onButtonClick(button);\n }\n\n // Check if button links to another page via targetPageId or semanticAction\n const targetPageId = button.targetPageId || button.semanticAction?.targetId;\n\n if (targetPageId && tree.pages[targetPageId]) {\n if (currentPage) {\n setPageHistory((prev) => [...prev, currentPage]);\n }\n setCurrentPageId(targetPageId);\n if (onPageChange) {\n onPageChange(targetPageId);\n }\n return;\n }\n\n // Otherwise add to message\n const word = button.message || button.label;\n const effort = buttonMetricsLookup[button.id]?.effort || 1;\n\n setMessage((prev) => {\n const newMessage = prev + (prev ? ' ' : '') + word;\n updateStats(word, effort);\n return newMessage;\n });\n };\n\n const handleBack = () => {\n if (pageHistory.length > 0) {\n const previousPage = pageHistory[pageHistory.length - 1];\n setPageHistory((prev) => prev.slice(0, -1));\n setCurrentPageId(previousPage.id);\n if (onPageChange) {\n onPageChange(previousPage.id);\n }\n }\n };\n\n const clearMessage = () => {\n setMessage('');\n setCurrentWordCount(0);\n setCurrentEffort(0);\n };\n\n const getTextColor = (backgroundColor?: string) => {\n if (!backgroundColor) return 'text-gray-900 dark:text-gray-100';\n\n // Convert hex to rgb for brightness calculation\n const hex = backgroundColor.replace('#', '');\n if (hex.length === 6) {\n const r = parseInt(hex.substring(0, 2), 16);\n const g = parseInt(hex.substring(2, 4), 16);\n const b = parseInt(hex.substring(4, 6), 16);\n const brightness = (r * 299 + g * 587 + b * 114) / 1000;\n return brightness >= 128 ? 'text-gray-900' : 'text-white';\n }\n\n return 'text-gray-900 dark:text-gray-100';\n };\n\n if (!currentPage) {\n return (\n <div className={`flex items-center justify-center h-96 bg-gray-100 dark:bg-gray-800 rounded-lg ${className}`}>\n <div className=\"text-center\">\n <p className=\"text-gray-600 dark:text-gray-400\">No pages available</p>\n </div>\n </div>\n );\n }\n\n // Get toolbar page if it exists\n const toolbarPage = tree.toolbarId ? tree.pages[tree.toolbarId] : null;\n\n // Get grid dimensions\n const gridRows = currentPage.grid.length;\n const gridCols = gridRows > 0 ? currentPage.grid[0].length : 0;\n\n return (\n <div className={`bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden ${className}`}>\n {/* Message Bar */}\n {showMessageBar && (\n <div className=\"p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50\">\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex-1 min-w-0\">\n {message ? (\n <div className=\"space-y-2\">\n <p className=\"text-lg text-gray-900 dark:text-white break-words\">{message}</p>\n <div className=\"flex gap-4 text-sm\">\n <div className=\"text-gray-600 dark:text-gray-400\">\n {currentWordCount} {currentWordCount === 1 ? 'word' : 'words'}\n </div>\n {buttonMetrics && (\n <>\n <div className=\"text-gray-600 dark:text-gray-400\">\n Effort: <span className=\"font-medium\">{currentEffort.toFixed(2)}</span>\n </div>\n <div className=\"text-gray-600 dark:text-gray-400\">\n Avg:{' '}\n <span className=\"font-medium\">\n {currentWordCount > 0\n ? (currentEffort / currentWordCount).toFixed(2)\n : '0.00'}\n </span>\n </div>\n </>\n )}\n </div>\n </div>\n ) : (\n <p className=\"text-gray-500 dark:text-gray-400 italic\">\n Tap buttons to build a sentence...\n </p>\n )}\n </div>\n {message && (\n <button\n onClick={clearMessage}\n className=\"p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition\"\n aria-label=\"Clear message\"\n >\n <svg\n className=\"h-5 w-5 text-gray-600 dark:text-gray-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M6 18L18 6M6 6l12 12\"\n />\n </svg>\n </button>\n )}\n </div>\n </div>\n )}\n\n {/* Main Content Area */}\n <div className=\"flex\">\n {/* Toolbar Sidebar (if exists) */}\n {toolbarPage && (\n <div className=\"w-16 sm:w-20 border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50\">\n <div className=\"p-2\">\n <p className=\"text-[10px] text-gray-500 dark:text-gray-400 text-center mb-2 font-semibold\">\n TOOLBAR\n </p>\n <div className=\"grid gap-1\">\n {toolbarPage.grid.map((row, _rowIndex) =>\n row.map((button, _colIndex) => {\n if (!button) return null;\n\n const buttonMetric = buttonMetricsLookup[button.id];\n const effort = buttonMetric?.effort || 0;\n\n return (\n <button\n key={button.id}\n onClick={() => handleButtonClick(button)}\n className=\"aspect-square p-1 rounded border border-gray-200 dark:border-gray-700 transition flex flex-col items-center justify-center gap-0.5 hover:bg-gray-200 dark:hover:bg-gray-700 relative\"\n style={{\n backgroundColor: button.style?.backgroundColor || '#f3f4f6',\n borderColor: button.style?.borderColor || '#e5e7eb',\n }}\n title={`${button.label}\\n${button.message || ''}`}\n >\n {buttonMetric && showEffortBadges && effort > 0 && (\n <div className=\"absolute top-0 right-0 px-0.5 py-0 text-[8px] font-semibold rounded bg-blue-600 text-white\">\n {effort.toFixed(1)}\n </div>\n )}\n <span\n className={`text-[8px] sm:text-[9px] text-center font-medium leading-tight line-clamp-2 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.label}\n </span>\n </button>\n );\n })\n )}\n </div>\n </div>\n </div>\n )}\n\n {/* Main Page Content */}\n <div className=\"flex-1\">\n {/* Header */}\n <div className=\"flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700\">\n <div className=\"flex items-center gap-2\">\n {pageHistory.length > 0 && (\n <button\n onClick={handleBack}\n className=\"p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition\"\n aria-label=\"Go back\"\n >\n <svg\n className=\"h-5 w-5 text-gray-600 dark:text-gray-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 19l-7-7 7-7\"\n />\n </svg>\n </button>\n )}\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white\">\n {currentPage.name}\n </h3>\n </div>\n <div className=\"text-sm text-gray-500 dark:text-gray-400\">\n {gridRows}×{gridCols} grid\n </div>\n </div>\n\n {/* Grid */}\n <div\n className=\"p-4 gap-2 overflow-auto max-h-[600px]\"\n style={{\n display: 'grid',\n gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`,\n }}\n >\n {currentPage.grid.map((row, rowIndex) =>\n row.map((button, colIndex) => {\n if (!button) {\n return <div key={`empty-${rowIndex}-${colIndex}`} className=\"aspect-square\" />;\n }\n\n const buttonMetric = buttonMetricsLookup[button.id];\n const effort = buttonMetric?.effort || 0;\n const targetPageId = button.targetPageId || button.semanticAction?.targetId;\n const hasLink = targetPageId && tree.pages[targetPageId];\n\n return (\n <button\n key={button.id}\n onClick={() => handleButtonClick(button)}\n 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\"\n style={{\n backgroundColor: button.style?.backgroundColor || '#f3f4f6',\n borderColor: button.style?.borderColor || '#e5e7eb',\n color: button.style?.fontColor || undefined,\n }}\n >\n {/* Effort Badge */}\n {buttonMetric && showEffortBadges && (\n <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\">\n {effort.toFixed(1)}\n </div>\n )}\n\n {/* Link Indicator */}\n {hasLink && showLinkIndicators && (\n <div className=\"absolute top-1 left-1 w-2 h-2 bg-green-500 rounded-full shadow-sm\" />\n )}\n\n {/* Label */}\n <span\n className={`text-xs sm:text-sm text-center font-medium leading-tight line-clamp-3 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.label}\n </span>\n\n {/* Message (if different from label) */}\n {button.message && button.message !== button.label && (\n <span\n className={`text-[10px] sm:text-xs text-center opacity-75 line-clamp-2 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.message}\n </span>\n )}\n </button>\n );\n })\n )}\n </div>\n\n {/* Page Navigation (if multiple pages) */}\n {Object.keys(tree.pages).length > 1 && (\n <div className=\"p-4 border-t border-gray-200 dark:border-gray-700\">\n <p className=\"text-sm text-gray-600 dark:text-gray-400 mb-2\">\n {Object.keys(tree.pages).length} pages in this vocabulary\n </p>\n </div>\n )}\n </div>\n </div>\n </div>\n );\n}\n","/**\n * React hooks for AAC Board Viewer\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { loadAACFileFromURL, calculateMetrics } from '../utils/loaders';\nimport type { AACTree } from '@willwade/aac-processors';\nimport type { ButtonMetric, MetricsOptions } from '../types';\n\n/**\n * Hook to load an AAC file from a URL\n *\n * @param url - URL to the AAC file\n * @param options - Processor options (e.g., pageLayoutPreference for SNAP)\n * @returns Object with tree, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer() {\n * const { tree, loading, error, reload } = useAACFile('/files/board.sps');\n *\n * if (loading) return <div>Loading...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n *\n * return <BoardViewer tree={tree} />;\n * }\n * ```\n */\nexport function useAACFile(\n url: string,\n options?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (options?.enabled === false) {\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFileFromURL(url, options?.processorOptions);\n setTree(loadedTree);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [url, options]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to load an AAC file and calculate metrics\n *\n * @param url - URL to the AAC file\n * @param metricsOptions - Metrics calculation options\n * @returns Object with tree, metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer() {\n * const { tree, metrics, loading, error } = useAACFileWithMetrics(\n * '/files/board.sps',\n * { accessMethod: 'direct' }\n * );\n *\n * if (loading) return <div>Loading...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n *\n * return <BoardViewer tree={tree} buttonMetrics={metrics} />;\n * }\n * ```\n */\nexport function useAACFileWithMetrics(\n url: string,\n metricsOptions?: MetricsOptions,\n fileOptions?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [metrics, setMetrics] = useState<ButtonMetric[] | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (fileOptions?.enabled === false) {\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFileFromURL(url, fileOptions?.processorOptions);\n setTree(loadedTree);\n\n // Calculate metrics\n const calculatedMetrics = await calculateMetrics(loadedTree, metricsOptions || {});\n setMetrics(calculatedMetrics);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [url, metricsOptions, fileOptions]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n metrics,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to calculate metrics for a tree\n *\n * @param tree - The AAC tree\n * @param options - Metrics calculation options\n * @returns Object with metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer({ tree }) {\n * const { metrics, loading } = useMetrics(tree, {\n * accessMethod: 'scanning',\n * scanningConfig: { pattern: 'row-column' }\n * });\n *\n * return <BoardViewer tree={tree} buttonMetrics={metrics} />;\n * }\n * ```\n */\nexport function useMetrics(\n tree: AACTree | null,\n options?: MetricsOptions\n) {\n const [metrics, setMetrics] = useState<ButtonMetric[] | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const calculate = useCallback(async () => {\n if (!tree) {\n setMetrics(null);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const calculatedMetrics = await calculateMetrics(tree, options || {});\n setMetrics(calculatedMetrics);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to calculate metrics'));\n } finally {\n setLoading(false);\n }\n }, [tree, options]);\n\n useEffect(() => {\n calculate();\n }, [calculate]);\n\n return {\n metrics,\n loading,\n error,\n recalculate: calculate,\n };\n}\n\n/**\n * Hook for sentence building state\n *\n * @returns Object with message state and handlers\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { message, wordCount, effort, addWord, clear } = useSentenceBuilder();\n *\n * return (\n * <div>\n * <p>{message || 'Start building...'}</p>\n * <p>{wordCount} words, {effort.toFixed(2)} effort</p>\n * <button onClick={clear}>Clear</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useSentenceBuilder() {\n const [message, setMessage] = useState('');\n const [wordCount, setWordCount] = useState(0);\n const [effort, setEffort] = useState(0);\n\n const addWord = useCallback((word: string, wordEffort: number = 1) => {\n setMessage((prev) => {\n const newMessage = prev + (prev ? ' ' : '') + word;\n setWordCount((prev) => prev + 1);\n setEffort((prev) => prev + wordEffort);\n return newMessage;\n });\n }, []);\n\n const clear = useCallback(() => {\n setMessage('');\n setWordCount(0);\n setEffort(0);\n }, []);\n\n return {\n message,\n wordCount,\n effort,\n addWord,\n clear,\n };\n}\n","/**\n * AAC File Loading Utilities\n *\n * Provides utilities for loading AAC files from various sources\n * (URLs, File objects, file paths) in both client and server contexts.\n */\n\nimport type { AACTree } from '@willwade/aac-processors';\nimport type { LoadAACFileResult } from '../types';\n\ntype ProcessorOptions = Record<string, unknown> | undefined;\n\ntype ProcessorModule = typeof import('@willwade/aac-processors');\n\n// Lazily load processors so browser bundles avoid pulling in Node APIs\nasync function importProcessors(): Promise<ProcessorModule> {\n return import('@willwade/aac-processors');\n}\n\n/**\n * Get the appropriate processor for a file based on its extension\n */\nasync function getProcessorForFile(filepath: string, options?: ProcessorOptions) {\n const {\n getProcessor,\n GridsetProcessor,\n SnapProcessor,\n TouchChatProcessor,\n ObfProcessor,\n ApplePanelsProcessor,\n AstericsGridProcessor,\n OpmlProcessor,\n ExcelProcessor,\n DotProcessor,\n } = await importProcessors();\n\n const ext = filepath.toLowerCase();\n\n // GridSet files (.gridset)\n if (ext.endsWith('.gridset')) {\n return new GridsetProcessor();\n }\n\n // SNAP files (.sps, .spb)\n if (ext.endsWith('.sps') || ext.endsWith('.spb')) {\n return options ? new SnapProcessor(null, options) : new SnapProcessor();\n }\n\n // TouchChat files (.ce)\n if (ext.endsWith('.ce')) {\n return new TouchChatProcessor();\n }\n\n // OpenBoard files (.obf, .obz)\n if (ext.endsWith('.obf')) {\n return new ObfProcessor();\n }\n if (ext.endsWith('.obz')) {\n return new ObfProcessor();\n }\n\n // Asterics Grid files (.grd)\n if (ext.endsWith('.grd')) {\n return new AstericsGridProcessor();\n }\n\n // Apple Panels files\n if (ext.endsWith('.plist')) {\n return new ApplePanelsProcessor();\n }\n\n // OPML files\n if (ext.endsWith('.opml')) {\n return new OpmlProcessor();\n }\n\n // Excel files\n if (ext.endsWith('.xlsx') || ext.endsWith('.xls')) {\n return new ExcelProcessor();\n }\n\n // DOT files\n if (ext.endsWith('.dot')) {\n return new DotProcessor();\n }\n\n // Fallback to generic processor detection\n return getProcessor(filepath);\n}\n\n/**\n * Load an AAC file from a file path (server-side only)\n *\n * @param filepath - Path to the AAC file\n * @param options - Processor options (e.g., pageLayoutPreference for SNAP)\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const tree = await loadAACFile('/path/to/file.sps');\n * ```\n */\nexport async function loadAACFile(\n filepath: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const processor = await getProcessorForFile(filepath, options);\n return processor.loadIntoTree(filepath);\n}\n\n/**\n * Load an AAC file and return extended result with format info\n *\n * @param filepath - Path to the AAC file\n * @param options - Processor options\n * @returns Promise resolving to LoadAACFileResult\n */\nexport async function loadAACFileWithMetadata(\n filepath: string,\n options?: ProcessorOptions\n): Promise<LoadAACFileResult> {\n const tree = await loadAACFile(filepath, options);\n\n // Detect format from file extension\n const ext = filepath.toLowerCase();\n let format = 'unknown';\n\n if (ext.endsWith('.gridset')) format = 'gridset';\n else if (ext.endsWith('.sps') || ext.endsWith('.spb')) format = 'snap';\n else if (ext.endsWith('.ce')) format = 'touchchat';\n else if (ext.endsWith('.obf')) format = 'openboard';\n else if (ext.endsWith('.obz')) format = 'openboard';\n else if (ext.endsWith('.grd')) format = 'asterics-grid';\n else if (ext.endsWith('.plist')) format = 'apple-panels';\n else if (ext.endsWith('.opml')) format = 'opml';\n else if (ext.endsWith('.xlsx') || ext.endsWith('.xls')) format = 'excel';\n else if (ext.endsWith('.dot')) format = 'dot';\n\n return {\n tree,\n format,\n metadata: tree.metadata,\n };\n}\n\n/**\n * Load an AAC file from a URL (client-side)\n *\n * Note: This requires the server to provide the file with appropriate CORS headers.\n * For better performance, consider server-side loading instead.\n *\n * @param url - URL to the AAC file\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const tree = await loadAACFileFromURL('https://example.com/file.sps');\n * ```\n */\nexport async function loadAACFileFromURL(\n url: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`Failed to load file: ${response.statusText}`);\n }\n\n const blob = await response.blob();\n const filename = getFilenameFromURL(url);\n\n return loadAACFileFromFile(blob, filename, options);\n}\n\n/**\n * Load an AAC file from a File object (client-side file input)\n *\n * @param file - File object from file input\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const input = document.querySelector('input[type=\"file\"]');\n * input.onchange = async (e) => {\n * const file = e.target.files[0];\n * const tree = await loadAACFileFromFile(file);\n * // Use tree...\n * };\n * ```\n */\nexport async function loadAACFileFromFile(\n _file: File | Blob,\n _filename?: string,\n _options?: unknown\n): Promise<AACTree> {\n throw new Error('Client-side file loading not yet fully implemented. Please use server-side loading or loadAACFileFromURL with proper CORS headers.');\n}\n\n/**\n * Extract filename from URL\n */\nfunction getFilenameFromURL(url: string): string {\n try {\n const urlObj = new URL(url);\n const pathname = urlObj.pathname;\n const parts = pathname.split('/');\n return parts[parts.length - 1] || 'unknown';\n } catch {\n return 'unknown';\n }\n}\n\n/**\n * Calculate cognitive effort metrics for an AAC tree\n *\n * @param tree - The AAC tree\n * @param options - Metrics calculation options\n * @returns Promise resolving to array of ButtonMetrics\n *\n * @example\n * ```ts\n * import { calculateMetrics } from 'aac-board-viewer';\n *\n * const metrics = await calculateMetrics(tree, {\n * accessMethod: 'direct',\n * });\n * ```\n */\nexport async function calculateMetrics(\n tree: AACTree,\n options: {\n accessMethod?: 'direct' | 'scanning';\n scanningConfig?: {\n pattern?: 'linear' | 'row-column' | 'block';\n selectionMethod?: string;\n errorCorrection?: boolean;\n };\n } = {}\n) {\n // Import MetricsCalculator dynamically to avoid circular dependencies\n const { MetricsCalculator } = await import('@willwade/aac-processors');\n\n const calculator = new MetricsCalculator();\n\n let metricsOptions: Record<string, unknown> = {};\n\n if (options.accessMethod === 'scanning' && options.scanningConfig) {\n // Import scanning enums\n const { CellScanningOrder, ScanningSelectionMethod } = await import('@willwade/aac-processors');\n\n let cellScanningOrder = CellScanningOrder.SimpleScan;\n let blockScanEnabled = false;\n\n switch (options.scanningConfig.pattern) {\n case 'linear':\n cellScanningOrder = CellScanningOrder.SimpleScan;\n break;\n case 'row-column':\n cellScanningOrder = CellScanningOrder.RowColumnScan;\n break;\n case 'block':\n cellScanningOrder = CellScanningOrder.RowColumnScan;\n blockScanEnabled = true;\n break;\n }\n\n metricsOptions = {\n scanningConfig: {\n cellScanningOrder,\n blockScanEnabled,\n selectionMethod: ScanningSelectionMethod.AutoScan,\n errorCorrectionEnabled: options.scanningConfig.errorCorrection || false,\n errorRate: 0.1,\n },\n };\n }\n\n const metricsResult = calculator.analyze(tree, metricsOptions);\n\n // Convert to the format expected by BoardViewer\n type MetricsButton = {\n id: string;\n label: string;\n effort: number;\n count?: number;\n level?: number;\n semantic_id?: string;\n clone_id?: string;\n };\n\n return metricsResult.buttons.map((btn: MetricsButton) => ({\n id: btn.id,\n label: btn.label,\n effort: btn.effort,\n count: btn.count ?? 0,\n is_word: true,\n level: btn.level,\n semantic_id: btn.semantic_id,\n clone_id: btn.clone_id,\n }));\n}\n\n/**\n * Get a list of supported file formats\n *\n * @returns Array of format information\n */\nexport function getSupportedFormats(): Array<{\n name: string;\n extensions: string[];\n description: string;\n}> {\n return [\n {\n name: 'Grid 3',\n extensions: ['.gridset'],\n description: 'Smartbox Grid 3 communication boards',\n },\n {\n name: 'TD Snap',\n extensions: ['.sps', '.spb'],\n description: 'Tobii Dynavox Snap files',\n },\n {\n name: 'TouchChat',\n extensions: ['.ce'],\n description: 'Saltillo TouchChat files',\n },\n {\n name: 'OpenBoard',\n extensions: ['.obf', '.obz'],\n description: 'OpenBoard Format (OBZ/OBF)',\n },\n {\n name: 'Asterics Grid',\n extensions: ['.grd'],\n description: 'Asterics Grid files (.grd)',\n },\n {\n name: 'Apple Panels',\n extensions: ['.plist'],\n description: 'Apple iOS Panels files',\n },\n {\n name: 'OPML',\n extensions: ['.opml'],\n description: 'OPML outline files',\n },\n {\n name: 'Excel',\n extensions: ['.xlsx', '.xls'],\n description: 'Excel spreadsheet boards',\n },\n {\n name: 'DOT',\n extensions: ['.dot'],\n description: 'DOT graph visualization files',\n },\n ];\n}\n"],"mappings":"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","names":["useState","useCallback","useState","useCallback","prev"]}
1
+ {"version":3,"sources":["../src/components/BoardViewer.tsx","../src/hooks/useAACFile.ts","../src/utils/loaders.ts"],"sourcesContent":["import React, { useState, useMemo, useCallback } from 'react';\nimport type {\n AACPage,\n AACButton,\n} from '@willwade/aac-processors';\nimport type { BoardViewerProps, ButtonMetric } from '../types';\n\n/**\n * Predictions Tooltip Component\n *\n * Shows a tooltip with predicted word forms when clicking the predictions indicator\n */\ninterface PredictionsTooltipProps {\n predictions: string[];\n label: string;\n position: { x: number; y: number };\n onClose: () => void;\n}\n\nfunction PredictionsTooltip({ predictions, label, position, onClose }: PredictionsTooltipProps) {\n // Close tooltip when clicking outside\n React.useEffect(() => {\n const handleClickOutside = (e: MouseEvent) => {\n if (e.target instanceof HTMLElement && !e.target.closest('.predictions-tooltip')) {\n onClose();\n }\n };\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, [onClose]);\n\n return (\n <div\n 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\"\n style={{\n left: `${Math.min(position.x, window.innerWidth - 200)}px`,\n top: `${Math.min(position.y, window.innerHeight - 150)}px`,\n }}\n >\n <div className=\"flex items-center justify-between mb-2\">\n <h4 className=\"text-sm font-semibold text-gray-900 dark:text-white\">\n Word forms for &quot;{label}&quot;\n </h4>\n <button\n onClick={onClose}\n className=\"p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded\"\n aria-label=\"Close\"\n >\n <svg className=\"h-4 w-4 text-gray-600 dark:text-gray-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n <div className=\"flex flex-wrap gap-1\">\n {predictions.map((word, idx) => (\n <span\n key={idx}\n className=\"px-2 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded text-xs font-medium\"\n >\n {word}\n </span>\n ))}\n </div>\n </div>\n );\n}\n\n/**\n * AAC Board Viewer Component\n *\n * Displays AAC boards with interactive navigation, sentence building,\n * and optional effort metrics.\n *\n * @param props - BoardViewerProps\n */\nexport function BoardViewer({\n tree,\n buttonMetrics,\n showMessageBar = true,\n showEffortBadges = true,\n showLinkIndicators = true,\n initialPageId,\n onButtonClick,\n onPageChange,\n className = '',\n}: BoardViewerProps) {\n const resolveInitialPageId = useCallback(() => {\n // Explicit override\n if (initialPageId && tree.pages[initialPageId]) {\n return initialPageId;\n }\n // If toolbar exists and rootId is different, use rootId\n if (tree.toolbarId && tree.rootId && tree.rootId !== tree.toolbarId) {\n return tree.rootId;\n }\n // If rootId exists and is not a toolbar, use it\n if (tree.rootId && tree.pages[tree.rootId]) {\n const rootPage = tree.pages[tree.rootId];\n const isToolbar =\n rootPage.name.toLowerCase().includes('toolbar') ||\n rootPage.name.toLowerCase().includes('tool bar');\n if (!isToolbar) {\n return tree.rootId;\n }\n }\n // Explicit Start page fallback if present\n const startPage = Object.values(tree.pages).find(\n (p) => p.name.toLowerCase() === 'start'\n );\n if (startPage) {\n return startPage.id;\n }\n // Fall back to first page that's not a toolbar\n const nonToolbarPage = Object.values(tree.pages).find(\n (p) => !p.name.toLowerCase().includes('toolbar') && !p.name.toLowerCase().includes('tool bar')\n );\n if (nonToolbarPage) {\n return nonToolbarPage.id;\n }\n // Last resort: first page\n const pageIds = Object.keys(tree.pages);\n return pageIds.length > 0 ? pageIds[0] : null;\n }, [initialPageId, tree]);\n\n // Determine which page to show\n const [currentPageId, setCurrentPageId] = useState<string | null>(() => resolveInitialPageId());\n\n const [pageHistory, setPageHistory] = useState<AACPage[]>([]);\n const [message, setMessage] = useState('');\n const [currentWordCount, setCurrentWordCount] = useState(0);\n const [currentEffort, setCurrentEffort] = useState(0);\n\n // Predictions tooltip state\n const [predictionsTooltip, setPredictionsTooltip] = useState<{\n predictions: string[];\n label: string;\n position: { x: number; y: number };\n } | null>(null);\n\n // Convert button metrics array to lookup object for easy access\n const buttonMetricsLookup = useMemo(() => {\n if (!buttonMetrics) return {};\n const lookup: { [buttonId: string]: ButtonMetric } = {};\n buttonMetrics.forEach((metric) => {\n lookup[metric.id] = metric;\n });\n return lookup;\n }, [buttonMetrics]);\n\n const currentPage = currentPageId ? tree.pages[currentPageId] : null;\n const goToPage = (targetPageId: string | undefined | null) => {\n if (!targetPageId || !tree.pages[targetPageId]) return false;\n if (currentPage) {\n setPageHistory((prev) => [...prev, currentPage]);\n }\n setCurrentPageId(targetPageId);\n if (onPageChange) {\n onPageChange(targetPageId);\n }\n return true;\n };\n\n // Sync when tree or initialPageId changes\n React.useEffect(() => {\n setCurrentPageId(resolveInitialPageId());\n setPageHistory([]);\n }, [resolveInitialPageId]);\n\n // Calculate total stats for current word\n const updateStats = (word: string, effort: number) => {\n setCurrentWordCount((prev) => prev + 1);\n setCurrentEffort((prev) => prev + effort);\n };\n\n const handleButtonClick = (button: AACButton) => {\n // Call external callback if provided\n if (onButtonClick) {\n onButtonClick(button);\n }\n\n const intent = button.semanticAction?.intent\n ? String(button.semanticAction.intent)\n : undefined;\n const targetPageId = button.targetPageId || button.semanticAction?.targetId;\n const effort = buttonMetricsLookup[button.id]?.effort || 1;\n const textValue =\n button.semanticAction?.text || button.message || button.label || '';\n\n const deleteLastWord = () => {\n setMessage((prev) => {\n const parts = prev.trim().split(/\\s+/);\n parts.pop();\n const newMsg = parts.join(' ');\n return newMsg;\n });\n };\n\n const deleteLastCharacter = () => {\n setMessage((prev) => prev.slice(0, -1));\n };\n\n const appendText = (word: string) => {\n const trimmed = word || button.label || '';\n setMessage((prev) => {\n const newMessage = trimmed\n ? prev + (prev ? ' ' : '') + trimmed\n : prev;\n if (trimmed) {\n updateStats(trimmed, effort);\n }\n return newMessage;\n });\n };\n\n // Navigation takes precedence\n if (intent === 'NAVIGATE_TO' && goToPage(targetPageId)) {\n return;\n }\n\n switch (intent) {\n case 'GO_BACK':\n handleBack();\n return;\n case 'GO_HOME':\n if (tree.rootId && goToPage(tree.rootId)) return;\n break;\n case 'DELETE_WORD':\n deleteLastWord();\n return;\n case 'DELETE_CHARACTER':\n deleteLastCharacter();\n return;\n case 'CLEAR_TEXT':\n clearMessage();\n return;\n case 'SPEAK_IMMEDIATE':\n case 'SPEAK_TEXT':\n case 'INSERT_TEXT':\n appendText(textValue);\n return;\n default:\n break;\n }\n\n // Fallback navigation if intent not set but target exists\n if (targetPageId && goToPage(targetPageId)) {\n return;\n }\n\n // Otherwise add to message\n appendText(textValue);\n };\n\n const handleBack = () => {\n if (pageHistory.length > 0) {\n const previousPage = pageHistory[pageHistory.length - 1];\n setPageHistory((prev) => prev.slice(0, -1));\n setCurrentPageId(previousPage.id);\n if (onPageChange) {\n onPageChange(previousPage.id);\n }\n }\n };\n\n const clearMessage = () => {\n setMessage('');\n setCurrentWordCount(0);\n setCurrentEffort(0);\n };\n\n const handleShowPredictions = (\n button: AACButton,\n event: React.MouseEvent<HTMLDivElement>\n ) => {\n event.stopPropagation(); // Prevent button click\n const predictions = button.predictions || (button.parameters as { predictions?: string[] })?.predictions;\n if (predictions && predictions.length > 0) {\n setPredictionsTooltip({\n predictions,\n label: button.label,\n position: { x: event.clientX, y: event.clientY },\n });\n }\n };\n\n const getTextColor = (backgroundColor?: string) => {\n if (!backgroundColor) return 'text-gray-900 dark:text-gray-100';\n\n // Convert hex to rgb for brightness calculation\n const hex = backgroundColor.replace('#', '');\n if (hex.length === 6) {\n const r = parseInt(hex.substring(0, 2), 16);\n const g = parseInt(hex.substring(2, 4), 16);\n const b = parseInt(hex.substring(4, 6), 16);\n const brightness = (r * 299 + g * 587 + b * 114) / 1000;\n return brightness >= 128 ? 'text-gray-900' : 'text-white';\n }\n\n return 'text-gray-900 dark:text-gray-100';\n };\n\n if (!currentPage) {\n return (\n <div className={`flex items-center justify-center h-96 bg-gray-100 dark:bg-gray-800 rounded-lg ${className}`}>\n <div className=\"text-center\">\n <p className=\"text-gray-600 dark:text-gray-400\">No pages available</p>\n </div>\n </div>\n );\n }\n\n // Get toolbar page if it exists\n const toolbarPage = tree.toolbarId ? tree.pages[tree.toolbarId] : null;\n\n // Get grid dimensions\n const gridRows = currentPage.grid.length;\n const gridCols = gridRows > 0 ? currentPage.grid[0].length : 0;\n\n return (\n <div className={`bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden ${className}`}>\n {/* Message Bar */}\n {showMessageBar && (\n <div className=\"p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50\">\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex-1 min-w-0\">\n {message ? (\n <div className=\"space-y-2\">\n <p className=\"text-lg text-gray-900 dark:text-white break-words\">{message}</p>\n <div className=\"flex gap-4 text-sm\">\n <div className=\"text-gray-600 dark:text-gray-400\">\n {currentWordCount} {currentWordCount === 1 ? 'word' : 'words'}\n </div>\n {buttonMetrics && (\n <>\n <div className=\"text-gray-600 dark:text-gray-400\">\n Effort: <span className=\"font-medium\">{currentEffort.toFixed(2)}</span>\n </div>\n <div className=\"text-gray-600 dark:text-gray-400\">\n Avg:{' '}\n <span className=\"font-medium\">\n {currentWordCount > 0\n ? (currentEffort / currentWordCount).toFixed(2)\n : '0.00'}\n </span>\n </div>\n </>\n )}\n </div>\n </div>\n ) : (\n <p className=\"text-gray-500 dark:text-gray-400 italic\">\n Tap buttons to build a sentence...\n </p>\n )}\n </div>\n {message && (\n <button\n onClick={clearMessage}\n className=\"p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition\"\n aria-label=\"Clear message\"\n >\n <svg\n className=\"h-5 w-5 text-gray-600 dark:text-gray-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M6 18L18 6M6 6l12 12\"\n />\n </svg>\n </button>\n )}\n </div>\n </div>\n )}\n\n {/* Main Content Area */}\n <div className=\"flex\">\n {/* Toolbar Sidebar (if exists) */}\n {toolbarPage && (\n <div className=\"w-16 sm:w-20 border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50\">\n <div className=\"p-2\">\n <p className=\"text-[10px] text-gray-500 dark:text-gray-400 text-center mb-2 font-semibold\">\n TOOLBAR\n </p>\n <div className=\"grid gap-1\">\n {toolbarPage.grid.map((row, _rowIndex) =>\n row.map((button, _colIndex) => {\n if (!button) return null;\n\n const buttonMetric = buttonMetricsLookup[button.id];\n const effort = buttonMetric?.effort || 0;\n\n return (\n <button\n key={button.id}\n onClick={() => handleButtonClick(button)}\n className=\"aspect-square p-1 rounded border border-gray-200 dark:border-gray-700 transition flex flex-col items-center justify-center gap-0.5 hover:bg-gray-200 dark:hover:bg-gray-700 relative\"\n style={{\n backgroundColor: button.style?.backgroundColor || '#f3f4f6',\n borderColor: button.style?.borderColor || '#e5e7eb',\n }}\n title={`${button.label}\\n${button.message || ''}`}\n >\n {buttonMetric && showEffortBadges && effort > 0 && (\n <div className=\"absolute top-0 right-0 px-0.5 py-0 text-[8px] font-semibold rounded bg-blue-600 text-white\">\n {effort.toFixed(1)}\n </div>\n )}\n <span\n className={`text-[8px] sm:text-[9px] text-center font-medium leading-tight line-clamp-2 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.label}\n </span>\n </button>\n );\n })\n )}\n </div>\n </div>\n </div>\n )}\n\n {/* Main Page Content */}\n <div className=\"flex-1\">\n {/* Header */}\n <div className=\"flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700\">\n <div className=\"flex items-center gap-2\">\n {pageHistory.length > 0 && (\n <button\n onClick={handleBack}\n className=\"p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition\"\n aria-label=\"Go back\"\n >\n <svg\n className=\"h-5 w-5 text-gray-600 dark:text-gray-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 19l-7-7 7-7\"\n />\n </svg>\n </button>\n )}\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white\">\n {currentPage.name}\n </h3>\n </div>\n <div className=\"text-sm text-gray-500 dark:text-gray-400\">\n {gridRows}×{gridCols} grid\n </div>\n </div>\n\n {/* Grid */}\n <div\n className=\"p-4 gap-2 overflow-auto max-h-[600px]\"\n style={{\n display: 'grid',\n gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`,\n }}\n >\n {(() => {\n const rendered = new Set<string>();\n return currentPage.grid.flatMap((row, rowIndex) =>\n row.map((button, colIndex) => {\n if (!button) {\n return <div key={`empty-${rowIndex}-${colIndex}`} className=\"aspect-square\" />;\n }\n\n if (rendered.has(button.id)) {\n return null;\n }\n rendered.add(button.id);\n\n const buttonMetric = buttonMetricsLookup[button.id];\n const effort = buttonMetric?.effort || 0;\n const targetPageId = button.targetPageId || button.semanticAction?.targetId;\n const hasLink = targetPageId && tree.pages[targetPageId];\n const colSpan = button.columnSpan || 1;\n const rowSpan = button.rowSpan || 1;\n const predictions =\n button.predictions || (button.parameters as { predictions?: string[] })?.predictions;\n const hasPredictions = predictions && predictions.length > 0;\n const isPredictionCell =\n button.contentType === 'AutoContent' &&\n (button.contentSubType || '').toLowerCase() === 'prediction';\n const isWorkspace = button.contentType === 'Workspace';\n\n const imageSrc =\n (button.resolvedImageEntry && !String(button.resolvedImageEntry).startsWith('[')\n ? button.resolvedImageEntry\n : null) ||\n (button.image && !String(button.image).startsWith('[') ? button.image : null);\n\n if (isWorkspace) {\n return (\n <div\n key={button.id}\n className=\"relative p-3 rounded-lg border-2 bg-gray-50 text-gray-800 flex items-center gap-2\"\n style={{\n borderColor: button.style?.borderColor || '#e5e7eb',\n gridColumn: `${colIndex + 1} / span ${colSpan}`,\n gridRow: `${rowIndex + 1} / span ${rowSpan}`,\n }}\n >\n <div className=\"font-semibold text-sm\">{button.label || 'Workspace'}</div>\n <div className=\"text-xs text-gray-500 truncate\">\n {message || 'Chat writing area'}\n </div>\n </div>\n );\n }\n\n return (\n <button\n key={button.id}\n onClick={() => handleButtonClick(button)}\n 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\"\n style={{\n backgroundColor: button.style?.backgroundColor || '#f3f4f6',\n borderColor: button.style?.borderColor || '#e5e7eb',\n color: button.style?.fontColor || undefined,\n gridColumn: `${colIndex + 1} / span ${colSpan}`,\n gridRow: `${rowIndex + 1} / span ${rowSpan}`,\n }}\n >\n {/* Effort Badge */}\n {buttonMetric && showEffortBadges && (\n <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\">\n {effort.toFixed(1)}\n </div>\n )}\n\n {/* Link Indicator */}\n {hasLink && showLinkIndicators && (\n <div className=\"absolute top-1 left-1 w-2 h-2 bg-green-500 rounded-full shadow-sm\" />\n )}\n\n {/* Predictions Indicator */}\n {hasPredictions && (\n <div\n onClick={(e) => handleShowPredictions(button, e)}\n 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\"\n title={`Has ${predictions?.length} word form${predictions && predictions.length > 1 ? 's' : ''}`}\n >\n {predictions?.length}\n </div>\n )}\n\n {/* Image */}\n {imageSrc && (\n <img\n src={imageSrc}\n alt={button.label}\n className=\"max-h-12 object-contain\"\n />\n )}\n\n {/* Label / Predictions */}\n <div className=\"flex flex-col items-center justify-center\">\n <span\n className={`text-xs sm:text-sm text-center font-medium leading-tight line-clamp-3 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.label}\n </span>\n {isPredictionCell && predictions && predictions.length > 0 && (\n <div className=\"mt-1 text-[10px] sm:text-xs text-center opacity-80 space-y-0.5\">\n {predictions.slice(0, 3).map((p, idx) => (\n <div key={`${button.id}-pred-${idx}`}>\n {p}\n </div>\n ))}\n {predictions.length > 3 && <div>…</div>}\n </div>\n )}\n </div>\n\n {/* Message (if different from label) */}\n {button.message && button.message !== button.label && !isPredictionCell && (\n <span\n className={`text-[10px] sm:text-xs text-center opacity-75 line-clamp-2 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.message}\n </span>\n )}\n </button>\n );\n })\n );\n })()}\n </div>\n\n {/* Page Navigation (if multiple pages) */}\n {Object.keys(tree.pages).length > 1 && (\n <div className=\"p-4 border-t border-gray-200 dark:border-gray-700\">\n <p className=\"text-sm text-gray-600 dark:text-gray-400 mb-2\">\n {Object.keys(tree.pages).length} pages in this vocabulary\n </p>\n </div>\n )}\n </div>\n </div>\n\n {/* Predictions Tooltip */}\n {predictionsTooltip && (\n <PredictionsTooltip\n predictions={predictionsTooltip.predictions}\n label={predictionsTooltip.label}\n position={predictionsTooltip.position}\n onClose={() => setPredictionsTooltip(null)}\n />\n )}\n </div>\n );\n}\n","/**\n * React hooks for AAC Board Viewer\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { loadAACFileFromURL, calculateMetrics } from '../utils/loaders';\nimport type { AACTree } from '@willwade/aac-processors';\nimport type { ButtonMetric, MetricsOptions } from '../types';\n\n/**\n * Hook to load an AAC file from a URL\n *\n * @param url - URL to the AAC file\n * @param options - Processor options (e.g., pageLayoutPreference for SNAP)\n * @returns Object with tree, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer() {\n * const { tree, loading, error, reload } = useAACFile('/files/board.sps');\n *\n * if (loading) return <div>Loading...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n *\n * return <BoardViewer tree={tree} />;\n * }\n * ```\n */\nexport function useAACFile(\n url: string,\n options?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (options?.enabled === false) {\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFileFromURL(url, options?.processorOptions);\n setTree(loadedTree);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [url, options]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to load an AAC file and calculate metrics\n *\n * @param url - URL to the AAC file\n * @param metricsOptions - Metrics calculation options\n * @returns Object with tree, metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer() {\n * const { tree, metrics, loading, error } = useAACFileWithMetrics(\n * '/files/board.sps',\n * { accessMethod: 'direct' }\n * );\n *\n * if (loading) return <div>Loading...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n *\n * return <BoardViewer tree={tree} buttonMetrics={metrics} />;\n * }\n * ```\n */\nexport function useAACFileWithMetrics(\n url: string,\n metricsOptions?: MetricsOptions,\n fileOptions?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [metrics, setMetrics] = useState<ButtonMetric[] | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (fileOptions?.enabled === false) {\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFileFromURL(url, fileOptions?.processorOptions);\n setTree(loadedTree);\n\n // Calculate metrics\n const calculatedMetrics = await calculateMetrics(loadedTree, metricsOptions || {});\n setMetrics(calculatedMetrics);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [url, metricsOptions, fileOptions]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n metrics,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to calculate metrics for a tree\n *\n * @param tree - The AAC tree\n * @param options - Metrics calculation options\n * @returns Object with metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer({ tree }) {\n * const { metrics, loading } = useMetrics(tree, {\n * accessMethod: 'scanning',\n * scanningConfig: { pattern: 'row-column' }\n * });\n *\n * return <BoardViewer tree={tree} buttonMetrics={metrics} />;\n * }\n * ```\n */\nexport function useMetrics(\n tree: AACTree | null,\n options?: MetricsOptions\n) {\n const [metrics, setMetrics] = useState<ButtonMetric[] | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const calculate = useCallback(async () => {\n if (!tree) {\n setMetrics(null);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const calculatedMetrics = await calculateMetrics(tree, options || {});\n setMetrics(calculatedMetrics);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to calculate metrics'));\n } finally {\n setLoading(false);\n }\n }, [tree, options]);\n\n useEffect(() => {\n calculate();\n }, [calculate]);\n\n return {\n metrics,\n loading,\n error,\n recalculate: calculate,\n };\n}\n\n/**\n * Hook for sentence building state\n *\n * @returns Object with message state and handlers\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { message, wordCount, effort, addWord, clear } = useSentenceBuilder();\n *\n * return (\n * <div>\n * <p>{message || 'Start building...'}</p>\n * <p>{wordCount} words, {effort.toFixed(2)} effort</p>\n * <button onClick={clear}>Clear</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useSentenceBuilder() {\n const [message, setMessage] = useState('');\n const [wordCount, setWordCount] = useState(0);\n const [effort, setEffort] = useState(0);\n\n const addWord = useCallback((word: string, wordEffort: number = 1) => {\n setMessage((prev) => {\n const newMessage = prev + (prev ? ' ' : '') + word;\n setWordCount((prev) => prev + 1);\n setEffort((prev) => prev + wordEffort);\n return newMessage;\n });\n }, []);\n\n const clear = useCallback(() => {\n setMessage('');\n setWordCount(0);\n setEffort(0);\n }, []);\n\n return {\n message,\n wordCount,\n effort,\n addWord,\n clear,\n };\n}\n","/**\n * AAC File Loading Utilities\n *\n * Provides utilities for loading AAC files from various sources\n * (URLs, File objects, file paths) in both client and server contexts.\n */\n\nimport type { AACTree } from '@willwade/aac-processors';\nimport type { LoadAACFileResult } from '../types';\n\ntype ProcessorOptions = Record<string, unknown> | undefined;\n\ntype ProcessorModule = typeof import('@willwade/aac-processors');\n\n// Lazily load processors so browser bundles avoid pulling in Node APIs\nasync function importProcessors(): Promise<ProcessorModule> {\n return import('@willwade/aac-processors');\n}\n\n/**\n * Get the appropriate processor for a file based on its extension\n */\nasync function getProcessorForFile(filepath: string, options?: ProcessorOptions) {\n const {\n getProcessor,\n GridsetProcessor,\n SnapProcessor,\n TouchChatProcessor,\n ObfProcessor,\n ApplePanelsProcessor,\n AstericsGridProcessor,\n OpmlProcessor,\n ExcelProcessor,\n DotProcessor,\n } = await importProcessors();\n\n const ext = filepath.toLowerCase();\n\n // GridSet files (.gridset)\n if (ext.endsWith('.gridset')) {\n return new GridsetProcessor();\n }\n\n // SNAP files (.sps, .spb)\n if (ext.endsWith('.sps') || ext.endsWith('.spb')) {\n return options ? new SnapProcessor(null, options) : new SnapProcessor();\n }\n\n // TouchChat files (.ce)\n if (ext.endsWith('.ce')) {\n return new TouchChatProcessor();\n }\n\n // OpenBoard files (.obf, .obz)\n if (ext.endsWith('.obf')) {\n return new ObfProcessor();\n }\n if (ext.endsWith('.obz')) {\n return new ObfProcessor();\n }\n\n // Asterics Grid files (.grd)\n if (ext.endsWith('.grd')) {\n return new AstericsGridProcessor();\n }\n\n // Apple Panels files\n if (ext.endsWith('.plist')) {\n return new ApplePanelsProcessor();\n }\n\n // OPML files\n if (ext.endsWith('.opml')) {\n return new OpmlProcessor();\n }\n\n // Excel files\n if (ext.endsWith('.xlsx') || ext.endsWith('.xls')) {\n return new ExcelProcessor();\n }\n\n // DOT files\n if (ext.endsWith('.dot')) {\n return new DotProcessor();\n }\n\n // Fallback to generic processor detection\n return getProcessor(filepath);\n}\n\n/**\n * Load an AAC file from a file path (server-side only)\n *\n * @param filepath - Path to the AAC file\n * @param options - Processor options (e.g., pageLayoutPreference for SNAP)\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const tree = await loadAACFile('/path/to/file.sps');\n * ```\n */\nexport async function loadAACFile(\n filepath: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const processor = await getProcessorForFile(filepath, options);\n return processor.loadIntoTree(filepath);\n}\n\n/**\n * Load an AAC file and return extended result with format info\n *\n * @param filepath - Path to the AAC file\n * @param options - Processor options\n * @returns Promise resolving to LoadAACFileResult\n */\nexport async function loadAACFileWithMetadata(\n filepath: string,\n options?: ProcessorOptions\n): Promise<LoadAACFileResult> {\n const tree = await loadAACFile(filepath, options);\n\n // Detect format from file extension\n const ext = filepath.toLowerCase();\n let format = 'unknown';\n\n if (ext.endsWith('.gridset')) format = 'gridset';\n else if (ext.endsWith('.sps') || ext.endsWith('.spb')) format = 'snap';\n else if (ext.endsWith('.ce')) format = 'touchchat';\n else if (ext.endsWith('.obf')) format = 'openboard';\n else if (ext.endsWith('.obz')) format = 'openboard';\n else if (ext.endsWith('.grd')) format = 'asterics-grid';\n else if (ext.endsWith('.plist')) format = 'apple-panels';\n else if (ext.endsWith('.opml')) format = 'opml';\n else if (ext.endsWith('.xlsx') || ext.endsWith('.xls')) format = 'excel';\n else if (ext.endsWith('.dot')) format = 'dot';\n\n return {\n tree,\n format,\n metadata: tree.metadata,\n };\n}\n\n/**\n * Load an AAC file from a URL (client-side)\n *\n * Note: This requires the server to provide the file with appropriate CORS headers.\n * For better performance, consider server-side loading instead.\n *\n * @param url - URL to the AAC file\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const tree = await loadAACFileFromURL('https://example.com/file.sps');\n * ```\n */\nexport async function loadAACFileFromURL(\n url: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`Failed to load file: ${response.statusText}`);\n }\n\n const blob = await response.blob();\n const filename = getFilenameFromURL(url);\n\n return loadAACFileFromFile(blob, filename, options);\n}\n\n/**\n * Load an AAC file from a File object (client-side file input)\n *\n * @param file - File object from file input\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const input = document.querySelector('input[type=\"file\"]');\n * input.onchange = async (e) => {\n * const file = e.target.files[0];\n * const tree = await loadAACFileFromFile(file);\n * // Use tree...\n * };\n * ```\n */\nexport async function loadAACFileFromFile(\n _file: File | Blob,\n _filename?: string,\n _options?: unknown\n): Promise<AACTree> {\n throw new Error('Client-side file loading not yet fully implemented. Please use server-side loading or loadAACFileFromURL with proper CORS headers.');\n}\n\n/**\n * Extract filename from URL\n */\nfunction getFilenameFromURL(url: string): string {\n try {\n const urlObj = new URL(url);\n const pathname = urlObj.pathname;\n const parts = pathname.split('/');\n return parts[parts.length - 1] || 'unknown';\n } catch {\n return 'unknown';\n }\n}\n\n/**\n * Calculate cognitive effort metrics for an AAC tree\n *\n * @param tree - The AAC tree\n * @param options - Metrics calculation options\n * @returns Promise resolving to array of ButtonMetrics\n *\n * @example\n * ```ts\n * import { calculateMetrics } from 'aac-board-viewer';\n *\n * const metrics = await calculateMetrics(tree, {\n * accessMethod: 'direct',\n * });\n * ```\n */\nexport async function calculateMetrics(\n tree: AACTree,\n options: {\n accessMethod?: 'direct' | 'scanning';\n scanningConfig?: {\n pattern?: 'linear' | 'row-column' | 'block';\n selectionMethod?: string;\n errorCorrection?: boolean;\n };\n } = {}\n) {\n // Import MetricsCalculator dynamically to avoid circular dependencies\n const { MetricsCalculator } = await import('@willwade/aac-processors');\n\n const calculator = new MetricsCalculator();\n\n let metricsOptions: Record<string, unknown> = {};\n\n if (options.accessMethod === 'scanning' && options.scanningConfig) {\n // Import scanning enums\n const { CellScanningOrder, ScanningSelectionMethod } = await import('@willwade/aac-processors');\n\n let cellScanningOrder = CellScanningOrder.SimpleScan;\n let blockScanEnabled = false;\n\n switch (options.scanningConfig.pattern) {\n case 'linear':\n cellScanningOrder = CellScanningOrder.SimpleScan;\n break;\n case 'row-column':\n cellScanningOrder = CellScanningOrder.RowColumnScan;\n break;\n case 'block':\n cellScanningOrder = CellScanningOrder.RowColumnScan;\n blockScanEnabled = true;\n break;\n }\n\n metricsOptions = {\n scanningConfig: {\n cellScanningOrder,\n blockScanEnabled,\n selectionMethod: ScanningSelectionMethod.AutoScan,\n errorCorrectionEnabled: options.scanningConfig.errorCorrection || false,\n errorRate: 0.1,\n },\n };\n }\n\n const metricsResult = calculator.analyze(tree, metricsOptions);\n\n // Convert to the format expected by BoardViewer\n type MetricsButton = {\n id: string;\n label: string;\n effort: number;\n count?: number;\n level?: number;\n semantic_id?: string;\n clone_id?: string;\n };\n\n return metricsResult.buttons.map((btn: MetricsButton) => ({\n id: btn.id,\n label: btn.label,\n effort: btn.effort,\n count: btn.count ?? 0,\n is_word: true,\n level: btn.level,\n semantic_id: btn.semantic_id,\n clone_id: btn.clone_id,\n }));\n}\n\n/**\n * Get a list of supported file formats\n *\n * @returns Array of format information\n */\nexport function getSupportedFormats(): Array<{\n name: string;\n extensions: string[];\n description: string;\n}> {\n return [\n {\n name: 'Grid 3',\n extensions: ['.gridset'],\n description: 'Smartbox Grid 3 communication boards',\n },\n {\n name: 'TD Snap',\n extensions: ['.sps', '.spb'],\n description: 'Tobii Dynavox Snap files',\n },\n {\n name: 'TouchChat',\n extensions: ['.ce'],\n description: 'Saltillo TouchChat files',\n },\n {\n name: 'OpenBoard',\n extensions: ['.obf', '.obz'],\n description: 'OpenBoard Format (OBZ/OBF)',\n },\n {\n name: 'Asterics Grid',\n extensions: ['.grd'],\n description: 'Asterics Grid files (.grd)',\n },\n {\n name: 'Apple Panels',\n extensions: ['.plist'],\n description: 'Apple iOS Panels files',\n },\n {\n name: 'OPML',\n extensions: ['.opml'],\n description: 'OPML outline files',\n },\n {\n name: 'Excel',\n extensions: ['.xlsx', '.xls'],\n description: 'Excel spreadsheet boards',\n },\n {\n name: 'DOT',\n extensions: ['.dot'],\n description: 'DOT graph visualization files',\n },\n ];\n}\n"],"mappings":"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","names":["useState","useCallback","useState","useCallback","prev"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aac-board-viewer",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Universal AAC board viewer component for React. Supports GridSet, TouchChat, SNAP, OpenBoard, Asterics, Apple Panels, and more.",
5
5
  "keywords": [
6
6
  "aac",
@@ -54,7 +54,7 @@
54
54
  "react-dom": "^18.0.0"
55
55
  },
56
56
  "dependencies": {
57
- "@willwade/aac-processors": "^0.0.23"
57
+ "@willwade/aac-processors": "^0.0.26"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/react": "^18.3.0",