aac-board-viewer 0.2.8 → 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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components/BoardViewer.tsx"],"sourcesContent":["import React, { useState, useMemo, useCallback } from 'react';\nimport type {\n AACPage,\n AACButton,\n} from '@willwade/aac-processors';\nimport type { BoardViewerProps, ButtonMetric } from '../types';\n\n/**\n * Predictions Tooltip Component\n *\n * Shows a tooltip with predicted word forms when clicking the predictions indicator\n */\ninterface PredictionsTooltipProps {\n predictions: string[];\n label: string;\n position: { x: number; y: number };\n buttonMetricsLookup: { [buttonId: string]: ButtonMetric };\n onClose: () => void;\n onWordClick?: (word: string, effort: number) => void;\n}\n\nfunction PredictionsTooltip({ predictions, label, position, buttonMetricsLookup, onClose, onWordClick }: PredictionsTooltipProps) {\n // Close tooltip when clicking outside\n React.useEffect(() => {\n const handleClickOutside = (e: MouseEvent) => {\n if (e.target instanceof HTMLElement && !e.target.closest('.predictions-tooltip')) {\n onClose();\n }\n };\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, [onClose]);\n\n return (\n <div\n className=\"predictions-tooltip fixed z-50 bg-white dark:bg-gray-800 rounded-lg shadow-xl border-2 border-purple-500 p-3 max-w-xs\"\n style={{\n left: `${Math.min(position.x, window.innerWidth - 200)}px`,\n top: `${Math.min(position.y, window.innerHeight - 150)}px`,\n }}\n onClick={(e) => e.stopPropagation()} // Prevent clicks from bubbling to underlying button\n >\n <div className=\"flex items-center justify-between mb-2\">\n <h4 className=\"text-sm font-semibold text-gray-900 dark:text-white\">\n Word forms for &quot;{label}&quot;\n </h4>\n <button\n onClick={onClose}\n className=\"p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded\"\n aria-label=\"Close\"\n >\n <svg className=\"h-4 w-4 text-gray-600 dark:text-gray-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n <div className=\"flex flex-wrap gap-1\">\n {predictions.map((word, idx) => {\n // Try to find metrics for this word form by label\n const metricForWord = Object.values(buttonMetricsLookup).find(m => m.label === word);\n const effort = metricForWord?.effort;\n\n return (\n <span\n key={idx}\n className=\"px-2 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded text-xs font-medium relative cursor-pointer hover:bg-purple-200 dark:hover:bg-purple-800 transition\"\n onClick={(e) => {\n e.stopPropagation(); // Prevent bubbling\n if (onWordClick) {\n onWordClick(word, effort || 1.0);\n onClose();\n }\n }}\n title={`Click to add \"${word}\" to message`}\n >\n {effort !== undefined && (\n <span className=\"absolute -top-1 -right-1 px-1 py-0 text-[8px] font-semibold rounded bg-blue-600 text-white shadow-xs\">\n {effort.toFixed(1)}\n </span>\n )}\n {word}\n </span>\n );\n })}\n </div>\n </div>\n );\n}\n\n/**\n * AAC Board Viewer Component\n *\n * Displays AAC boards with interactive navigation, sentence building,\n * and optional effort metrics.\n *\n * @param props - BoardViewerProps\n */\nexport function BoardViewer({\n tree,\n buttonMetrics,\n showMessageBar = true,\n showEffortBadges = true,\n showLinkIndicators = true,\n initialPageId,\n 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"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/components/BoardViewer.tsx"],"sourcesContent":["import React, { useState, useMemo, useCallback } from 'react';\nimport type {\n AACPage,\n AACButton,\n} from '@willwade/aac-processors';\nimport type { BoardViewerProps, ButtonMetric } from '../types';\n\n/**\n * Predictions Tooltip Component\n *\n * Shows a tooltip with predicted word forms when clicking the predictions indicator\n */\ninterface PredictionsTooltipProps {\n predictions: string[];\n label: string;\n position: { x: number; y: number };\n buttonMetricsLookup: { [buttonId: string]: ButtonMetric };\n onClose: () => void;\n onWordClick?: (word: string, effort: number) => void;\n 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 &quot;{label}&quot;\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"],"mappings":"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","names":[]}
@@ -1,22 +1,21 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
2
1
  import * as _willwade_aac_processors from '@willwade/aac-processors';
3
2
 
4
3
  /**
5
4
  * Button metric information for effort scoring
6
5
  */
7
6
  interface ButtonMetric {
8
- /** Button ID */
9
7
  id: string;
10
- /** Button label text */
11
8
  label: string;
12
- /** Cognitive effort score (lower is easier) */
13
9
  effort: number;
14
- /** Usage count */
15
10
  count?: number;
16
- /** Whether the button represents a word */
17
11
  is_word?: boolean;
18
- /** Depth level in navigation tree */
19
12
  level?: number;
13
+ pos?: string;
14
+ is_word_form?: boolean;
15
+ parent_button_id?: string;
16
+ parent_button_label?: string;
17
+ semantic_id?: string;
18
+ clone_id?: string;
20
19
  }
21
20
  /**
22
21
  * Props for BoardViewer component
@@ -65,27 +64,14 @@ interface LoadAACFileResult {
65
64
  }
66
65
 
67
66
  interface MetricsOptions {
68
- /** Access method: 'direct' or 'scanning' */
69
67
  accessMethod?: 'direct' | 'scanning';
70
- /** Scanning configuration */
71
68
  scanningConfig?: {
72
- /** Scanning pattern: 'linear', 'row-column', or 'block' */
73
69
  pattern?: 'linear' | 'row-column' | 'block';
74
- /** Selection method for scanning */
75
70
  selectionMethod?: string;
76
- /** Enable error correction */
77
71
  errorCorrection?: boolean;
78
72
  };
73
+ useSmartGrammar?: boolean;
74
+ morphologyLocale?: string;
79
75
  }
