aac-board-viewer 0.1.4 → 0.1.5

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,7 +47,7 @@ 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 }) {
50
+ function PredictionsTooltip({ predictions, label, position, buttonMetricsLookup, onClose, onWordClick }) {
51
51
  import_react.default.useEffect(() => {
52
52
  const handleClickOutside = (e) => {
53
53
  if (e.target instanceof HTMLElement && !e.target.closest(".predictions-tooltip")) {
@@ -65,6 +65,7 @@ function PredictionsTooltip({ predictions, label, position, buttonMetricsLookup,
65
65
  left: `${Math.min(position.x, window.innerWidth - 200)}px`,
66
66
  top: `${Math.min(position.y, window.innerHeight - 150)}px`
67
67
  },
68
+ onClick: (e) => e.stopPropagation(),
68
69
  children: [
69
70
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "flex items-center justify-between mb-2", children: [
70
71
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h4", { className: "text-sm font-semibold text-gray-900 dark:text-white", children: [
@@ -88,7 +89,15 @@ function PredictionsTooltip({ predictions, label, position, buttonMetricsLookup,
88
89
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
89
90
  "span",
90
91
  {
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
+ 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 cursor-pointer hover:bg-purple-200 dark:hover:bg-purple-800 transition",
93
+ onClick: (e) => {
94
+ e.stopPropagation();
95
+ if (onWordClick) {
96
+ onWordClick(word, effort || 1);
97
+ onClose();
98
+ }
99
+ },
100
+ title: `Click to add "${word}" to message`,
92
101
  children: [
93
102
  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
103
  word
@@ -259,7 +268,17 @@ function BoardViewer({
259
268
  predictions,
260
269
  label: button.label,
261
270
  position: { x: event.clientX, y: event.clientY },
262
- buttonMetricsLookup
271
+ buttonMetricsLookup,
272
+ onWordClick: (word, effort) => {
273
+ const trimmed = word || "";
274
+ if (trimmed) {
275
+ setMessage((prev) => {
276
+ const newMessage = prev + (prev ? " " : "") + trimmed;
277
+ updateStats(trimmed, effort);
278
+ return newMessage;
279
+ });
280
+ }
281
+ }
263
282
  });
264
283
  }
265
284
  };
@@ -532,6 +551,7 @@ ${button.message || ""}`,
532
551
  label: predictionsTooltip.label,
533
552
  position: predictionsTooltip.position,
534
553
  buttonMetricsLookup: predictionsTooltip.buttonMetricsLookup,
554
+ onWordClick: predictionsTooltip.onWordClick,
535
555
  onClose: () => setPredictionsTooltip(null)
536
556
  }
537
557
  )
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 * 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"]}
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 onWordClick?: (word: string, effort: number) => void;\n}\n\nfunction PredictionsTooltip({ predictions, label, position, buttonMetricsLookup, onClose, onWordClick }: 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 onClick={(e) => e.stopPropagation()} // Prevent clicks from bubbling to underlying button\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 cursor-pointer hover:bg-purple-200 dark:hover:bg-purple-800 transition\"\n onClick={(e) => {\n e.stopPropagation(); // Prevent bubbling\n if (onWordClick) {\n onWordClick(word, effort || 1.0);\n onClose();\n }\n }}\n title={`Click to add \"${word}\" to message`}\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 onWordClick?: (word: string, effort: number) => void;\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 onWordClick: (word, effort) => {\n const trimmed = word || '';\n if (trimmed) {\n setMessage((prev) => {\n const newMessage = prev + (prev ? ' ' : '') + trimmed;\n updateStats(trimmed, effort);\n return newMessage;\n });\n }\n },\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 onWordClick={predictionsTooltip.onWordClick}\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,7 +1,7 @@
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, buttonMetricsLookup, onClose }) {
4
+ function PredictionsTooltip({ predictions, label, position, buttonMetricsLookup, onClose, onWordClick }) {
5
5
  React.useEffect(() => {
6
6
  const handleClickOutside = (e) => {
7
7
  if (e.target instanceof HTMLElement && !e.target.closest(".predictions-tooltip")) {
@@ -19,6 +19,7 @@ function PredictionsTooltip({ predictions, label, position, buttonMetricsLookup,
19
19
  left: `${Math.min(position.x, window.innerWidth - 200)}px`,
20
20
  top: `${Math.min(position.y, window.innerHeight - 150)}px`
21
21
  },
22
+ onClick: (e) => e.stopPropagation(),
22
23
  children: [
23
24
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-2", children: [
24
25
  /* @__PURE__ */ jsxs("h4", { className: "text-sm font-semibold text-gray-900 dark:text-white", children: [
@@ -42,7 +43,15 @@ function PredictionsTooltip({ predictions, label, position, buttonMetricsLookup,
42
43
  return /* @__PURE__ */ jsxs(
43
44
  "span",
44
45
  {
45
- 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",
46
+ 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 cursor-pointer hover:bg-purple-200 dark:hover:bg-purple-800 transition",
47
+ onClick: (e) => {
48
+ e.stopPropagation();
49
+ if (onWordClick) {
50
+ onWordClick(word, effort || 1);
51
+ onClose();
52
+ }
53
+ },
54
+ title: `Click to add "${word}" to message`,
46
55
  children: [
47
56
  effort !== void 0 && /* @__PURE__ */ 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) }),
48
57
  word
@@ -213,7 +222,17 @@ function BoardViewer({
213
222
  predictions,
214
223
  label: button.label,
215
224
  position: { x: event.clientX, y: event.clientY },
216
- buttonMetricsLookup
225
+ buttonMetricsLookup,
226
+ onWordClick: (word, effort) => {
227
+ const trimmed = word || "";
228
+ if (trimmed) {
229
+ setMessage((prev) => {
230
+ const newMessage = prev + (prev ? " " : "") + trimmed;
231
+ updateStats(trimmed, effort);
232
+ return newMessage;
233
+ });
234
+ }
235
+ }
217
236
  });
218
237
  }
219
238
  };
@@ -486,6 +505,7 @@ ${button.message || ""}`,
486
505
  label: predictionsTooltip.label,
487
506
  position: predictionsTooltip.position,
488
507
  buttonMetricsLookup: predictionsTooltip.buttonMetricsLookup,
508
+ onWordClick: predictionsTooltip.onWordClick,
489
509
  onClose: () => setPredictionsTooltip(null)
490
510
  }
491
511
  )
@@ -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 * 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":["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 buttonMetricsLookup: { [buttonId: string]: ButtonMetric };\n onClose: () => void;\n onWordClick?: (word: string, effort: number) => void;\n}\n\nfunction PredictionsTooltip({ predictions, label, position, buttonMetricsLookup, onClose, onWordClick }: 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 onClick={(e) => e.stopPropagation()} // Prevent clicks from bubbling to underlying button\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 cursor-pointer hover:bg-purple-200 dark:hover:bg-purple-800 transition\"\n onClick={(e) => {\n e.stopPropagation(); // Prevent bubbling\n if (onWordClick) {\n onWordClick(word, effort || 1.0);\n onClose();\n }\n }}\n title={`Click to add \"${word}\" to message`}\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 onWordClick?: (word: string, effort: number) => void;\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 onWordClick: (word, effort) => {\n const trimmed = word || '';\n if (trimmed) {\n setMessage((prev) => {\n const newMessage = prev + (prev ? ' ' : '') + trimmed;\n updateStats(trimmed, effort);\n return newMessage;\n });\n }\n },\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 onWordClick={predictionsTooltip.onWordClick}\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.4",
3
+ "version": "0.1.5",
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.26"
57
+ "@willwade/aac-processors": "^0.0.27"
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/react": "^18.3.0",