aac-board-viewer 0.2.9 → 0.3.0
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/board-viewer.d.mts +2 -2
- package/dist/board-viewer.d.ts +2 -2
- package/dist/board-viewer.js +14 -2
- package/dist/board-viewer.js.map +1 -1
- package/dist/board-viewer.mjs +14 -2
- package/dist/board-viewer.mjs.map +1 -1
- package/dist/{index-BL1tS3BY.d.mts → index-B783kPn6.d.mts} +8 -11
- package/dist/{index-BL1tS3BY.d.ts → index-B783kPn6.d.ts} +8 -11
- package/dist/index.d.mts +7 -10
- package/dist/index.d.ts +7 -10
- package/dist/index.js +22 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +22 -4
- package/dist/index.mjs.map +1 -1
- package/dist/vue/index.js +21 -4
- package/dist/vue/index.js.map +1 -1
- package/dist/vue/index.mjs +21 -4
- package/dist/vue/index.mjs.map +1 -1
- package/dist/vue.d.mts +2 -2
- package/dist/vue.d.ts +2 -2
- package/package.json +5 -5
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 useAACFileFromFile,\n useAACFileWithMetrics,\n useAACFileFromFileWithMetrics,\n useMetrics,\n useSentenceBuilder,\n} from './hooks/useAACFile';\n\n// Utilities\nexport {\n loadAACFile,\n loadAACFileFromURL,\n loadAACFileWithMetadata,\n calculateMetrics,\n configureBrowserSqlJs,\n getSupportedFormats,\n isBrowserCompatible,\n getBrowserExtensions,\n getNodeOnlyExtensions,\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 "{label}"\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 navigateToPageId,\n highlight,\n onButtonClick,\n onPageChange,\n className = '',\n loadId,\n}: BoardViewerProps) {\n console.log('[BoardViewer] COMPONENT LOADED - Running updated code!');\n console.log('[BoardViewer] loadId:', loadId);\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 highlightedButtonRef = React.useRef<HTMLButtonElement | null>(null);\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 React.useEffect(() => {\n if (!navigateToPageId) return;\n if (!tree.pages[navigateToPageId]) return;\n setCurrentPageId(navigateToPageId);\n setPageHistory([]);\n if (onPageChange) {\n onPageChange(navigateToPageId);\n }\n }, [navigateToPageId, onPageChange, tree.pages]);\n\n React.useEffect(() => {\n if (highlightedButtonRef.current) {\n highlightedButtonRef.current.scrollIntoView({\n behavior: 'smooth',\n block: 'center',\n inline: 'center',\n });\n }\n }, [currentPageId, highlight?.pageId, highlight?.x, highlight?.y, highlight?.label]);\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 '#111827';\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 ? '#111827' : '#f9fafb';\n }\n\n return '#111827';\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 color: button.style?.fontColor || getTextColor(button.style?.backgroundColor),\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\"\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 isHighlighted =\n highlight &&\n highlight.pageId === currentPageId &&\n (highlight.buttonId === button.id ||\n (highlight.x !== undefined && highlight.y !== undefined\n ? button.x === highlight.x && button.y === highlight.y\n : false) ||\n (highlight.label && button.label === highlight.label));\n\n // Determine the image source\n // Priority: data URLs (used directly) > file paths (via API for Grid3/OBZ)\n const params = button.parameters as {\n image_id?: string;\n gridPageName?: string;\n imageData?: Buffer;\n };\n\n let imageSrc: string | null = null;\n let apiUrl: string | undefined = undefined;\n\n // IMPORTANT: Only use string values, never Buffers or objects\n // Buffers don't serialize correctly across server/client boundary in SSR\n\n // First, check if we have a data URL (Snap files, OBZ, etc.)\n // NOTE: Check button.resolvedImageEntry first as it's the canonical source\n const resolvedEntry = button.resolvedImageEntry;\n const buttonImage = button.image;\n\n // Safely check resolvedImageEntry - must be a string\n if (resolvedEntry && typeof resolvedEntry === 'string' && resolvedEntry.startsWith('data:image/')) {\n // Snap files: resolvedImageEntry is a data URL string\n imageSrc = resolvedEntry;\n }\n // Safely check button.image - must be a string\n else if (buttonImage && typeof buttonImage === 'string' && buttonImage.startsWith('data:image/')) {\n // Fallback to button.image if it's a data URL string\n imageSrc = buttonImage;\n }\n // Grid3 file path (not a data URL, not a symbol reference)\n else if (\n resolvedEntry &&\n typeof resolvedEntry === 'string' &&\n !resolvedEntry.startsWith('[') &&\n !resolvedEntry.startsWith('data:image/')\n ) {\n // Grid3 files: use API endpoint for file paths\n if (loadId) {\n const entryPath = resolvedEntry;\n const pathMatch = entryPath.match(/^(?:Grids\\/)?(.+)$/);\n if (pathMatch) {\n apiUrl = `/api/image/${loadId}/${encodeURIComponent(pathMatch[1])}`;\n }\n } else {\n imageSrc = resolvedEntry;\n }\n }\n // Fallback to button.image if it's a string (not a symbol reference)\n else if (\n buttonImage &&\n typeof buttonImage === 'string' &&\n !buttonImage.startsWith('[')\n ) {\n imageSrc = buttonImage;\n }\n\n // For OBZ files with loadId and image_id, use API (but not if we already have a data URL)\n // Note: We check buttonImage.length > 1000 but we're NOT using the Buffer imageData\n if (loadId && !imageSrc && !apiUrl && params && params.image_id && typeof params.image_id === 'string') {\n apiUrl = `/api/image/${loadId}/${encodeURIComponent(params.image_id)}`;\n }\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 ref={isHighlighted ? highlightedButtonRef : undefined}\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 isHighlighted\n ? 'ring-4 ring-amber-400 ring-offset-2 ring-offset-white'\n : ''\n }`}\n style={{\n backgroundColor: button.style?.backgroundColor || '#f3f4f6',\n borderColor: button.style?.borderColor || '#e5e7eb',\n color: button.style?.fontColor || getTextColor(button.style?.backgroundColor),\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 || apiUrl) && (\n <img\n src={imageSrc || apiUrl}\n alt={button.label}\n className=\"max-h-12 object-contain\"\n onError={(e) => {\n console.warn('Image failed to load:', button.label, 'src:', (e.target as HTMLImageElement).src);\n (e.target as HTMLImageElement).style.display = 'none';\n }}\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\"\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\"\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 { loadAACFile, 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 from a File object (Browser only)\n *\n * @param file - File object from file input\n * @param options - Processor options\n * @returns Object with tree, loading state, and error\n *\n * @example\n * ```tsx\n * function MyUploader() {\n * const [file, setFile] = useState<File | null>(null);\n * const { tree, loading, error } = useAACFileFromFile(file);\n *\n * return (\n * <div>\n * <input type=\"file\" onChange={(e) => setFile(e.target.files?.[0])} />\n * {loading && <div>Loading...</div>}\n * {tree && <BoardViewer tree={tree} />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useAACFileFromFile(\n file: File | Blob | null,\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(false);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (!file || options?.enabled === false) {\n setTree(null);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFile(file, 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 }, [file, 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 load an AAC file from a File object and calculate metrics\n *\n * @param file - File object from file input\n * @param metricsOptions - Metrics calculation options\n * @returns Object with tree, metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyUploader() {\n * const [file, setFile] = useState<File | null>(null);\n * const { tree, metrics, loading, error } = useAACFileFromFileWithMetrics(\n * file,\n * { accessMethod: 'direct' }\n * );\n *\n * return (\n * <div>\n * <input type=\"file\" onChange={(e) => setFile(e.target.files?.[0])} />\n * {loading && <div>Loading...</div>}\n * {tree && <BoardViewer tree={tree} buttonMetrics={metrics} />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useAACFileFromFileWithMetrics(\n file: File | Blob | null,\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(false);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (!file || fileOptions?.enabled === false) {\n setTree(null);\n setMetrics(null);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFile(file, 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 }, [file, 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 * (File/Blob objects, file paths, URLs) in both browser and server contexts.\n *\n * Browser: Supports all formats. SQLite-backed formats (.sps, .spb, .ce) require SQL.js configuration.\n * Node.js: Supports all formats including filesystem access and SQLite-backed formats.\n */\n\nimport type { AACTree } from '@willwade/aac-processors';\nimport type { LoadAACFileResult } from '../types';\n\ntype ProcessorOptions = Record<string, unknown> | undefined;\ntype ProcessorInput = string | ArrayBuffer | Uint8Array;\ntype ProcessorInstance = {\n loadIntoTree: (input: ProcessorInput) => Promise<AACTree>;\n};\n\ntype ProcessorModule = typeof import('@willwade/aac-processors');\ntype AnyProcessorModule = ProcessorModule;\n\ntype SqlJsConfig = {\n locateFile: (file: string) => string;\n [key: string]: unknown;\n};\n\n// Node-only file extensions that require server-side processing\nconst NODE_ONLY_EXTENSIONS = ['.xlsx', '.xls'];\n\n// Browser-compatible file extensions (SQLite-backed formats require SQL.js configuration)\nconst BROWSER_EXTENSIONS = [\n '.obf',\n '.obz',\n '.gridset',\n '.plist',\n '.grd',\n '.opml',\n '.dot',\n '.sps',\n '.spb',\n '.ce',\n];\n\n/**\n * Detect if running in browser environment\n */\nfunction isBrowserEnvironment(): boolean {\n return typeof window !== 'undefined' && typeof window.document !== 'undefined';\n}\n\n/**\n * Lazily load processors so browser bundles avoid pulling in Node APIs\n */\nasync function importProcessors(): Promise<AnyProcessorModule> {\n return import('@willwade/aac-processors');\n}\n\n/**\n * Configure SQL.js for browser-only SQLite-backed formats (.sps/.spb/.ce).\n */\nexport async function configureBrowserSqlJs(config: SqlJsConfig): Promise<void> {\n if (!isBrowserEnvironment()) {\n throw new Error('configureBrowserSqlJs can only be used in a browser environment.');\n }\n\n const processors = (await import('@willwade/aac-processors')) as {\n configureSqlJs?: (cfg: SqlJsConfig) => void;\n };\n if (typeof processors.configureSqlJs !== 'function') {\n throw new Error('configureSqlJs is not available in this build of @willwade/aac-processors.');\n }\n\n processors.configureSqlJs(config);\n}\n\n/**\n * Get the appropriate processor for a file based on its extension\n */\nasync function getProcessorForFile(\n filepath: string,\n options?: ProcessorOptions\n): Promise<{ processor: ProcessorInstance; extension: string }> {\n const { getProcessor } = await importProcessors();\n\n const ext = filepath.toLowerCase().split('.').pop();\n if (!ext) {\n throw new Error('Invalid file path: no extension found');\n }\n\n const extension = '.' + ext;\n\n // Check if this is a Node-only processor in browser environment\n if (isBrowserEnvironment() && NODE_ONLY_EXTENSIONS.includes(extension)) {\n throw new Error(\n `File type ${extension} requires server-side processing. ` +\n `Please use the server API or upload a browser-compatible format. ` +\n `Browser supports: ${BROWSER_EXTENSIONS.join(', ')}`\n );\n }\n\n // Use the factory function from v0.1.0\n const processor = getProcessor(extension) as ProcessorInstance | undefined;\n\n if (!processor) {\n throw new Error(`Unsupported file type: ${extension}`);\n }\n\n // For SNAP processor, it needs special options\n if (extension === '.sps' || extension === '.spb') {\n const { SnapProcessor } = await importProcessors();\n return {\n processor: (options ? new SnapProcessor(null, options) : new SnapProcessor()) as ProcessorInstance,\n extension,\n };\n }\n\n return { processor, extension };\n}\n\n/**\n * Load an AAC file from a file path (Node.js 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\n/**\n * Load an AAC file from a File or Blob object (Browser only)\n *\n * @param file - File or Blob object\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 * const file = input.files[0];\n * const tree = await loadAACFile(file);\n * ```\n */\nexport async function loadAACFile(\n file: File | Blob,\n options?: ProcessorOptions\n): Promise<AACTree>;\n\n/**\n * Load an AAC file from an ArrayBuffer (Browser only)\n *\n * @param buffer - ArrayBuffer containing file data\n * @param filename - Filename with extension to detect processor\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const arrayBuffer = await file.arrayBuffer();\n * const tree = await loadAACFile(arrayBuffer, 'board.obf');\n * ```\n */\nexport async function loadAACFile(\n buffer: ArrayBuffer,\n filename: string,\n options?: ProcessorOptions\n): Promise<AACTree>;\n\n/**\n * Unified AAC file loading function that works in both browser and Node.js\n */\nexport async function loadAACFile(\n input: string | File | Blob | ArrayBuffer,\n optionsOrFilename?: ProcessorOptions | string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n // Handle ArrayBuffer case (requires filename as second parameter)\n if (input instanceof ArrayBuffer) {\n const filename = typeof optionsOrFilename === 'string' ? optionsOrFilename : 'unknown.bin';\n return loadFromArrayBuffer(input, filename, options);\n }\n\n // Handle File/Blob case (browser)\n if (input instanceof File || input instanceof Blob) {\n return loadAACFileFromFile(input, undefined, optionsOrFilename as ProcessorOptions);\n }\n\n // Handle string path case (Node.js or URL)\n if (typeof input === 'string') {\n // Check if it's a URL\n if (input.startsWith('http://') || input.startsWith('https://')) {\n return loadAACFileFromURL(input, optionsOrFilename as ProcessorOptions);\n }\n // It's a file path (Node.js only)\n return loadFromFilePath(input, optionsOrFilename as ProcessorOptions);\n }\n\n throw new Error('Invalid input type. Expected File, Blob, ArrayBuffer, or file path string.');\n}\n\n/**\n * Load from file path (Node.js only)\n */\nasync function loadFromFilePath(\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 from File or Blob (Browser only)\n */\nasync function loadAACFileFromFile(\n file: File | Blob,\n filename?: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n // Extract filename from File object if not provided\n const actualFilename = filename || (file instanceof File ? file.name : 'unknown.bin');\n\n // Check if this is a Node-only format\n const ext = '.' + actualFilename.toLowerCase().split('.').pop();\n if (NODE_ONLY_EXTENSIONS.includes(ext)) {\n throw new Error(\n `File type ${ext} requires server-side processing. ` +\n `Please use the server API or upload a browser-compatible format. ` +\n `Browser supports: ${BROWSER_EXTENSIONS.join(', ')}`\n );\n }\n\n // Read file as ArrayBuffer\n const arrayBuffer = await file.arrayBuffer();\n\n // Load using ArrayBuffer\n return loadFromArrayBuffer(arrayBuffer, actualFilename, options);\n}\n\n/**\n * Load from ArrayBuffer (Browser only)\n */\nasync function loadFromArrayBuffer(\n arrayBuffer: ArrayBuffer,\n filename: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const { processor } = await getProcessorForFile(filename, options);\n\n // AACProcessors v0.1.0+ supports loading from ArrayBuffer in browser\n return processor.loadIntoTree(arrayBuffer);\n}\n\n/**\n * Load an AAC file from a URL (Browser only)\n *\n * Note: This requires the server to provide the file with appropriate CORS headers.\n * For better performance, consider using loadAACFileFromFile() with direct file upload 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.obf');\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 and return extended result with format info\n *\n * @param input - File path (Node.js), File/Blob (Browser), or URL string\n * @param options - Processor options\n * @returns Promise resolving to LoadAACFileResult\n */\nexport async function loadAACFileWithMetadata(\n input: string | File | Blob,\n options?: ProcessorOptions\n): Promise<LoadAACFileResult> {\n let tree: AACTree;\n if (typeof input === 'string') {\n tree = await loadAACFile(input, options);\n } else {\n tree = await loadAACFile(input, options);\n }\n\n // Detect format from file extension\n let filepath = '';\n if (typeof input === 'string') {\n filepath = input;\n } else if (input instanceof File) {\n filepath = input.name;\n }\n\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 * 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 aacProcessors = await importProcessors();\n const hasMetricsCalculator =\n typeof (aacProcessors as { MetricsCalculator?: unknown }).MetricsCalculator === 'function';\n\n // Use the calculator if available, otherwise return empty metrics\n if (!hasMetricsCalculator) {\n console.warn('MetricsCalculator not available in this environment');\n return [];\n }\n\n type MetricsCalculator = {\n analyze: (\n targetTree: AACTree,\n metricsOptions: Record<string, unknown>\n ) => { buttons: MetricsButton[] };\n };\n const MetricsCalculatorConstructor = (aacProcessors as { MetricsCalculator?: new () => MetricsCalculator })\n .MetricsCalculator;\n\n if (!MetricsCalculatorConstructor) {\n console.warn('MetricsCalculator constructor not available');\n return [];\n }\n\n const calculator = new MetricsCalculatorConstructor();\n\n let metricsOptions: Record<string, unknown> = {};\n\n if (options.accessMethod === 'scanning' && options.scanningConfig) {\n // Use string literals instead of enums to avoid import issues\n let cellScanningOrder = 0; // SimpleScan\n let blockScanEnabled = false;\n let selectionMethod = 0; // AutoScan\n\n switch (options.scanningConfig.pattern) {\n case 'linear':\n cellScanningOrder = 0; // SimpleScan\n break;\n case 'row-column':\n cellScanningOrder = 1; // RowColumnScan\n break;\n case 'block':\n cellScanningOrder = 1; // RowColumnScan\n blockScanEnabled = true;\n break;\n }\n\n metricsOptions = {\n scanningConfig: {\n cellScanningOrder,\n blockScanEnabled,\n selectionMethod,\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 browserCompatible: boolean;\n}> {\n return [\n {\n name: 'Grid 3',\n extensions: ['.gridset'],\n description: 'Smartbox Grid 3 communication boards',\n browserCompatible: true,\n },\n {\n name: 'TD Snap',\n extensions: ['.sps', '.spb'],\n description: 'Tobii Dynavox Snap files (requires SQL.js in browser)',\n browserCompatible: true,\n },\n {\n name: 'TouchChat',\n extensions: ['.ce'],\n description: 'Saltillo TouchChat files (requires SQL.js in browser)',\n browserCompatible: true,\n },\n {\n name: 'OpenBoard',\n extensions: ['.obf', '.obz'],\n description: 'OpenBoard Format (OBZ/OBF)',\n browserCompatible: true,\n },\n {\n name: 'Asterics Grid',\n extensions: ['.grd'],\n description: 'Asterics Grid files (.grd)',\n browserCompatible: true,\n },\n {\n name: 'Apple Panels',\n extensions: ['.plist'],\n description: 'Apple iOS Panels files',\n browserCompatible: true,\n },\n {\n name: 'OPML',\n extensions: ['.opml'],\n description: 'OPML outline files',\n browserCompatible: true,\n },\n {\n name: 'Excel',\n extensions: ['.xlsx', '.xls'],\n description: 'Excel spreadsheet boards',\n browserCompatible: false,\n },\n {\n name: 'DOT',\n extensions: ['.dot'],\n description: 'DOT graph visualization files',\n browserCompatible: true,\n },\n ];\n}\n\n/**\n * Check if a file format is compatible with browser processing\n *\n * @param extension - File extension (e.g., '.obf', 'obf')\n * @returns true if browser-compatible, false if server-only\n */\nexport function isBrowserCompatible(extension: string): boolean {\n const ext = extension.startsWith('.') ? extension.toLowerCase() : '.' + extension.toLowerCase();\n return BROWSER_EXTENSIONS.includes(ext);\n}\n\n/**\n * Get list of browser-compatible file extensions\n */\nexport function getBrowserExtensions(): string[] {\n return [...BROWSER_EXTENSIONS];\n}\n\n/**\n * Get list of Node-only file extensions\n */\nexport function getNodeOnlyExtensions(): string[] {\n return [...NODE_ONLY_EXTENSIONS];\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 useAACFileFromFile,\n useAACFileWithMetrics,\n useAACFileFromFileWithMetrics,\n useMetrics,\n useSentenceBuilder,\n} from './hooks/useAACFile';\n\n// Utilities\nexport {\n loadAACFile,\n loadAACFileFromURL,\n loadAACFileWithMetadata,\n calculateMetrics,\n configureBrowserSqlJs,\n getSupportedFormats,\n isBrowserCompatible,\n getBrowserExtensions,\n getNodeOnlyExtensions,\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 pos?: string;\n}\n\nfunction PredictionsTooltip({ predictions, label, position, buttonMetricsLookup, onClose, onWordClick, pos }: 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 "{label}"\n {pos && pos !== 'Unknown' && pos !== 'Ignore' && (\n <span className={`ml-1.5 px-1.5 py-0 text-[10px] font-semibold rounded text-white ${\n pos === 'Verb' ? 'bg-orange-500' :\n pos === 'Noun' ? 'bg-teal-500' :\n pos === 'Pronoun' ? 'bg-pink-500' :\n pos === 'Adjective' ? 'bg-yellow-500' :\n 'bg-gray-500'\n }`}>\n {pos}\n </span>\n )}\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 navigateToPageId,\n highlight,\n onButtonClick,\n onPageChange,\n className = '',\n loadId,\n}: BoardViewerProps) {\n console.log('[BoardViewer] COMPONENT LOADED - Running updated code!');\n console.log('[BoardViewer] loadId:', loadId);\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 highlightedButtonRef = React.useRef<HTMLButtonElement | null>(null);\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 pos?: string;\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 React.useEffect(() => {\n if (!navigateToPageId) return;\n if (!tree.pages[navigateToPageId]) return;\n setCurrentPageId(navigateToPageId);\n setPageHistory([]);\n if (onPageChange) {\n onPageChange(navigateToPageId);\n }\n }, [navigateToPageId, onPageChange, tree.pages]);\n\n React.useEffect(() => {\n if (highlightedButtonRef.current) {\n highlightedButtonRef.current.scrollIntoView({\n behavior: 'smooth',\n block: 'center',\n inline: 'center',\n });\n }\n }, [currentPageId, highlight?.pageId, highlight?.x, highlight?.y, highlight?.label]);\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 const buttonMetric = buttonMetricsLookup[button.id];\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 pos: buttonMetric?.pos || button.pos,\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 '#111827';\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 ? '#111827' : '#f9fafb';\n }\n\n return '#111827';\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 color: button.style?.fontColor || getTextColor(button.style?.backgroundColor),\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\"\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 isHighlighted =\n highlight &&\n highlight.pageId === currentPageId &&\n (highlight.buttonId === button.id ||\n (highlight.x !== undefined && highlight.y !== undefined\n ? button.x === highlight.x && button.y === highlight.y\n : false) ||\n (highlight.label && button.label === highlight.label));\n\n // Determine the image source\n // Priority: data URLs (used directly) > file paths (via API for Grid3/OBZ)\n const params = button.parameters as {\n image_id?: string;\n gridPageName?: string;\n imageData?: Buffer;\n };\n\n let imageSrc: string | null = null;\n let apiUrl: string | undefined = undefined;\n\n // IMPORTANT: Only use string values, never Buffers or objects\n // Buffers don't serialize correctly across server/client boundary in SSR\n\n // First, check if we have a data URL (Snap files, OBZ, etc.)\n // NOTE: Check button.resolvedImageEntry first as it's the canonical source\n const resolvedEntry = button.resolvedImageEntry;\n const buttonImage = button.image;\n\n // Safely check resolvedImageEntry - must be a string\n if (resolvedEntry && typeof resolvedEntry === 'string' && resolvedEntry.startsWith('data:image/')) {\n // Snap files: resolvedImageEntry is a data URL string\n imageSrc = resolvedEntry;\n }\n // Safely check button.image - must be a string\n else if (buttonImage && typeof buttonImage === 'string' && buttonImage.startsWith('data:image/')) {\n // Fallback to button.image if it's a data URL string\n imageSrc = buttonImage;\n }\n // Grid3 file path (not a data URL, not a symbol reference)\n else if (\n resolvedEntry &&\n typeof resolvedEntry === 'string' &&\n !resolvedEntry.startsWith('[') &&\n !resolvedEntry.startsWith('data:image/')\n ) {\n // Grid3 files: use API endpoint for file paths\n if (loadId) {\n const entryPath = resolvedEntry;\n const pathMatch = entryPath.match(/^(?:Grids\\/)?(.+)$/);\n if (pathMatch) {\n apiUrl = `/api/image/${loadId}/${encodeURIComponent(pathMatch[1])}`;\n }\n } else {\n imageSrc = resolvedEntry;\n }\n }\n // Fallback to button.image if it's a string (not a symbol reference)\n else if (\n buttonImage &&\n typeof buttonImage === 'string' &&\n !buttonImage.startsWith('[')\n ) {\n imageSrc = buttonImage;\n }\n\n // For OBZ files with loadId and image_id, use API (but not if we already have a data URL)\n // Note: We check buttonImage.length > 1000 but we're NOT using the Buffer imageData\n if (loadId && !imageSrc && !apiUrl && params && params.image_id && typeof params.image_id === 'string') {\n apiUrl = `/api/image/${loadId}/${encodeURIComponent(params.image_id)}`;\n }\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 ref={isHighlighted ? highlightedButtonRef : undefined}\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 isHighlighted\n ? 'ring-4 ring-amber-400 ring-offset-2 ring-offset-white'\n : ''\n }`}\n style={{\n backgroundColor: button.style?.backgroundColor || '#f3f4f6',\n borderColor: button.style?.borderColor || '#e5e7eb',\n color: button.style?.fontColor || getTextColor(button.style?.backgroundColor),\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 {/* POS Badge */}\n {buttonMetric?.pos && buttonMetric.pos !== 'Unknown' && buttonMetric.pos !== 'Ignore' && (\n <div\n className={`absolute top-1 left-1 ${hasLink && showLinkIndicators ? 'left-4' : 'left-1'} px-1 py-0 text-[8px] font-semibold rounded shadow-sm text-white ${\n buttonMetric.pos === 'Verb' ? 'bg-orange-500' :\n buttonMetric.pos === 'Noun' ? 'bg-teal-500' :\n buttonMetric.pos === 'Pronoun' ? 'bg-pink-500' :\n buttonMetric.pos === 'Adjective' ? 'bg-yellow-500' :\n 'bg-gray-500'\n }`}\n title={`Part of speech: ${buttonMetric.pos}`}\n >\n {buttonMetric.pos.charAt(0)}\n </div>\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 || apiUrl) && (\n <img\n src={imageSrc || apiUrl}\n alt={button.label}\n className=\"max-h-12 object-contain\"\n onError={(e) => {\n console.warn('Image failed to load:', button.label, 'src:', (e.target as HTMLImageElement).src);\n (e.target as HTMLImageElement).style.display = 'none';\n }}\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\"\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\"\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 pos={predictionsTooltip.pos}\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 { loadAACFile, 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 from a File object (Browser only)\n *\n * @param file - File object from file input\n * @param options - Processor options\n * @returns Object with tree, loading state, and error\n *\n * @example\n * ```tsx\n * function MyUploader() {\n * const [file, setFile] = useState<File | null>(null);\n * const { tree, loading, error } = useAACFileFromFile(file);\n *\n * return (\n * <div>\n * <input type=\"file\" onChange={(e) => setFile(e.target.files?.[0])} />\n * {loading && <div>Loading...</div>}\n * {tree && <BoardViewer tree={tree} />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useAACFileFromFile(\n file: File | Blob | null,\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(false);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (!file || options?.enabled === false) {\n setTree(null);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFile(file, 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 }, [file, 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 load an AAC file from a File object and calculate metrics\n *\n * @param file - File object from file input\n * @param metricsOptions - Metrics calculation options\n * @returns Object with tree, metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyUploader() {\n * const [file, setFile] = useState<File | null>(null);\n * const { tree, metrics, loading, error } = useAACFileFromFileWithMetrics(\n * file,\n * { accessMethod: 'direct' }\n * );\n *\n * return (\n * <div>\n * <input type=\"file\" onChange={(e) => setFile(e.target.files?.[0])} />\n * {loading && <div>Loading...</div>}\n * {tree && <BoardViewer tree={tree} buttonMetrics={metrics} />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useAACFileFromFileWithMetrics(\n file: File | Blob | null,\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(false);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (!file || fileOptions?.enabled === false) {\n setTree(null);\n setMetrics(null);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFile(file, 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 }, [file, 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 * (File/Blob objects, file paths, URLs) in both browser and server contexts.\n *\n * Browser: Supports all formats. SQLite-backed formats (.sps, .spb, .ce) require SQL.js configuration.\n * Node.js: Supports all formats including filesystem access and SQLite-backed formats.\n */\n\nimport type { AACTree } from '@willwade/aac-processors';\nimport type { LoadAACFileResult, MetricsOptions } from '../types';\n\ntype ProcessorOptions = Record<string, unknown> | undefined;\ntype ProcessorInput = string | ArrayBuffer | Uint8Array;\ntype ProcessorInstance = {\n loadIntoTree: (input: ProcessorInput) => Promise<AACTree>;\n};\n\ntype ProcessorModule = typeof import('@willwade/aac-processors');\ntype AnyProcessorModule = ProcessorModule;\n\ntype SqlJsConfig = {\n locateFile: (file: string) => string;\n [key: string]: unknown;\n};\n\n// Node-only file extensions that require server-side processing\nconst NODE_ONLY_EXTENSIONS = ['.xlsx', '.xls'];\n\n// Browser-compatible file extensions (SQLite-backed formats require SQL.js configuration)\nconst BROWSER_EXTENSIONS = [\n '.obf',\n '.obz',\n '.gridset',\n '.plist',\n '.grd',\n '.opml',\n '.dot',\n '.sps',\n '.spb',\n '.ce',\n];\n\n/**\n * Detect if running in browser environment\n */\nfunction isBrowserEnvironment(): boolean {\n return typeof window !== 'undefined' && typeof window.document !== 'undefined';\n}\n\n/**\n * Lazily load processors so browser bundles avoid pulling in Node APIs\n */\nasync function importProcessors(): Promise<AnyProcessorModule> {\n return import('@willwade/aac-processors');\n}\n\n/**\n * Configure SQL.js for browser-only SQLite-backed formats (.sps/.spb/.ce).\n */\nexport async function configureBrowserSqlJs(config: SqlJsConfig): Promise<void> {\n if (!isBrowserEnvironment()) {\n throw new Error('configureBrowserSqlJs can only be used in a browser environment.');\n }\n\n const processors = (await import('@willwade/aac-processors')) as {\n configureSqlJs?: (cfg: SqlJsConfig) => void;\n };\n if (typeof processors.configureSqlJs !== 'function') {\n throw new Error('configureSqlJs is not available in this build of @willwade/aac-processors.');\n }\n\n processors.configureSqlJs(config);\n}\n\n/**\n * Get the appropriate processor for a file based on its extension\n */\nasync function getProcessorForFile(\n filepath: string,\n options?: ProcessorOptions\n): Promise<{ processor: ProcessorInstance; extension: string }> {\n const { getProcessor } = await importProcessors();\n\n const ext = filepath.toLowerCase().split('.').pop();\n if (!ext) {\n throw new Error('Invalid file path: no extension found');\n }\n\n const extension = '.' + ext;\n\n // Check if this is a Node-only processor in browser environment\n if (isBrowserEnvironment() && NODE_ONLY_EXTENSIONS.includes(extension)) {\n throw new Error(\n `File type ${extension} requires server-side processing. ` +\n `Please use the server API or upload a browser-compatible format. ` +\n `Browser supports: ${BROWSER_EXTENSIONS.join(', ')}`\n );\n }\n\n // Use the factory function from v0.1.0\n const processor = getProcessor(extension) as ProcessorInstance | undefined;\n\n if (!processor) {\n throw new Error(`Unsupported file type: ${extension}`);\n }\n\n // For SNAP processor, it needs special options\n if (extension === '.sps' || extension === '.spb') {\n const { SnapProcessor } = await importProcessors();\n return {\n processor: (options ? new SnapProcessor(null, options) : new SnapProcessor()) as ProcessorInstance,\n extension,\n };\n }\n\n return { processor, extension };\n}\n\n/**\n * Load an AAC file from a file path (Node.js 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\n/**\n * Load an AAC file from a File or Blob object (Browser only)\n *\n * @param file - File or Blob object\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 * const file = input.files[0];\n * const tree = await loadAACFile(file);\n * ```\n */\nexport async function loadAACFile(\n file: File | Blob,\n options?: ProcessorOptions\n): Promise<AACTree>;\n\n/**\n * Load an AAC file from an ArrayBuffer (Browser only)\n *\n * @param buffer - ArrayBuffer containing file data\n * @param filename - Filename with extension to detect processor\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const arrayBuffer = await file.arrayBuffer();\n * const tree = await loadAACFile(arrayBuffer, 'board.obf');\n * ```\n */\nexport async function loadAACFile(\n buffer: ArrayBuffer,\n filename: string,\n options?: ProcessorOptions\n): Promise<AACTree>;\n\n/**\n * Unified AAC file loading function that works in both browser and Node.js\n */\nexport async function loadAACFile(\n input: string | File | Blob | ArrayBuffer,\n optionsOrFilename?: ProcessorOptions | string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n // Handle ArrayBuffer case (requires filename as second parameter)\n if (input instanceof ArrayBuffer) {\n const filename = typeof optionsOrFilename === 'string' ? optionsOrFilename : 'unknown.bin';\n return loadFromArrayBuffer(input, filename, options);\n }\n\n // Handle File/Blob case (browser)\n if (input instanceof File || input instanceof Blob) {\n return loadAACFileFromFile(input, undefined, optionsOrFilename as ProcessorOptions);\n }\n\n // Handle string path case (Node.js or URL)\n if (typeof input === 'string') {\n // Check if it's a URL\n if (input.startsWith('http://') || input.startsWith('https://')) {\n return loadAACFileFromURL(input, optionsOrFilename as ProcessorOptions);\n }\n // It's a file path (Node.js only)\n return loadFromFilePath(input, optionsOrFilename as ProcessorOptions);\n }\n\n throw new Error('Invalid input type. Expected File, Blob, ArrayBuffer, or file path string.');\n}\n\n/**\n * Load from file path (Node.js only)\n */\nasync function loadFromFilePath(\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 from File or Blob (Browser only)\n */\nasync function loadAACFileFromFile(\n file: File | Blob,\n filename?: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n // Extract filename from File object if not provided\n const actualFilename = filename || (file instanceof File ? file.name : 'unknown.bin');\n\n // Check if this is a Node-only format\n const ext = '.' + actualFilename.toLowerCase().split('.').pop();\n if (NODE_ONLY_EXTENSIONS.includes(ext)) {\n throw new Error(\n `File type ${ext} requires server-side processing. ` +\n `Please use the server API or upload a browser-compatible format. ` +\n `Browser supports: ${BROWSER_EXTENSIONS.join(', ')}`\n );\n }\n\n // Read file as ArrayBuffer\n const arrayBuffer = await file.arrayBuffer();\n\n // Load using ArrayBuffer\n return loadFromArrayBuffer(arrayBuffer, actualFilename, options);\n}\n\n/**\n * Load from ArrayBuffer (Browser only)\n */\nasync function loadFromArrayBuffer(\n arrayBuffer: ArrayBuffer,\n filename: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const { processor } = await getProcessorForFile(filename, options);\n\n // AACProcessors v0.1.0+ supports loading from ArrayBuffer in browser\n return processor.loadIntoTree(arrayBuffer);\n}\n\n/**\n * Load an AAC file from a URL (Browser only)\n *\n * Note: This requires the server to provide the file with appropriate CORS headers.\n * For better performance, consider using loadAACFileFromFile() with direct file upload 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.obf');\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 and return extended result with format info\n *\n * @param input - File path (Node.js), File/Blob (Browser), or URL string\n * @param options - Processor options\n * @returns Promise resolving to LoadAACFileResult\n */\nexport async function loadAACFileWithMetadata(\n input: string | File | Blob,\n options?: ProcessorOptions\n): Promise<LoadAACFileResult> {\n let tree: AACTree;\n if (typeof input === 'string') {\n tree = await loadAACFile(input, options);\n } else {\n tree = await loadAACFile(input, options);\n }\n\n // Detect format from file extension\n let filepath = '';\n if (typeof input === 'string') {\n filepath = input;\n } else if (input instanceof File) {\n filepath = input.name;\n }\n\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 * 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: MetricsOptions = {}\n) {\n // Import MetricsCalculator dynamically to avoid circular dependencies\n const aacProcessors = await importProcessors();\n const hasMetricsCalculator =\n typeof (aacProcessors as { MetricsCalculator?: unknown }).MetricsCalculator === 'function';\n\n // Use the calculator if available, otherwise return empty metrics\n if (!hasMetricsCalculator) {\n console.warn('MetricsCalculator not available in this environment');\n return [];\n }\n\n type MetricsCalculator = {\n analyze: (\n targetTree: AACTree,\n metricsOptions: Record<string, unknown>\n ) => { buttons: MetricsButton[] };\n };\n const MetricsCalculatorConstructor = (aacProcessors as { MetricsCalculator?: new () => MetricsCalculator })\n .MetricsCalculator;\n\n if (!MetricsCalculatorConstructor) {\n console.warn('MetricsCalculator constructor not available');\n return [];\n }\n\n const calculator = new MetricsCalculatorConstructor();\n\n let metricsOptions: Record<string, unknown> = {};\n\n if (options.accessMethod === 'scanning' && options.scanningConfig) {\n // Use string literals instead of enums to avoid import issues\n let cellScanningOrder = 0; // SimpleScan\n let blockScanEnabled = false;\n let selectionMethod = 0; // AutoScan\n\n switch (options.scanningConfig.pattern) {\n case 'linear':\n cellScanningOrder = 0; // SimpleScan\n break;\n case 'row-column':\n cellScanningOrder = 1; // RowColumnScan\n break;\n case 'block':\n cellScanningOrder = 1; // RowColumnScan\n blockScanEnabled = true;\n break;\n }\n\n metricsOptions = {\n scanningConfig: {\n cellScanningOrder,\n blockScanEnabled,\n selectionMethod,\n errorCorrectionEnabled: options.scanningConfig.errorCorrection || false,\n errorRate: 0.1,\n },\n useSmartGrammar: options.useSmartGrammar,\n morphologyLocale: options.morphologyLocale,\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 pos?: string;\n is_word_form?: boolean;\n parent_button_id?: string;\n parent_button_label?: 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 pos: btn.pos,\n is_word_form: btn.is_word_form,\n parent_button_id: btn.parent_button_id,\n parent_button_label: btn.parent_button_label,\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 browserCompatible: boolean;\n}> {\n return [\n {\n name: 'Grid 3',\n extensions: ['.gridset'],\n description: 'Smartbox Grid 3 communication boards',\n browserCompatible: true,\n },\n {\n name: 'TD Snap',\n extensions: ['.sps', '.spb'],\n description: 'Tobii Dynavox Snap files (requires SQL.js in browser)',\n browserCompatible: true,\n },\n {\n name: 'TouchChat',\n extensions: ['.ce'],\n description: 'Saltillo TouchChat files (requires SQL.js in browser)',\n browserCompatible: true,\n },\n {\n name: 'OpenBoard',\n extensions: ['.obf', '.obz'],\n description: 'OpenBoard Format (OBZ/OBF)',\n browserCompatible: true,\n },\n {\n name: 'Asterics Grid',\n extensions: ['.grd'],\n description: 'Asterics Grid files (.grd)',\n browserCompatible: true,\n },\n {\n name: 'Apple Panels',\n extensions: ['.plist'],\n description: 'Apple iOS Panels files',\n browserCompatible: true,\n },\n {\n name: 'OPML',\n extensions: ['.opml'],\n description: 'OPML outline files',\n browserCompatible: true,\n },\n {\n name: 'Excel',\n extensions: ['.xlsx', '.xls'],\n description: 'Excel spreadsheet boards',\n browserCompatible: false,\n },\n {\n name: 'DOT',\n extensions: ['.dot'],\n description: 'DOT graph visualization files',\n browserCompatible: true,\n },\n ];\n}\n\n/**\n * Check if a file format is compatible with browser processing\n *\n * @param extension - File extension (e.g., '.obf', 'obf')\n * @returns true if browser-compatible, false if server-only\n */\nexport function isBrowserCompatible(extension: string): boolean {\n const ext = extension.startsWith('.') ? extension.toLowerCase() : '.' + extension.toLowerCase();\n return BROWSER_EXTENSIONS.includes(ext);\n}\n\n/**\n * Get list of browser-compatible file extensions\n */\nexport function getBrowserExtensions(): string[] {\n return [...BROWSER_EXTENSIONS];\n}\n\n/**\n * Get list of Node-only file extensions\n */\nexport function getNodeOnlyExtensions(): string[] {\n return [...NODE_ONLY_EXTENSIONS];\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, onWordClick }) {
|
|
4
|
+
function PredictionsTooltip({ predictions, label, position, buttonMetricsLookup, onClose, onWordClick, pos }) {
|
|
5
5
|
React.useEffect(() => {
|
|
6
6
|
const handleClickOutside = (e) => {
|
|
7
7
|
if (e.target instanceof HTMLElement && !e.target.closest(".predictions-tooltip")) {
|
|
@@ -25,7 +25,8 @@ function PredictionsTooltip({ predictions, label, position, buttonMetricsLookup,
|
|
|
25
25
|
/* @__PURE__ */ jsxs("h4", { className: "text-sm font-semibold text-gray-900 dark:text-white", children: [
|
|
26
26
|
'Word forms for "',
|
|
27
27
|
label,
|
|
28
|
-
'"'
|
|
28
|
+
'"',
|
|
29
|
+
pos && pos !== "Unknown" && pos !== "Ignore" && /* @__PURE__ */ jsx("span", { className: `ml-1.5 px-1.5 py-0 text-[10px] font-semibold rounded text-white ${pos === "Verb" ? "bg-orange-500" : pos === "Noun" ? "bg-teal-500" : pos === "Pronoun" ? "bg-pink-500" : pos === "Adjective" ? "bg-yellow-500" : "bg-gray-500"}`, children: pos })
|
|
29
30
|
] }),
|
|
30
31
|
/* @__PURE__ */ jsx(
|
|
31
32
|
"button",
|
|
@@ -241,12 +242,14 @@ function BoardViewer({
|
|
|
241
242
|
const handleShowPredictions = (button, event) => {
|
|
242
243
|
event.stopPropagation();
|
|
243
244
|
const predictions = button.predictions || button.parameters?.predictions;
|
|
245
|
+
const buttonMetric = buttonMetricsLookup[button.id];
|
|
244
246
|
if (predictions && predictions.length > 0) {
|
|
245
247
|
setPredictionsTooltip({
|
|
246
248
|
predictions,
|
|
247
249
|
label: button.label,
|
|
248
250
|
position: { x: event.clientX, y: event.clientY },
|
|
249
251
|
buttonMetricsLookup,
|
|
252
|
+
pos: buttonMetric?.pos || button.pos,
|
|
250
253
|
onWordClick: (word, effort) => {
|
|
251
254
|
const trimmed = word || "";
|
|
252
255
|
if (trimmed) {
|
|
@@ -491,6 +494,14 @@ ${button.message || ""}`,
|
|
|
491
494
|
children: [
|
|
492
495
|
buttonMetric && showEffortBadges && /* @__PURE__ */ jsx("div", { className: "absolute top-1 right-1 px-1.5 py-0.5 text-xs font-semibold rounded bg-blue-600 text-white shadow-sm", children: effort.toFixed(1) }),
|
|
493
496
|
hasLink && showLinkIndicators && /* @__PURE__ */ jsx("div", { className: "absolute top-1 left-1 w-2 h-2 bg-green-500 rounded-full shadow-sm" }),
|
|
497
|
+
buttonMetric?.pos && buttonMetric.pos !== "Unknown" && buttonMetric.pos !== "Ignore" && /* @__PURE__ */ jsx(
|
|
498
|
+
"div",
|
|
499
|
+
{
|
|
500
|
+
className: `absolute top-1 left-1 ${hasLink && showLinkIndicators ? "left-4" : "left-1"} px-1 py-0 text-[8px] font-semibold rounded shadow-sm text-white ${buttonMetric.pos === "Verb" ? "bg-orange-500" : buttonMetric.pos === "Noun" ? "bg-teal-500" : buttonMetric.pos === "Pronoun" ? "bg-pink-500" : buttonMetric.pos === "Adjective" ? "bg-yellow-500" : "bg-gray-500"}`,
|
|
501
|
+
title: `Part of speech: ${buttonMetric.pos}`,
|
|
502
|
+
children: buttonMetric.pos.charAt(0)
|
|
503
|
+
}
|
|
504
|
+
),
|
|
494
505
|
hasPredictions && /* @__PURE__ */ jsx(
|
|
495
506
|
"div",
|
|
496
507
|
{
|
|
@@ -555,6 +566,7 @@ ${button.message || ""}`,
|
|
|
555
566
|
position: predictionsTooltip.position,
|
|
556
567
|
buttonMetricsLookup: predictionsTooltip.buttonMetricsLookup,
|
|
557
568
|
onWordClick: predictionsTooltip.onWordClick,
|
|
569
|
+
pos: predictionsTooltip.pos,
|
|
558
570
|
onClose: () => setPredictionsTooltip(null)
|
|
559
571
|
}
|
|
560
572
|
)
|
|
@@ -741,7 +753,9 @@ async function calculateMetrics(tree, options = {}) {
|
|
|
741
753
|
selectionMethod,
|
|
742
754
|
errorCorrectionEnabled: options.scanningConfig.errorCorrection || false,
|
|
743
755
|
errorRate: 0.1
|
|
744
|
-
}
|
|
756
|
+
},
|
|
757
|
+
useSmartGrammar: options.useSmartGrammar,
|
|
758
|
+
morphologyLocale: options.morphologyLocale
|
|
745
759
|
};
|
|
746
760
|
}
|
|
747
761
|
const metricsResult = calculator.analyze(tree, metricsOptions);
|
|
@@ -753,7 +767,11 @@ async function calculateMetrics(tree, options = {}) {
|
|
|
753
767
|
is_word: true,
|
|
754
768
|
level: btn.level,
|
|
755
769
|
semantic_id: btn.semantic_id,
|
|
756
|
-
clone_id: btn.clone_id
|
|
770
|
+
clone_id: btn.clone_id,
|
|
771
|
+
pos: btn.pos,
|
|
772
|
+
is_word_form: btn.is_word_form,
|
|
773
|
+
parent_button_id: btn.parent_button_id,
|
|
774
|
+
parent_button_label: btn.parent_button_label
|
|
757
775
|
}));
|
|
758
776
|
}
|
|
759
777
|
function getSupportedFormats() {
|