80
76
 
81
- /**
82
- * AAC Board Viewer Component
83
- *
84
- * Displays AAC boards with interactive navigation, sentence building,
85
- * and optional effort metrics.
86
- *
87
- * @param props - BoardViewerProps
88
- */
89
- declare function BoardViewer({ tree, buttonMetrics, showMessageBar, showEffortBadges, showLinkIndicators, initialPageId, navigateToPageId, highlight, onButtonClick, onPageChange, className, loadId, }: BoardViewerProps): react_jsx_runtime.JSX.Element;
90
-
91
- export { type ButtonMetric as B, type LoadAACFileResult as L, type MetricsOptions as M, BoardViewer as a, type BoardViewerProps as b };
77
+ export type { ButtonMetric as B, LoadAACFileResult as L, MetricsOptions as M, BoardViewerProps as a };
@@ -1,22 +1,21 @@
1
- import * as react_jsx_runtime from 'react/jsx-runtime';
2
1
  import * as _willwade_aac_processors from '@willwade/aac-processors';
3
2
 
4
3
  /**
5
4
  * Button metric information for effort scoring
6
5
  */
7
6
  interface ButtonMetric {
8
- /** Button ID */
9
7
  id: string;
10
- /** Button label text */
11
8
  label: string;
12
- /** Cognitive effort score (lower is easier) */
13
9
  effort: number;
14
- /** Usage count */
15
10
  count?: number;
16
- /** Whether the button represents a word */
17
11
  is_word?: boolean;
18
- /** Depth level in navigation tree */
19
12
  level?: number;
13
+ pos?: string;
14
+ is_word_form?: boolean;
15
+ parent_button_id?: string;
16
+ parent_button_label?: string;
17
+ semantic_id?: string;
18
+ clone_id?: string;
20
19
  }
21
20
  /**
22
21
  * Props for BoardViewer component
@@ -65,27 +64,14 @@ interface LoadAACFileResult {
65
64
  }
66
65
 
67
66
  interface MetricsOptions {
68
- /** Access method: 'direct' or 'scanning' */
69
67
  accessMethod?: 'direct' | 'scanning';
70
- /** Scanning configuration */
71
68
  scanningConfig?: {
72
- /** Scanning pattern: 'linear', 'row-column', or 'block' */
73
69
  pattern?: 'linear' | 'row-column' | 'block';
74
- /** Selection method for scanning */
75
70
  selectionMethod?: string;
76
- /** Enable error correction */
77
71
  errorCorrection?: boolean;
78
72
  };
73
+ useSmartGrammar?: boolean;
74
+ morphologyLocale?: string;
79
75
  }
80
76
 
