aac-board-viewer 0.1.2 → 0.1.4

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,60 @@ 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, buttonMetricsLookup, 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) => {
86
+ const metricForWord = Object.values(buttonMetricsLookup).find((m) => m.label === word);
87
+ const effort = metricForWord?.effort;
88
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
89
+ "span",
90
+ {
91
+ className: "px-2 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded text-xs font-medium relative",
92
+ children: [
93
+ effort !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "absolute -top-1 -right-1 px-1 py-0 text-[8px] font-semibold rounded bg-blue-600 text-white shadow-xs", children: effort.toFixed(1) }),
94
+ word
95
+ ]
96
+ },
97
+ idx
98
+ );
99
+ }) })
100
+ ]
101
+ }
102
+ );
103
+ }
50
104
  function BoardViewer({
51
105
  tree,
52
106
  buttonMetrics,
@@ -72,6 +126,12 @@ function BoardViewer({
72
126
  return tree.rootId;
73
127
  }
74
128
  }
129
+ const startPage = Object.values(tree.pages).find(
130
+ (p) => p.name.toLowerCase() === "start"
131
+ );
132
+ if (startPage) {
133
+ return startPage.id;
134
+ }
75
135
  const nonToolbarPage = Object.values(tree.pages).find(
76
136
  (p) => !p.name.toLowerCase().includes("toolbar") && !p.name.toLowerCase().includes("tool bar")
77
137
  );
@@ -86,6 +146,7 @@ function BoardViewer({
86
146
  const [message, setMessage] = (0, import_react.useState)("");
87
147
  const [currentWordCount, setCurrentWordCount] = (0, import_react.useState)(0);
88
148
  const [currentEffort, setCurrentEffort] = (0, import_react.useState)(0);
149
+ const [predictionsTooltip, setPredictionsTooltip] = (0, import_react.useState)(null);
89
150
  const buttonMetricsLookup = (0, import_react.useMemo)(() => {
90
151
  if (!buttonMetrics) return {};
91
152
  const lookup = {};
@@ -95,6 +156,17 @@ function BoardViewer({
95
156
  return lookup;
96
157
  }, [buttonMetrics]);
97
158
  const currentPage = currentPageId ? tree.pages[currentPageId] : null;
159
+ const goToPage = (targetPageId) => {
160
+ if (!targetPageId || !tree.pages[targetPageId]) return false;
161
+ if (currentPage) {
162
+ setPageHistory((prev) => [...prev, currentPage]);
163
+ }
164
+ setCurrentPageId(targetPageId);
165
+ if (onPageChange) {
166
+ onPageChange(targetPageId);
167
+ }
168
+ return true;
169
+ };
98
170
  import_react.default.useEffect(() => {
99
171
  setCurrentPageId(resolveInitialPageId());
100
172
  setPageHistory([]);
@@ -107,24 +179,62 @@ function BoardViewer({
107
179
  if (onButtonClick) {
108
180
  onButtonClick(button);
109
181
  }
182
+ const intent = button.semanticAction?.intent ? String(button.semanticAction.intent) : void 0;
110
183
  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
- }
184
+ const effort = buttonMetricsLookup[button.id]?.effort || 1;
185
+ const textValue = button.semanticAction?.text || button.message || button.label || "";
186
+ const deleteLastWord = () => {
187
+ setMessage((prev) => {
188
+ const parts = prev.trim().split(/\s+/);
189
+ parts.pop();
190
+ const newMsg = parts.join(" ");
191
+ return newMsg;
192
+ });
193
+ };
194
+ const deleteLastCharacter = () => {
195
+ setMessage((prev) => prev.slice(0, -1));
196
+ };
197
+ const appendText = (word) => {
198
+ const trimmed = word || button.label || "";
199
+ setMessage((prev) => {
200
+ const newMessage = trimmed ? prev + (prev ? " " : "") + trimmed : prev;
201
+ if (trimmed) {
202
+ updateStats(trimmed, effort);
203
+ }
204
+ return newMessage;
205
+ });
206
+ };
207
+ if (intent === "NAVIGATE_TO" && goToPage(targetPageId)) {
119
208
  return;
120
209
  }
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
- });
210
+ switch (intent) {
211
+ case "GO_BACK":
212
+ handleBack();
213
+ return;
214
+ case "GO_HOME":
215
+ if (tree.rootId && goToPage(tree.rootId)) return;
216
+ break;
217
+ case "DELETE_WORD":
218
+ deleteLastWord();
219
+ return;
220
+ case "DELETE_CHARACTER":
221
+ deleteLastCharacter();
222
+ return;
223
+ case "CLEAR_TEXT":
224
+ clearMessage();
225
+ return;
226
+ case "SPEAK_IMMEDIATE":
227
+ case "SPEAK_TEXT":
228
+ case "INSERT_TEXT":
229
+ appendText(textValue);
230
+ return;
231
+ default:
232
+ break;
233
+ }
234
+ if (targetPageId && goToPage(targetPageId)) {
235
+ return;
236
+ }
237
+ appendText(textValue);
128
238
  };
129
239
  const handleBack = () => {
130
240
  if (pageHistory.length > 0) {
@@ -141,6 +251,18 @@ function BoardViewer({
141
251
  setCurrentWordCount(0);
142
252
  setCurrentEffort(0);
143
253
  };
254
+ const handleShowPredictions = (button, event) => {
255
+ event.stopPropagation();
256
+ const predictions = button.predictions || button.parameters?.predictions;
257
+ if (predictions && predictions.length > 0) {
258
+ setPredictionsTooltip({
259
+ predictions,
260
+ label: button.label,
261
+ position: { x: event.clientX, y: event.clientY },
262
+ buttonMetricsLookup
263
+ });
264
+ }
265
+ };
144
266
  const getTextColor = (backgroundColor) => {
145
267
  if (!backgroundColor) return "text-gray-900 dark:text-gray-100";
146
268
  const hex = backgroundColor.replace("#", "");
@@ -292,52 +414,109 @@ ${button.message || ""}`,
292
414
  display: "grid",
293
415
  gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`
294
416
  },
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
417
+ children: (() => {
418
+ const rendered = /* @__PURE__ */ new Set();
419
+ return currentPage.grid.flatMap(
420
+ (row, rowIndex) => row.map((button, colIndex) => {
421
+ if (!button) {
422
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "aspect-square" }, `empty-${rowIndex}-${colIndex}`);
423
+ }
424
+ if (rendered.has(button.id)) {
425
+ return null;
426
+ }
427
+ rendered.add(button.id);
428
+ const buttonMetric = buttonMetricsLookup[button.id];
429
+ const effort = buttonMetric?.effort || 0;
430
+ const targetPageId = button.targetPageId || button.semanticAction?.targetId;
431
+ const hasLink = targetPageId && tree.pages[targetPageId];
432
+ const colSpan = button.columnSpan || 1;
433
+ const rowSpan = button.rowSpan || 1;
434
+ const predictions = button.predictions || button.parameters?.predictions;
435
+ const hasPredictions = predictions && predictions.length > 0;
436
+ const isPredictionCell = button.contentType === "AutoContent" && (button.contentSubType || "").toLowerCase() === "prediction";
437
+ const isWorkspace = button.contentType === "Workspace";
438
+ const imageSrc = (button.resolvedImageEntry && !String(button.resolvedImageEntry).startsWith("[") ? button.resolvedImageEntry : null) || (button.image && !String(button.image).startsWith("[") ? button.image : null);
439
+ if (isWorkspace) {
440
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
441
+ "div",
442
+ {
443
+ className: "relative p-3 rounded-lg border-2 bg-gray-50 text-gray-800 flex items-center gap-2",
444
+ style: {
445
+ borderColor: button.style?.borderColor || "#e5e7eb",
446
+ gridColumn: `${colIndex + 1} / span ${colSpan}`,
447
+ gridRow: `${rowIndex + 1} / span ${rowSpan}`
448
+ },
449
+ children: [
450
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "font-semibold text-sm", children: button.label || "Workspace" }),
451
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "text-xs text-gray-500 truncate", children: message || "Chat writing area" })
452
+ ]
453
+ },
454
+ button.id
455
+ );
456
+ }
457
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
458
+ "button",
459
+ {
460
+ onClick: () => handleButtonClick(button),
461
+ 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",
462
+ style: {
463
+ backgroundColor: button.style?.backgroundColor || "#f3f4f6",
464
+ borderColor: button.style?.borderColor || "#e5e7eb",
465
+ color: button.style?.fontColor || void 0,
466
+ gridColumn: `${colIndex + 1} / span ${colSpan}`,
467
+ gridRow: `${rowIndex + 1} / span ${rowSpan}`
468
+ },
469
+ children: [
470
+ 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) }),
471
+ 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" }),
472
+ hasPredictions && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
473
+ "div",
474
+ {
475
+ onClick: (e) => handleShowPredictions(button, e),
476
+ 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",
477
+ title: `Has ${predictions?.length} word form${predictions && predictions.length > 1 ? "s" : ""}`,
478
+ children: predictions?.length
479
+ }
480
+ ),
481
+ imageSrc && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
482
+ "img",
483
+ {
484
+ src: imageSrc,
485
+ alt: button.label,
486
+ className: "max-h-12 object-contain"
487
+ }
488
+ ),
489
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex flex-col items-center justify-center", children: [
490
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
491
+ "span",
492
+ {
493
+ className: `text-xs sm:text-sm text-center font-medium leading-tight line-clamp-3 ${getTextColor(
494
+ button.style?.backgroundColor
495
+ )}`,
496
+ children: button.label
497
+ }
498
+ ),
499
+ 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: [
500
+ predictions.slice(0, 3).map((p, idx) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: p }, `${button.id}-pred-${idx}`)),
501
+ predictions.length > 3 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { children: "\u2026" })
502
+ ] })
503
+ ] }),
504
+ button.message && button.message !== button.label && !isPredictionCell && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
505
+ "span",
506
+ {
507
+ className: `text-[10px] sm:text-xs text-center opacity-75 line-clamp-2 ${getTextColor(
508
+ button.style?.backgroundColor
509
+ )}`,
510
+ children: button.message
511
+ }
512
+ )
513
+ ]
313
514
  },
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
- )
515
+ button.id
516
+ );
517
+ })
518
+ );
519
+ })()
341
520
  }