81
- /**
82
- * AAC Board Viewer Component
83
- *
84
- * Displays AAC boards with interactive navigation, sentence building,
85
- * and optional effort metrics.
86
- *
87
- * @param props - BoardViewerProps
88
- */
89
- declare function BoardViewer({ tree, buttonMetrics, showMessageBar, showEffortBadges, showLinkIndicators, initialPageId, navigateToPageId, highlight, onButtonClick, onPageChange, className, loadId, }: BoardViewerProps): react_jsx_runtime.JSX.Element;
90
-
91
- export { type ButtonMetric as B, type LoadAACFileResult as L, type MetricsOptions as M, BoardViewer as a, type BoardViewerProps as b };
77
+ export type { ButtonMetric as B, LoadAACFileResult as L, MetricsOptions as M, BoardViewerProps as a };
package/dist/index.d.mts CHANGED
@@ -1,7 +1,8 @@
1
- import { M as MetricsOptions, B as ButtonMetric, L as LoadAACFileResult } from './board-viewer-Cg3N4qqx.mjs';
2
- export { a as BoardViewer, b as BoardViewerProps } from './board-viewer-Cg3N4qqx.mjs';
1
+ export { BoardViewer } from './board-viewer.mjs';
3
2
  import { AACTree } from '@willwade/aac-processors';
4
3
  export { AACButton, AACPage, AACSemanticAction, AACSemanticCategory, AACSemanticIntent, AACTree } from '@willwade/aac-processors';
4
+ import { M as MetricsOptions, B as ButtonMetric, L as LoadAACFileResult } from './index-B783kPn6.mjs';
5
+ export { a as BoardViewerProps } from './index-B783kPn6.mjs';
5
6
  import 'react/jsx-runtime';
6
7
 
7
8
  /**
@@ -291,14 +292,7 @@ declare function loadAACFileWithMetadata(input: string | File | Blob, options?:
291
292
  * });
292
293
  * ```
293
294
  */
294
- declare function calculateMetrics(tree: AACTree, options?: {
295
- accessMethod?: 'direct' | 'scanning';
296
- scanningConfig?: {
297
- pattern?: 'linear' | 'row-column' | 'block';
298
- selectionMethod?: string;
299
- errorCorrection?: boolean;
300
- };
301
- }): Promise<{
295
+ declare function calculateMetrics(tree: AACTree, options?: MetricsOptions): Promise<{
302
296
  id: string;
303
297
  label: string;
304
298
  effort: number;
@@ -307,6 +301,10 @@ declare function calculateMetrics(tree: AACTree, options?: {
307
301
  level: number | undefined;
308
302
  semantic_id: string | undefined;
309
303
  clone_id: string | undefined;
304
+ pos: string | undefined;
305
+ is_word_form: boolean | undefined;
306
+ parent_button_id: string | undefined;
307
+ parent_button_label: string | undefined;
310
308
  }[]>;
311
309
  /**
312
310
  * Get a list of supported file formats
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
- import { M as MetricsOptions, B as ButtonMetric, L as LoadAACFileResult } from './board-viewer-Cg3N4qqx.js';
2
- export { a as BoardViewer, b as BoardViewerProps } from './board-viewer-Cg3N4qqx.js';
1
+ export { BoardViewer } from './board-viewer.js';
3
2
  import { AACTree } from '@willwade/aac-processors';
4
3
  export { AACButton, AACPage, AACSemanticAction, AACSemanticCategory, AACSemanticIntent, AACTree } from '@willwade/aac-processors';
4
+ import { M as MetricsOptions, B as ButtonMetric, L as LoadAACFileResult } from './index-B783kPn6.js';
5
+ export { a as BoardViewerProps } from './index-B783kPn6.js';
5
6
  import 'react/jsx-runtime';
6
7
 
7
8
  /**
@@ -291,14 +292,7 @@ declare function loadAACFileWithMetadata(input: string | File | Blob, options?:
291
292
  * });
292
293
  * ```
293
294
  */
294
- declare function calculateMetrics(tree: AACTree, options?: {
295
- accessMethod?: 'direct' | 'scanning';
296
- scanningConfig?: {
297
- pattern?: 'linear' | 'row-column' | 'block';
298
- selectionMethod?: string;
299
- errorCorrection?: boolean;
300
- };
301
- }): Promise<{
295
+ declare function calculateMetrics(tree: AACTree, options?: MetricsOptions): Promise<{
302
296
  id: string;
303
297
  label: string;
304
298
  effort: number;
@@ -307,6 +301,10 @@ declare function calculateMetrics(tree: AACTree, options?: {
307
301
  level: number | undefined;
308
302
  semantic_id: string | undefined;
309
303
  clone_id: string | undefined;
304
+ pos: string | undefined;
305
+ is_word_form: boolean | undefined;
306
+ parent_button_id: string | undefined;
307
+ parent_button_label: string | undefined;
310
308
  }[]>;
311
309
  /**
312
310
  * Get a list of supported file formats
package/dist/index.js CHANGED
@@ -52,7 +52,7 @@ module.exports = __toCommonJS(index_exports);
52
52
  // src/components/BoardViewer.tsx
53
53
  var import_react = __toESM(require("react"));
54
54
  var import_jsx_runtime = require("react/jsx-runtime");
55
- function PredictionsTooltip({ predictions, label, position, buttonMetricsLookup, onClose, onWordClick }) {
55
+ function PredictionsTooltip({ predictions, label, position, buttonMetricsLookup, onClose, onWordClick, pos }) {
56
56
  import_react.default.useEffect(() => {
57
57
  const handleClickOutside = (e) => {
58
58
  if (e.target instanceof HTMLElement && !e.target.closest(".predictions-tooltip")) {
@@ -76,7 +76,8 @@ function PredictionsTooltip({ predictions, label, position, buttonMetricsLookup,
76
76
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("h4", { className: "text-sm font-semibold text-gray-900 dark:text-white", children: [
77
77
  'Word forms for "',
78
78
  label,
79
- '"'
79
+ '"',
80
+ pos && pos !== "Unknown" && pos !== "Ignore" && /* @__PURE__ */ (0, import_jsx_runtime.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 })
80
81
  ] }),
81
82
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
82
83
  "button",
@@ -292,12 +293,14 @@ function BoardViewer({
292
293
  const handleShowPredictions = (button, event) => {
293
294
  event.stopPropagation();
294
295
  const predictions = button.predictions || button.parameters?.predictions;
296
+ const buttonMetric = buttonMetricsLookup[button.id];
295
297
  if (predictions && predictions.length > 0) {
296
298
  setPredictionsTooltip({
297
299
  predictions,
298
300
  label: button.label,
299
301
  position: { x: event.clientX, y: event.clientY },
300
302
  buttonMetricsLookup,
303
+ pos: buttonMetric?.pos || button.pos,
301
304
  onWordClick: (word, effort) => {
302
305
  const trimmed = word || "";
303
306
  if (trimmed) {
@@ -542,6 +545,14 @@ ${button.message || ""}`,
542
545
  children: [
543
546
  buttonMetric && showEffortBadges && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "absolute top-1 right-1 px-1.5 py-0.5 text-xs font-semibold rounded bg-blue-600 text-white shadow-sm", children: effort.toFixed(1) }),
544
547
  hasLink && showLinkIndicators && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "absolute top-1 left-1 w-2 h-2 bg-green-500 rounded-full shadow-sm" }),
548
+ buttonMetric?.pos && buttonMetric.pos !== "Unknown" && buttonMetric.pos !== "Ignore" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
549
+ "div",
550
+ {
551
+ 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"}`,
552
+ title: `Part of speech: ${buttonMetric.pos}`,
553
+ children: buttonMetric.pos.charAt(0)
554
+ }
555
+ ),
545
556
  hasPredictions && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
546
557
  "div",
547
558
  {
@@ -606,6 +617,7 @@ ${button.message || ""}`,
606
617
  position: predictionsTooltip.position,
607
618
  buttonMetricsLookup: predictionsTooltip.buttonMetricsLookup,
608
619
  onWordClick: predictionsTooltip.onWordClick,
620
+ pos: predictionsTooltip.pos,
609
621
  onClose: () => setPredictionsTooltip(null)
610
622
  }
611
623
  )
@@ -792,7 +804,9 @@ async function calculateMetrics(tree, options = {}) {
792
804
  selectionMethod,
793
805
  errorCorrectionEnabled: options.scanningConfig.errorCorrection || false,
794
806
  errorRate: 0.1
795
- }
807
+ },
808
+ useSmartGrammar: options.useSmartGrammar,
809
+ morphologyLocale: options.morphologyLocale
796
810
  };
797
811
  }
798
812
  const metricsResult = calculator.analyze(tree, metricsOptions);
@@ -804,7 +818,11 @@ async function calculateMetrics(tree, options = {}) {
804
818
  is_word: true,
805
819
  level: btn.level,
806
820
  semantic_id: btn.semantic_id,
807
- clone_id: btn.clone_id
821
+ clone_id: btn.clone_id,
822
+ pos: btn.pos,
823
+ is_word_form: btn.is_word_form,
824
+ parent_button_id: btn.parent_button_id,
825
+ parent_button_label: btn.parent_button_label
808
826
  }));
809
827
  }
810
828
  function getSupportedFormats() {