342
521
  ),
343
522
  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 +524,17 @@ ${button.message || ""}`,
345
524
  " pages in this vocabulary"
346
525
  ] }) })
347
526
  ] })
348
- ] })
527
+ ] }),
528
+ predictionsTooltip && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
529
+ PredictionsTooltip,
530
+ {
531
+ predictions: predictionsTooltip.predictions,
532
+ label: predictionsTooltip.label,
533
+ position: predictionsTooltip.position,
534
+ buttonMetricsLookup: predictionsTooltip.buttonMetricsLookup,
535
+ onClose: () => setPredictionsTooltip(null)
536
+ }
537
+ )
349
538
  ] });
350
539
  }
351
540
 
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 buttonMetricsLookup: { [buttonId: string]: ButtonMetric };\n onClose: () => void;\n}\n\nfunction PredictionsTooltip({ predictions, label, position, buttonMetricsLookup, 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 // Try to find metrics for this word form by label\n const metricForWord = Object.values(buttonMetricsLookup).find(m => m.label === word);\n const effort = metricForWord?.effort;\n\n return (\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 relative\"\n >\n {effort !== undefined && (\n <span className=\"absolute -top-1 -right-1 px-1 py-0 text-[8px] font-semibold rounded bg-blue-600 text-white shadow-xs\">\n {effort.toFixed(1)}\n </span>\n )}\n {word}\n </span>\n );\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 buttonMetricsLookup: { [buttonId: string]: ButtonMetric };\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 buttonMetricsLookup,\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 buttonMetricsLookup={predictionsTooltip.buttonMetricsLookup}\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"]}