aac-board-viewer 0.2.5 → 0.2.6
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.js +20 -4
- package/dist/board-viewer.js.map +1 -1
- package/dist/board-viewer.mjs +20 -4
- package/dist/board-viewer.mjs.map +1 -1
- package/dist/index.js +26 -5
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +26 -5
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
package/dist/board-viewer.js
CHANGED
|
@@ -113,6 +113,8 @@ function BoardViewer({
|
|
|
113
113
|
className = "",
|
|
114
114
|
loadId
|
|
115
115
|
}) {
|
|
116
|
+
console.log("[BoardViewer] COMPONENT LOADED - Running updated code!");
|
|
117
|
+
console.log("[BoardViewer] loadId:", loadId);
|
|
116
118
|
const resolveInitialPageId = (0, import_react.useCallback)(() => {
|
|
117
119
|
if (initialPageId && tree.pages[initialPageId]) {
|
|
118
120
|
return initialPageId;
|
|
@@ -459,12 +461,26 @@ ${button.message || ""}`,
|
|
|
459
461
|
const imageSrc = (button.resolvedImageEntry && !String(button.resolvedImageEntry).startsWith("[") ? button.resolvedImageEntry : null) || (button.image && !String(button.image).startsWith("[") ? button.image : null);
|
|
460
462
|
const params = button.parameters;
|
|
461
463
|
let apiUrl = void 0;
|
|
462
|
-
|
|
463
|
-
|
|
464
|
+
console.log("[BoardViewer] Button:", button.label, "loadId:", loadId, "resolvedImageEntry:", button.resolvedImageEntry);
|
|
465
|
+
if (loadId) {
|
|
466
|
+
console.log("[BoardViewer] loadId exists, checking image conditions");
|
|
467
|
+
if (button.image && button.image.length > 1e3 && params.image_id) {
|
|
464
468
|
apiUrl = `/api/image/${loadId}/${params.image_id}`;
|
|
465
|
-
|
|
466
|
-
|
|
469
|
+
console.log("[BoardViewer] Using OBZ image_id API:", apiUrl);
|
|
470
|
+
} else if (button.resolvedImageEntry && !button.resolvedImageEntry.startsWith("[")) {
|
|
471
|
+
const entryPath = button.resolvedImageEntry;
|
|
472
|
+
const pathMatch = entryPath.match(/^(?:Grids\/)?(.+)$/);
|
|
473
|
+
if (pathMatch) {
|
|
474
|
+
apiUrl = `/api/image/${loadId}/${pathMatch[1]}`;
|
|
475
|
+
console.log("[BoardViewer] Button:", button.label, "resolvedImageEntry:", entryPath, "apiUrl:", apiUrl);
|
|
476
|
+
} else {
|
|
477
|
+
console.log("[BoardViewer] Button:", button.label, "resolvedImageEntry no match:", entryPath);
|
|
478
|
+
}
|
|
479
|
+
} else {
|
|
480
|
+
console.log("[BoardViewer] loadId exists but no valid image condition");
|
|
467
481
|
}
|
|
482
|
+
} else {
|
|
483
|
+
console.log("[BoardViewer] loadId is undefined/null");
|
|
468
484
|
}
|
|
469
485
|
if (isWorkspace) {
|
|
470
486
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
package/dist/board-viewer.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/board-viewer.ts","../src/components/BoardViewer.tsx"],"sourcesContent":["/**\n * Minimal entrypoint for rendering the BoardViewer without loaders.\n */\n\nexport { BoardViewer } from './components/BoardViewer';\nexport type { BoardViewerProps, ButtonMetric } from './types';\nexport type { AACTree, AACPage, AACButton } 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 highlight,\n onButtonClick,\n onPageChange,\n className = '',\n loadId,\n}: BoardViewerProps) {\n const resolveInitialPageId = useCallback(() => {\n // Explicit override\n if (initialPageId && tree.pages[initialPageId]) {\n return initialPageId;\n }\n // If toolbar exists and rootId is different, use rootId\n if (tree.toolbarId && tree.rootId && tree.rootId !== tree.toolbarId) {\n return tree.rootId;\n }\n // If rootId exists and is not a toolbar, use it\n if (tree.rootId && tree.pages[tree.rootId]) {\n const rootPage = tree.pages[tree.rootId];\n const isToolbar =\n rootPage.name.toLowerCase().includes('toolbar') ||\n rootPage.name.toLowerCase().includes('tool bar');\n if (!isToolbar) {\n return tree.rootId;\n }\n }\n // Explicit Start page fallback if present\n const startPage = Object.values(tree.pages).find(\n (p) => p.name.toLowerCase() === 'start'\n );\n if (startPage) {\n return startPage.id;\n }\n // Fall back to first page that's not a toolbar\n const nonToolbarPage = Object.values(tree.pages).find(\n (p) => !p.name.toLowerCase().includes('toolbar') && !p.name.toLowerCase().includes('tool bar')\n );\n if (nonToolbarPage) {\n return nonToolbarPage.id;\n }\n // Last resort: first page\n const pageIds = Object.keys(tree.pages);\n return pageIds.length > 0 ? pageIds[0] : null;\n }, [initialPageId, tree]);\n\n // Determine which page to show\n const [currentPageId, setCurrentPageId] = useState<string | null>(() => resolveInitialPageId());\n\n const 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 (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.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 const imageSrc =\n (button.resolvedImageEntry && !String(button.resolvedImageEntry).startsWith('[')\n ? button.resolvedImageEntry\n : null) ||\n (button.image && !String(button.image).startsWith('[') ? button.image : null);\n\n // For OBZ files with loadId, use the image API endpoint\n // For Grid3 files, also use API if we have gridPageName and position\n const params = button.parameters as {\n image_id?: string;\n gridPageName?: string;\n };\n\n let apiUrl: string | undefined = undefined;\n if (loadId && button.image) {\n // OBZ files have large data URLs or image_id\n if (button.image.length > 1000 && params.image_id) {\n apiUrl = `/api/image/${loadId}/${params.image_id}`;\n }\n // Grid3 files have gridPageName and x,y coordinates\n else if (params.gridPageName && button.x !== undefined && button.y !== undefined) {\n apiUrl = `/api/image/${loadId}/${params.gridPageName}/${button.x}-${button.y}`;\n }\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={(apiUrl || imageSrc) ?? undefined}\n alt={button.label}\n className=\"max-h-12 object-contain\"\n />\n )}\n\n {/* Label / Predictions */}\n <div className=\"flex flex-col items-center justify-center\">\n <span\n className=\"text-xs sm:text-sm text-center font-medium leading-tight line-clamp-3\"\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":["React"]}
|
|
1
|
+
{"version":3,"sources":["../src/board-viewer.ts","../src/components/BoardViewer.tsx"],"sourcesContent":["/**\n * Minimal entrypoint for rendering the BoardViewer without loaders.\n */\n\nexport { BoardViewer } from './components/BoardViewer';\nexport type { BoardViewerProps, ButtonMetric } from './types';\nexport type { AACTree, AACPage, AACButton } 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 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 (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.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 // Note: For Grid3 files with loadId, we'll use apiUrl instead (set below)\n const imageSrc =\n (button.resolvedImageEntry && !String(button.resolvedImageEntry).startsWith('[')\n ? button.resolvedImageEntry\n : null) ||\n (button.image && !String(button.image).startsWith('[') ? button.image : null);\n\n // For OBZ files with loadId, use the image API endpoint\n // For Grid3 files, also use API if we have resolvedImageEntry\n const params = button.parameters as {\n image_id?: string;\n gridPageName?: string;\n };\n\n let apiUrl: string | undefined = undefined;\n console.log('[BoardViewer] Button:', button.label, 'loadId:', loadId, 'resolvedImageEntry:', button.resolvedImageEntry);\n if (loadId) {\n console.log('[BoardViewer] loadId exists, checking image conditions');\n // OBZ files have large data URLs or image_id\n if (button.image && button.image.length > 1000 && params.image_id) {\n apiUrl = `/api/image/${loadId}/${params.image_id}`;\n console.log('[BoardViewer] Using OBZ image_id API:', apiUrl);\n }\n // Grid3 files: use resolvedImageEntry if available (contains full path)\n else if (button.resolvedImageEntry && !button.resolvedImageEntry.startsWith('[')) {\n // Extract just the path after \"Grids/\" for the API\n const entryPath = button.resolvedImageEntry;\n const pathMatch = entryPath.match(/^(?:Grids\\/)?(.+)$/);\n if (pathMatch) {\n apiUrl = `/api/image/${loadId}/${pathMatch[1]}`;\n console.log('[BoardViewer] Button:', button.label, 'resolvedImageEntry:', entryPath, 'apiUrl:', apiUrl);\n } else {\n console.log('[BoardViewer] Button:', button.label, 'resolvedImageEntry no match:', entryPath);\n }\n } else {\n console.log('[BoardViewer] loadId exists but no valid image condition');\n }\n } else {\n console.log('[BoardViewer] loadId is undefined/null');\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={(apiUrl || imageSrc) ?? undefined}\n alt={button.label}\n className=\"max-h-12 object-contain\"\n />\n )}\n\n {/* Label / Predictions */}\n <div className=\"flex flex-col items-center justify-center\">\n <span\n className=\"text-xs sm:text-sm text-center font-medium leading-tight line-clamp-3\"\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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAAsD;AA2C9C;AAtBR,SAAS,mBAAmB,EAAE,aAAa,OAAO,UAAU,qBAAqB,SAAS,YAAY,GAA4B;AAEhI,eAAAA,QAAM,UAAU,MAAM;AACpB,UAAM,qBAAqB,CAAC,MAAkB;AAC5C,UAAI,EAAE,kBAAkB,eAAe,CAAC,EAAE,OAAO,QAAQ,sBAAsB,GAAG;AAChF,gBAAQ;AAAA,MACV;AAAA,IACF;AACA,aAAS,iBAAiB,aAAa,kBAAkB;AACzD,WAAO,MAAM,SAAS,oBAAoB,aAAa,kBAAkB;AAAA,EAC3E,GAAG,CAAC,OAAO,CAAC;AAEZ,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,OAAO;AAAA,QACL,MAAM,GAAG,KAAK,IAAI,SAAS,GAAG,OAAO,aAAa,GAAG,CAAC;AAAA,QACtD,KAAK,GAAG,KAAK,IAAI,SAAS,GAAG,OAAO,cAAc,GAAG,CAAC;AAAA,MACxD;AAAA,MACA,SAAS,CAAC,MAAM,EAAE,gBAAgB;AAAA,MAElC;AAAA,qDAAC,SAAI,WAAU,0CACb;AAAA,uDAAC,QAAG,WAAU,uDAAsD;AAAA;AAAA,YAC5C;AAAA,YAAM;AAAA,aAC9B;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,SAAS;AAAA,cACT,WAAU;AAAA,cACV,cAAW;AAAA,cAEX,sDAAC,SAAI,WAAU,4CAA2C,MAAK,QAAO,QAAO,gBAAe,SAAQ,aAClG,sDAAC,UAAK,eAAc,SAAQ,gBAAe,SAAQ,aAAa,GAAG,GAAE,wBAAuB,GAC9F;AAAA;AAAA,UACF;AAAA,WACF;AAAA,QACA,4CAAC,SAAI,WAAU,wBACZ,sBAAY,IAAI,CAAC,MAAM,QAAQ;AAE9B,gBAAM,gBAAgB,OAAO,OAAO,mBAAmB,EAAE,KAAK,OAAK,EAAE,UAAU,IAAI;AACnF,gBAAM,SAAS,eAAe;AAE9B,iBACE;AAAA,YAAC;AAAA;AAAA,cAEC,WAAU;AAAA,cACV,SAAS,CAAC,MAAM;AACd,kBAAE,gBAAgB;AAClB,oBAAI,aAAa;AACf,8BAAY,MAAM,UAAU,CAAG;AAC/B,0BAAQ;AAAA,gBACV;AAAA,cACF;AAAA,cACA,OAAO,iBAAiB,IAAI;AAAA,cAE3B;AAAA,2BAAW,UACV,4CAAC,UAAK,WAAU,wGACb,iBAAO,QAAQ,CAAC,GACnB;AAAA,gBAED;AAAA;AAAA;AAAA,YAhBI;AAAA,UAiBP;AAAA,QAEJ,CAAC,GACH;AAAA;AAAA;AAAA,EACF;AAEJ;AAUO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,qBAAqB;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AACF,GAAqB;AACnB,UAAQ,IAAI,wDAAwD;AACpE,UAAQ,IAAI,yBAAyB,MAAM;AAC3C,QAAM,2BAAuB,0BAAY,MAAM;AAE7C,QAAI,iBAAiB,KAAK,MAAM,aAAa,GAAG;AAC9C,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,aAAa,KAAK,UAAU,KAAK,WAAW,KAAK,WAAW;AACnE,aAAO,KAAK;AAAA,IACd;AAEA,QAAI,KAAK,UAAU,KAAK,MAAM,KAAK,MAAM,GAAG;AAC1C,YAAM,WAAW,KAAK,MAAM,KAAK,MAAM;AACvC,YAAM,YACJ,SAAS,KAAK,YAAY,EAAE,SAAS,SAAS,KAC9C,SAAS,KAAK,YAAY,EAAE,SAAS,UAAU;AACjD,UAAI,CAAC,WAAW;AACd,eAAO,KAAK;AAAA,MACd;AAAA,IACF;AAEA,UAAM,YAAY,OAAO,OAAO,KAAK,KAAK,EAAE;AAAA,MAC1C,CAAC,MAAM,EAAE,KAAK,YAAY,MAAM;AAAA,IAClC;AACA,QAAI,WAAW;AACb,aAAO,UAAU;AAAA,IACnB;AAEA,UAAM,iBAAiB,OAAO,OAAO,KAAK,KAAK,EAAE;AAAA,MAC/C,CAAC,MAAM,CAAC,EAAE,KAAK,YAAY,EAAE,SAAS,SAAS,KAAK,CAAC,EAAE,KAAK,YAAY,EAAE,SAAS,UAAU;AAAA,IAC/F;AACA,QAAI,gBAAgB;AAClB,aAAO,eAAe;AAAA,IACxB;AAEA,UAAM,UAAU,OAAO,KAAK,KAAK,KAAK;AACtC,WAAO,QAAQ,SAAS,IAAI,QAAQ,CAAC,IAAI;AAAA,EAC3C,GAAG,CAAC,eAAe,IAAI,CAAC;AAGxB,QAAM,CAAC,eAAe,gBAAgB,QAAI,uBAAwB,MAAM,qBAAqB,CAAC;AAE9F,QAAM,uBAAuB,aAAAA,QAAM,OAAiC,IAAI;AAExE,QAAM,CAAC,aAAa,cAAc,QAAI,uBAAoB,CAAC,CAAC;AAC5D,QAAM,CAAC,SAAS,UAAU,QAAI,uBAAS,EAAE;AACzC,QAAM,CAAC,kBAAkB,mBAAmB,QAAI,uBAAS,CAAC;AAC1D,QAAM,CAAC,eAAe,gBAAgB,QAAI,uBAAS,CAAC;AAGpD,QAAM,CAAC,oBAAoB,qBAAqB,QAAI,uBAM1C,IAAI;AAGd,QAAM,0BAAsB,sBAAQ,MAAM;AACxC,QAAI,CAAC,cAAe,QAAO,CAAC;AAC5B,UAAM,SAA+C,CAAC;AACtD,kBAAc,QAAQ,CAAC,WAAW;AAChC,aAAO,OAAO,EAAE,IAAI;AAAA,IACtB,CAAC;AACD,WAAO;AAAA,EACT,GAAG,CAAC,aAAa,CAAC;AAElB,QAAM,cAAc,gBAAgB,KAAK,MAAM,aAAa,IAAI;AAChE,QAAM,WAAW,CAAC,iBAA4C;AAC5D,QAAI,CAAC,gBAAgB,CAAC,KAAK,MAAM,YAAY,EAAG,QAAO;AACvD,QAAI,aAAa;AACf,qBAAe,CAAC,SAAS,CAAC,GAAG,MAAM,WAAW,CAAC;AAAA,IACjD;AACA,qBAAiB,YAAY;AAC7B,QAAI,cAAc;AAChB,mBAAa,YAAY;AAAA,IAC3B;AACA,WAAO;AAAA,EACT;AAGA,eAAAA,QAAM,UAAU,MAAM;AACpB,qBAAiB,qBAAqB,CAAC;AACvC,mBAAe,CAAC,CAAC;AAAA,EACnB,GAAG,CAAC,oBAAoB,CAAC;AAEzB,eAAAA,QAAM,UAAU,MAAM;AACpB,QAAI,qBAAqB,SAAS;AAChC,2BAAqB,QAAQ,eAAe;AAAA,QAC1C,UAAU;AAAA,QACV,OAAO;AAAA,QACP,QAAQ;AAAA,MACV,CAAC;AAAA,IACH;AAAA,EACF,GAAG,CAAC,eAAe,WAAW,QAAQ,WAAW,GAAG,WAAW,GAAG,WAAW,KAAK,CAAC;AAGnF,QAAM,cAAc,CAAC,MAAc,WAAmB;AACpD,wBAAoB,CAAC,SAAS,OAAO,CAAC;AACtC,qBAAiB,CAAC,SAAS,OAAO,MAAM;AAAA,EAC1C;AAEA,QAAM,oBAAoB,CAAC,WAAsB;AAE/C,QAAI,eAAe;AACjB,oBAAc,MAAM;AAAA,IACtB;AAEA,UAAM,SAAS,OAAO,gBAAgB,SAClC,OAAO,OAAO,eAAe,MAAM,IACnC;AACJ,UAAM,eAAe,OAAO,gBAAgB,OAAO,gBAAgB;AACnE,UAAM,SAAS,oBAAoB,OAAO,EAAE,GAAG,UAAU;AACzD,UAAM,YACJ,OAAO,gBAAgB,QAAQ,OAAO,WAAW,OAAO,SAAS;AAEnE,UAAM,iBAAiB,MAAM;AAC3B,iBAAW,CAAC,SAAS;AACnB,cAAM,QAAQ,KAAK,KAAK,EAAE,MAAM,KAAK;AACrC,cAAM,IAAI;AACV,cAAM,SAAS,MAAM,KAAK,GAAG;AAC7B,eAAO;AAAA,MACT,CAAC;AAAA,IACH;AAEA,UAAM,sBAAsB,MAAM;AAChC,iBAAW,CAAC,SAAS,KAAK,MAAM,GAAG,EAAE,CAAC;AAAA,IACxC;AAEA,UAAM,aAAa,CAAC,SAAiB;AACnC,YAAM,UAAU,QAAQ,OAAO,SAAS;AACxC,iBAAW,CAAC,SAAS;AACnB,cAAM,aAAa,UACf,QAAQ,OAAO,MAAM,MAAM,UAC3B;AACJ,YAAI,SAAS;AACX,sBAAY,SAAS,MAAM;AAAA,QAC7B;AACA,eAAO;AAAA,MACT,CAAC;AAAA,IACH;AAGA,QAAI,WAAW,iBAAiB,SAAS,YAAY,GAAG;AACtD;AAAA,IACF;AAEA,YAAQ,QAAQ;AAAA,MACd,KAAK;AACH,mBAAW;AACX;AAAA,MACF,KAAK;AACH,YAAI,KAAK,UAAU,SAAS,KAAK,MAAM,EAAG;AAC1C;AAAA,MACF,KAAK;AACH,uBAAe;AACf;AAAA,MACF,KAAK;AACH,4BAAoB;AACpB;AAAA,MACF,KAAK;AACH,qBAAa;AACb;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,mBAAW,SAAS;AACpB;AAAA,MACF;AACE;AAAA,IACJ;AAGA,QAAI,gBAAgB,SAAS,YAAY,GAAG;AAC1C;AAAA,IACF;AAGA,eAAW,SAAS;AAAA,EACtB;AAEA,QAAM,aAAa,MAAM;AACvB,QAAI,YAAY,SAAS,GAAG;AAC1B,YAAM,eAAe,YAAY,YAAY,SAAS,CAAC;AACvD,qBAAe,CAAC,SAAS,KAAK,MAAM,GAAG,EAAE,CAAC;AAC1C,uBAAiB,aAAa,EAAE;AAChC,UAAI,cAAc;AAChB,qBAAa,aAAa,EAAE;AAAA,MAC9B;AAAA,IACF;AAAA,EACF;AAEA,QAAM,eAAe,MAAM;AACzB,eAAW,EAAE;AACb,wBAAoB,CAAC;AACrB,qBAAiB,CAAC;AAAA,EACpB;AAEA,QAAM,wBAAwB,CAC5B,QACA,UACG;AACH,UAAM,gBAAgB;AACtB,UAAM,cAAc,OAAO,eAAgB,OAAO,YAA2C;AAC7F,QAAI,eAAe,YAAY,SAAS,GAAG;AACzC,4BAAsB;AAAA,QACpB;AAAA,QACA,OAAO,OAAO;AAAA,QACd,UAAU,EAAE,GAAG,MAAM,SAAS,GAAG,MAAM,QAAQ;AAAA,QAC/C;AAAA,QACA,aAAa,CAAC,MAAM,WAAW;AAC7B,gBAAM,UAAU,QAAQ;AACxB,cAAI,SAAS;AACX,uBAAW,CAAC,SAAS;AACnB,oBAAM,aAAa,QAAQ,OAAO,MAAM,MAAM;AAC9C,0BAAY,SAAS,MAAM;AAC3B,qBAAO;AAAA,YACT,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,eAAe,CAAC,oBAA6B;AACjD,QAAI,CAAC,gBAAiB,QAAO;AAG7B,UAAM,MAAM,gBAAgB,QAAQ,KAAK,EAAE;AAC3C,QAAI,IAAI,WAAW,GAAG;AACpB,YAAM,IAAI,SAAS,IAAI,UAAU,GAAG,CAAC,GAAG,EAAE;AAC1C,YAAM,IAAI,SAAS,IAAI,UAAU,GAAG,CAAC,GAAG,EAAE;AAC1C,YAAM,IAAI,SAAS,IAAI,UAAU,GAAG,CAAC,GAAG,EAAE;AAC1C,YAAM,cAAc,IAAI,MAAM,IAAI,MAAM,IAAI,OAAO;AACnD,aAAO,cAAc,MAAM,YAAY;AAAA,IACzC;AAEA,WAAO;AAAA,EACT;AAEA,MAAI,CAAC,aAAa;AAChB,WACE,4CAAC,SAAI,WAAW,iFAAiF,SAAS,IACxG,sDAAC,SAAI,WAAU,eACb,sDAAC,OAAE,WAAU,oCAAmC,gCAAkB,GACpE,GACF;AAAA,EAEJ;AAGA,QAAM,cAAc,KAAK,YAAY,KAAK,MAAM,KAAK,SAAS,IAAI;AAGlE,QAAM,WAAW,YAAY,KAAK;AAClC,QAAM,WAAW,WAAW,IAAI,YAAY,KAAK,CAAC,EAAE,SAAS;AAE7D,SACE,6CAAC,SAAI,WAAW,kEAAkE,SAAS,IAExF;AAAA,sBACC,4CAAC,SAAI,WAAU,oFACb,uDAAC,SAAI,WAAU,0CACb;AAAA,kDAAC,SAAI,WAAU,kBACZ,oBACC,6CAAC,SAAI,WAAU,aACb;AAAA,oDAAC,OAAE,WAAU,qDAAqD,mBAAQ;AAAA,QAC1E,6CAAC,SAAI,WAAU,sBACb;AAAA,uDAAC,SAAI,WAAU,oCACZ;AAAA;AAAA,YAAiB;AAAA,YAAE,qBAAqB,IAAI,SAAS;AAAA,aACxD;AAAA,UACC,iBACC,4EACE;AAAA,yDAAC,SAAI,WAAU,oCAAmC;AAAA;AAAA,cACxC,4CAAC,UAAK,WAAU,eAAe,wBAAc,QAAQ,CAAC,GAAE;AAAA,eAClE;AAAA,YACA,6CAAC,SAAI,WAAU,oCAAmC;AAAA;AAAA,cAC3C;AAAA,cACL,4CAAC,UAAK,WAAU,eACb,6BAAmB,KACf,gBAAgB,kBAAkB,QAAQ,CAAC,IAC5C,QACN;AAAA,eACF;AAAA,aACF;AAAA,WAEJ;AAAA,SACF,IAEA,4CAAC,OAAE,WAAU,2CAA0C,gDAEvD,GAEJ;AAAA,MACC,WACC;AAAA,QAAC;AAAA;AAAA,UACC,SAAS;AAAA,UACT,WAAU;AAAA,UACV,cAAW;AAAA,UAEX;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,MAAK;AAAA,cACL,QAAO;AAAA,cACP,SAAQ;AAAA,cAER;AAAA,gBAAC;AAAA;AAAA,kBACC,eAAc;AAAA,kBACd,gBAAe;AAAA,kBACf,aAAa;AAAA,kBACb,GAAE;AAAA;AAAA,cACJ;AAAA;AAAA,UACF;AAAA;AAAA,MACF;AAAA,OAEJ,GACF;AAAA,IAIF,6CAAC,SAAI,WAAU,QAEZ;AAAA,qBACC,4CAAC,SAAI,WAAU,6FACb,uDAAC,SAAI,WAAU,OACb;AAAA,oDAAC,OAAE,WAAU,+EAA8E,qBAE3F;AAAA,QACA,4CAAC,SAAI,WAAU,cACZ,sBAAY,KAAK;AAAA,UAAI,CAAC,KAAK,cAC1B,IAAI,IAAI,CAAC,QAAQ,cAAc;AAC7B,gBAAI,CAAC,OAAQ,QAAO;AAEpB,kBAAM,eAAe,oBAAoB,OAAO,EAAE;AAClD,kBAAM,SAAS,cAAc,UAAU;AAEvC,mBACE;AAAA,cAAC;AAAA;AAAA,gBAEC,SAAS,MAAM,kBAAkB,MAAM;AAAA,gBACvC,WAAU;AAAA,gBACV,OAAO;AAAA,kBACL,iBAAiB,OAAO,OAAO,mBAAmB;AAAA,kBAClD,aAAa,OAAO,OAAO,eAAe;AAAA,kBAC1C,OAAO,OAAO,OAAO,aAAa,aAAa,OAAO,OAAO,eAAe;AAAA,gBAC9E;AAAA,gBACA,OAAO,GAAG,OAAO,KAAK;AAAA,EAAK,OAAO,WAAW,EAAE;AAAA,gBAE9C;AAAA,kCAAgB,oBAAoB,SAAS,KAC5C,4CAAC,SAAI,WAAU,8FACZ,iBAAO,QAAQ,CAAC,GACnB;AAAA,kBAEF;AAAA,oBAAC;AAAA;AAAA,sBACC,WAAU;AAAA,sBAET,iBAAO;AAAA;AAAA,kBACV;AAAA;AAAA;AAAA,cAnBK,OAAO;AAAA,YAoBd;AAAA,UAEJ,CAAC;AAAA,QACH,GACF;AAAA,SACF,GACF;AAAA,MAIF,6CAAC,SAAI,WAAU,UAEb;AAAA,qDAAC,SAAI,WAAU,uFACb;AAAA,uDAAC,SAAI,WAAU,2BACZ;AAAA,wBAAY,SAAS,KACpB;AAAA,cAAC;AAAA;AAAA,gBACC,SAAS;AAAA,gBACT,WAAU;AAAA,gBACV,cAAW;AAAA,gBAEX;AAAA,kBAAC;AAAA;AAAA,oBACC,WAAU;AAAA,oBACV,MAAK;AAAA,oBACL,QAAO;AAAA,oBACP,SAAQ;AAAA,oBAER;AAAA,sBAAC;AAAA;AAAA,wBACC,eAAc;AAAA,wBACd,gBAAe;AAAA,wBACf,aAAa;AAAA,wBACb,GAAE;AAAA;AAAA,oBACJ;AAAA;AAAA,gBACF;AAAA;AAAA,YACF;AAAA,YAEF,4CAAC,QAAG,WAAU,uDACX,sBAAY,MACf;AAAA,aACF;AAAA,UACA,6CAAC,SAAI,WAAU,4CACZ;AAAA;AAAA,YAAS;AAAA,YAAE;AAAA,YAAS;AAAA,aACvB;AAAA,WACF;AAAA,QAGA;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,OAAO;AAAA,cACL,SAAS;AAAA,cACT,qBAAqB,UAAU,QAAQ;AAAA,YACzC;AAAA,YAEE,iBAAM;AACN,oBAAM,WAAW,oBAAI,IAAY;AACjC,qBAAO,YAAY,KAAK;AAAA,gBAAQ,CAAC,KAAK,aACpC,IAAI,IAAI,CAAC,QAAQ,aAAa;AAC5B,sBAAI,CAAC,QAAQ;AACX,2BAAO,4CAAC,SAA0C,WAAU,mBAA3C,SAAS,QAAQ,IAAI,QAAQ,EAA8B;AAAA,kBAC9E;AAEA,sBAAI,SAAS,IAAI,OAAO,EAAE,GAAG;AAC3B,2BAAO;AAAA,kBACT;AACA,2BAAS,IAAI,OAAO,EAAE;AAEtB,wBAAM,eAAe,oBAAoB,OAAO,EAAE;AAClD,wBAAM,SAAS,cAAc,UAAU;AACvC,wBAAM,eAAe,OAAO,gBAAgB,OAAO,gBAAgB;AACnE,wBAAM,UAAU,gBAAgB,KAAK,MAAM,YAAY;AACvD,wBAAM,UAAU,OAAO,cAAc;AACrC,wBAAM,UAAU,OAAO,WAAW;AAClC,wBAAM,cACJ,OAAO,eAAgB,OAAO,YAA2C;AAC3E,wBAAM,iBAAiB,eAAe,YAAY,SAAS;AAC3D,wBAAM,mBACJ,OAAO,gBAAgB,kBACtB,OAAO,kBAAkB,IAAI,YAAY,MAAM;AAClD,wBAAM,cAAc,OAAO,gBAAgB;AAE3C,wBAAM,gBACJ,aACA,UAAU,WAAW,mBACnB,UAAU,MAAM,UAAa,UAAU,MAAM,SAC3C,OAAO,MAAM,UAAU,KAAK,OAAO,MAAM,UAAU,IACnD,UACD,UAAU,SAAS,OAAO,UAAU,UAAU;AAInD,wBAAM,YACH,OAAO,sBAAsB,CAAC,OAAO,OAAO,kBAAkB,EAAE,WAAW,GAAG,IAC3E,OAAO,qBACP,UACH,OAAO,SAAS,CAAC,OAAO,OAAO,KAAK,EAAE,WAAW,GAAG,IAAI,OAAO,QAAQ;AAI1E,wBAAM,SAAS,OAAO;AAKtB,sBAAI,SAA6B;AACjC,0BAAQ,IAAI,yBAAyB,OAAO,OAAO,WAAW,QAAQ,uBAAuB,OAAO,kBAAkB;AACtH,sBAAI,QAAQ;AACV,4BAAQ,IAAI,wDAAwD;AAEpE,wBAAI,OAAO,SAAS,OAAO,MAAM,SAAS,OAAQ,OAAO,UAAU;AACjE,+BAAS,cAAc,MAAM,IAAI,OAAO,QAAQ;AAChD,8BAAQ,IAAI,yCAAyC,MAAM;AAAA,oBAC7D,WAES,OAAO,sBAAsB,CAAC,OAAO,mBAAmB,WAAW,GAAG,GAAG;AAEhF,4BAAM,YAAY,OAAO;AACzB,4BAAM,YAAY,UAAU,MAAM,oBAAoB;AACtD,0BAAI,WAAW;AACb,iCAAS,cAAc,MAAM,IAAI,UAAU,CAAC,CAAC;AAC7C,gCAAQ,IAAI,yBAAyB,OAAO,OAAO,uBAAuB,WAAW,WAAW,MAAM;AAAA,sBACxG,OAAO;AACL,gCAAQ,IAAI,yBAAyB,OAAO,OAAO,gCAAgC,SAAS;AAAA,sBAC9F;AAAA,oBACF,OAAO;AACL,8BAAQ,IAAI,0DAA0D;AAAA,oBACxE;AAAA,kBACF,OAAO;AACL,4BAAQ,IAAI,wCAAwC;AAAA,kBACtD;AAEA,sBAAI,aAAa;AACf,2BACE;AAAA,sBAAC;AAAA;AAAA,wBAEC,WAAU;AAAA,wBACV,OAAO;AAAA,0BACL,aAAa,OAAO,OAAO,eAAe;AAAA,0BAC1C,YAAY,GAAG,WAAW,CAAC,WAAW,OAAO;AAAA,0BAC7C,SAAS,GAAG,WAAW,CAAC,WAAW,OAAO;AAAA,wBAC5C;AAAA,wBAEA;AAAA,sEAAC,SAAI,WAAU,yBAAyB,iBAAO,SAAS,aAAY;AAAA,0BACpE,4CAAC,SAAI,WAAU,kCACZ,qBAAW,qBACd;AAAA;AAAA;AAAA,sBAXK,OAAO;AAAA,oBAYd;AAAA,kBAEJ;AAEA,yBACE;AAAA,oBAAC;AAAA;AAAA,sBAEC,SAAS,MAAM,kBAAkB,MAAM;AAAA,sBACvC,KAAK,gBAAgB,uBAAuB;AAAA,sBAC5C,WAAW,8JACT,gBACI,0DACA,EACN;AAAA,sBACA,OAAO;AAAA,wBACL,iBAAiB,OAAO,OAAO,mBAAmB;AAAA,wBAClD,aAAa,OAAO,OAAO,eAAe;AAAA,wBAC1C,OAAO,OAAO,OAAO,aAAa,aAAa,OAAO,OAAO,eAAe;AAAA,wBAC5E,YAAY,GAAG,WAAW,CAAC,WAAW,OAAO;AAAA,wBAC7C,SAAS,GAAG,WAAW,CAAC,WAAW,OAAO;AAAA,sBAC5C;AAAA,sBAGC;AAAA,wCAAgB,oBACf,4CAAC,SAAI,WAAU,uGACZ,iBAAO,QAAQ,CAAC,GACnB;AAAA,wBAID,WAAW,sBACV,4CAAC,SAAI,WAAU,qEAAoE;AAAA,wBAIpF,kBACC;AAAA,0BAAC;AAAA;AAAA,4BACC,SAAS,CAAC,MAAM,sBAAsB,QAAQ,CAAC;AAAA,4BAC/C,WAAU;AAAA,4BACV,OAAO,OAAO,aAAa,MAAM,aAAa,eAAe,YAAY,SAAS,IAAI,MAAM,EAAE;AAAA,4BAE7F,uBAAa;AAAA;AAAA,wBAChB;AAAA,yBAIA,YAAY,WACZ;AAAA,0BAAC;AAAA;AAAA,4BACC,MAAM,UAAU,aAAa;AAAA,4BAC7B,KAAK,OAAO;AAAA,4BACZ,WAAU;AAAA;AAAA,wBACZ;AAAA,wBAIF,6CAAC,SAAI,WAAU,6CACb;AAAA;AAAA,4BAAC;AAAA;AAAA,8BACC,WAAU;AAAA,8BAET,iBAAO;AAAA;AAAA,0BACV;AAAA,0BACC,oBAAoB,eAAe,YAAY,SAAS,KACvD,6CAAC,SAAI,WAAU,kEACZ;AAAA,wCAAY,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,QAC/B,4CAAC,SACE,eADO,GAAG,OAAO,EAAE,SAAS,GAAG,EAElC,CACD;AAAA,4BACA,YAAY,SAAS,KAAK,4CAAC,SAAI,oBAAC;AAAA,6BACnC;AAAA,2BAEJ;AAAA,wBAGC,OAAO,WAAW,OAAO,YAAY,OAAO,SAAS,CAAC,oBACrD;AAAA,0BAAC;AAAA;AAAA,4BACC,WAAU;AAAA,4BAET,iBAAO;AAAA;AAAA,wBACV;AAAA;AAAA;AAAA,oBAzEG,OAAO;AAAA,kBA2Ed;AAAA,gBAEJ,CAAC;AAAA,cACH;AAAA,YACF,GAAG;AAAA;AAAA,QACL;AAAA,QAGC,OAAO,KAAK,KAAK,KAAK,EAAE,SAAS,KAChC,4CAAC,SAAI,WAAU,qDACb,uDAAC,OAAE,WAAU,iDACV;AAAA,iBAAO,KAAK,KAAK,KAAK,EAAE;AAAA,UAAO;AAAA,WAClC,GACF;AAAA,SAEJ;AAAA,OACF;AAAA,IAGC,sBACC;AAAA,MAAC;AAAA;AAAA,QACC,aAAa,mBAAmB;AAAA,QAChC,OAAO,mBAAmB;AAAA,QAC1B,UAAU,mBAAmB;AAAA,QAC7B,qBAAqB,mBAAmB;AAAA,QACxC,aAAa,mBAAmB;AAAA,QAChC,SAAS,MAAM,sBAAsB,IAAI;AAAA;AAAA,IAC3C;AAAA,KAEJ;AAEJ;","names":["React"]}
|
package/dist/board-viewer.mjs
CHANGED
|
@@ -77,6 +77,8 @@ function BoardViewer({
|
|
|
77
77
|
className = "",
|
|
78
78
|
loadId
|
|
79
79
|
}) {
|
|
80
|
+
console.log("[BoardViewer] COMPONENT LOADED - Running updated code!");
|
|
81
|
+
console.log("[BoardViewer] loadId:", loadId);
|
|
80
82
|
const resolveInitialPageId = useCallback(() => {
|
|
81
83
|
if (initialPageId && tree.pages[initialPageId]) {
|
|
82
84
|
return initialPageId;
|
|
@@ -423,12 +425,26 @@ ${button.message || ""}`,
|
|
|
423
425
|
const imageSrc = (button.resolvedImageEntry && !String(button.resolvedImageEntry).startsWith("[") ? button.resolvedImageEntry : null) || (button.image && !String(button.image).startsWith("[") ? button.image : null);
|
|
424
426
|
const params = button.parameters;
|
|
425
427
|
let apiUrl = void 0;
|
|
426
|
-
|
|
427
|
-
|
|
428
|
+
console.log("[BoardViewer] Button:", button.label, "loadId:", loadId, "resolvedImageEntry:", button.resolvedImageEntry);
|
|
429
|
+
if (loadId) {
|
|
430
|
+
console.log("[BoardViewer] loadId exists, checking image conditions");
|
|
431
|
+
if (button.image && button.image.length > 1e3 && params.image_id) {
|
|
428
432
|
apiUrl = `/api/image/${loadId}/${params.image_id}`;
|
|
429
|
-
|
|
430
|
-
|
|
433
|
+
console.log("[BoardViewer] Using OBZ image_id API:", apiUrl);
|
|
434
|
+
} else if (button.resolvedImageEntry && !button.resolvedImageEntry.startsWith("[")) {
|
|
435
|
+
const entryPath = button.resolvedImageEntry;
|
|
436
|
+
const pathMatch = entryPath.match(/^(?:Grids\/)?(.+)$/);
|
|
437
|
+
if (pathMatch) {
|
|
438
|
+
apiUrl = `/api/image/${loadId}/${pathMatch[1]}`;
|
|
439
|
+
console.log("[BoardViewer] Button:", button.label, "resolvedImageEntry:", entryPath, "apiUrl:", apiUrl);
|
|
440
|
+
} else {
|
|
441
|
+
console.log("[BoardViewer] Button:", button.label, "resolvedImageEntry no match:", entryPath);
|
|
442
|
+
}
|
|
443
|
+
} else {
|
|
444
|
+
console.log("[BoardViewer] loadId exists but no valid image condition");
|
|
431
445
|
}
|
|
446
|
+
} else {
|
|
447
|
+
console.log("[BoardViewer] loadId is undefined/null");
|
|
432
448
|
}
|
|
433
449
|
if (isWorkspace) {
|
|
434
450
|
return /* @__PURE__ */ jsxs(
|
|
@@ -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 "{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 highlight,\n onButtonClick,\n onPageChange,\n className = '',\n loadId,\n}: BoardViewerProps) {\n const resolveInitialPageId = useCallback(() => {\n // Explicit override\n if (initialPageId && tree.pages[initialPageId]) {\n return initialPageId;\n }\n // If toolbar exists and rootId is different, use rootId\n if (tree.toolbarId && tree.rootId && tree.rootId !== tree.toolbarId) {\n return tree.rootId;\n }\n // If rootId exists and is not a toolbar, use it\n if (tree.rootId && tree.pages[tree.rootId]) {\n const rootPage = tree.pages[tree.rootId];\n const isToolbar =\n rootPage.name.toLowerCase().includes('toolbar') ||\n rootPage.name.toLowerCase().includes('tool bar');\n if (!isToolbar) {\n return tree.rootId;\n }\n }\n // Explicit Start page fallback if present\n const startPage = Object.values(tree.pages).find(\n (p) => p.name.toLowerCase() === 'start'\n );\n if (startPage) {\n return startPage.id;\n }\n // Fall back to first page that's not a toolbar\n const nonToolbarPage = Object.values(tree.pages).find(\n (p) => !p.name.toLowerCase().includes('toolbar') && !p.name.toLowerCase().includes('tool bar')\n );\n if (nonToolbarPage) {\n return nonToolbarPage.id;\n }\n // Last resort: first page\n const pageIds = Object.keys(tree.pages);\n return pageIds.length > 0 ? pageIds[0] : null;\n }, [initialPageId, tree]);\n\n // Determine which page to show\n const [currentPageId, setCurrentPageId] = useState<string | null>(() => resolveInitialPageId());\n\n const 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 (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.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 const imageSrc =\n (button.resolvedImageEntry && !String(button.resolvedImageEntry).startsWith('[')\n ? button.resolvedImageEntry\n : null) ||\n (button.image && !String(button.image).startsWith('[') ? button.image : null);\n\n // For OBZ files with loadId, use the image API endpoint\n // For Grid3 files, also use API if we have gridPageName and position\n const params = button.parameters as {\n image_id?: string;\n gridPageName?: string;\n };\n\n let apiUrl: string | undefined = undefined;\n if (loadId && button.image) {\n // OBZ files have large data URLs or image_id\n if (button.image.length > 1000 && params.image_id) {\n apiUrl = `/api/image/${loadId}/${params.image_id}`;\n }\n // Grid3 files have gridPageName and x,y coordinates\n else if (params.gridPageName && button.x !== undefined && button.y !== undefined) {\n apiUrl = `/api/image/${loadId}/${params.gridPageName}/${button.x}-${button.y}`;\n }\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={(apiUrl || imageSrc) ?? undefined}\n alt={button.label}\n className=\"max-h-12 object-contain\"\n />\n )}\n\n {/* Label / Predictions */}\n <div className=\"flex flex-col items-center justify-center\">\n <span\n className=\"text-xs sm:text-sm text-center font-medium leading-tight line-clamp-3\"\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}\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 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 (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.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 // Note: For Grid3 files with loadId, we'll use apiUrl instead (set below)\n const imageSrc =\n (button.resolvedImageEntry && !String(button.resolvedImageEntry).startsWith('[')\n ? button.resolvedImageEntry\n : null) ||\n (button.image && !String(button.image).startsWith('[') ? button.image : null);\n\n // For OBZ files with loadId, use the image API endpoint\n // For Grid3 files, also use API if we have resolvedImageEntry\n const params = button.parameters as {\n image_id?: string;\n gridPageName?: string;\n };\n\n let apiUrl: string | undefined = undefined;\n console.log('[BoardViewer] Button:', button.label, 'loadId:', loadId, 'resolvedImageEntry:', button.resolvedImageEntry);\n if (loadId) {\n console.log('[BoardViewer] loadId exists, checking image conditions');\n // OBZ files have large data URLs or image_id\n if (button.image && button.image.length > 1000 && params.image_id) {\n apiUrl = `/api/image/${loadId}/${params.image_id}`;\n console.log('[BoardViewer] Using OBZ image_id API:', apiUrl);\n }\n // Grid3 files: use resolvedImageEntry if available (contains full path)\n else if (button.resolvedImageEntry && !button.resolvedImageEntry.startsWith('[')) {\n // Extract just the path after \"Grids/\" for the API\n const entryPath = button.resolvedImageEntry;\n const pathMatch = entryPath.match(/^(?:Grids\\/)?(.+)$/);\n if (pathMatch) {\n apiUrl = `/api/image/${loadId}/${pathMatch[1]}`;\n console.log('[BoardViewer] Button:', button.label, 'resolvedImageEntry:', entryPath, 'apiUrl:', apiUrl);\n } else {\n console.log('[BoardViewer] Button:', button.label, 'resolvedImageEntry no match:', entryPath);\n }\n } else {\n console.log('[BoardViewer] loadId exists but no valid image condition');\n }\n } else {\n console.log('[BoardViewer] loadId is undefined/null');\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={(apiUrl || imageSrc) ?? undefined}\n alt={button.label}\n className=\"max-h-12 object-contain\"\n />\n )}\n\n {/* Label / Predictions */}\n <div className=\"flex flex-col items-center justify-center\">\n <span\n className=\"text-xs sm:text-sm text-center font-medium leading-tight line-clamp-3\"\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":";AAAA,OAAO,SAAS,UAAU,SAAS,mBAAmB;AA2C9C,SAqVc,UA5UV,KATJ;AAtBR,SAAS,mBAAmB,EAAE,aAAa,OAAO,UAAU,qBAAqB,SAAS,YAAY,GAA4B;AAEhI,QAAM,UAAU,MAAM;AACpB,UAAM,qBAAqB,CAAC,MAAkB;AAC5C,UAAI,EAAE,kBAAkB,eAAe,CAAC,EAAE,OAAO,QAAQ,sBAAsB,GAAG;AAChF,gBAAQ;AAAA,MACV;AAAA,IACF;AACA,aAAS,iBAAiB,aAAa,kBAAkB;AACzD,WAAO,MAAM,SAAS,oBAAoB,aAAa,kBAAkB;AAAA,EAC3E,GAAG,CAAC,OAAO,CAAC;AAEZ,SACE;AAAA,IAAC;AAAA;AAAA,MACC,WAAU;AAAA,MACV,OAAO;AAAA,QACL,MAAM,GAAG,KAAK,IAAI,SAAS,GAAG,OAAO,aAAa,GAAG,CAAC;AAAA,QACtD,KAAK,GAAG,KAAK,IAAI,SAAS,GAAG,OAAO,cAAc,GAAG,CAAC;AAAA,MACxD;AAAA,MACA,SAAS,CAAC,MAAM,EAAE,gBAAgB;AAAA,MAElC;AAAA,6BAAC,SAAI,WAAU,0CACb;AAAA,+BAAC,QAAG,WAAU,uDAAsD;AAAA;AAAA,YAC5C;AAAA,YAAM;AAAA,aAC9B;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,SAAS;AAAA,cACT,WAAU;AAAA,cACV,cAAW;AAAA,cAEX,8BAAC,SAAI,WAAU,4CAA2C,MAAK,QAAO,QAAO,gBAAe,SAAQ,aAClG,8BAAC,UAAK,eAAc,SAAQ,gBAAe,SAAQ,aAAa,GAAG,GAAE,wBAAuB,GAC9F;AAAA;AAAA,UACF;AAAA,WACF;AAAA,QACA,oBAAC,SAAI,WAAU,wBACZ,sBAAY,IAAI,CAAC,MAAM,QAAQ;AAE9B,gBAAM,gBAAgB,OAAO,OAAO,mBAAmB,EAAE,KAAK,OAAK,EAAE,UAAU,IAAI;AACnF,gBAAM,SAAS,eAAe;AAE9B,iBACE;AAAA,YAAC;AAAA;AAAA,cAEC,WAAU;AAAA,cACV,SAAS,CAAC,MAAM;AACd,kBAAE,gBAAgB;AAClB,oBAAI,aAAa;AACf,8BAAY,MAAM,UAAU,CAAG;AAC/B,0BAAQ;AAAA,gBACV;AAAA,cACF;AAAA,cACA,OAAO,iBAAiB,IAAI;AAAA,cAE3B;AAAA,2BAAW,UACV,oBAAC,UAAK,WAAU,wGACb,iBAAO,QAAQ,CAAC,GACnB;AAAA,gBAED;AAAA;AAAA;AAAA,YAhBI;AAAA,UAiBP;AAAA,QAEJ,CAAC,GACH;AAAA;AAAA;AAAA,EACF;AAEJ;AAUO,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA,iBAAiB;AAAA,EACjB,mBAAmB;AAAA,EACnB,qBAAqB;AAAA,EACrB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AAAA,EACZ;AACF,GAAqB;AACnB,UAAQ,IAAI,wDAAwD;AACpE,UAAQ,IAAI,yBAAyB,MAAM;AAC3C,QAAM,uBAAuB,YAAY,MAAM;AAE7C,QAAI,iBAAiB,KAAK,MAAM,aAAa,GAAG;AAC9C,aAAO;AAAA,IACT;AAEA,QAAI,KAAK,aAAa,KAAK,UAAU,KAAK,WAAW,KAAK,WAAW;AACnE,aAAO,KAAK;AAAA,IACd;AAEA,QAAI,KAAK,UAAU,KAAK,MAAM,KAAK,MAAM,GAAG;AAC1C,YAAM,WAAW,KAAK,MAAM,KAAK,MAAM;AACvC,YAAM,YACJ,SAAS,KAAK,YAAY,EAAE,SAAS,SAAS,KAC9C,SAAS,KAAK,YAAY,EAAE,SAAS,UAAU;AACjD,UAAI,CAAC,WAAW;AACd,eAAO,KAAK;AAAA,MACd;AAAA,IACF;AAEA,UAAM,YAAY,OAAO,OAAO,KAAK,KAAK,EAAE;AAAA,MAC1C,CAAC,MAAM,EAAE,KAAK,YAAY,MAAM;AAAA,IAClC;AACA,QAAI,WAAW;AACb,aAAO,UAAU;AAAA,IACnB;AAEA,UAAM,iBAAiB,OAAO,OAAO,KAAK,KAAK,EAAE;AAAA,MAC/C,CAAC,MAAM,CAAC,EAAE,KAAK,YAAY,EAAE,SAAS,SAAS,KAAK,CAAC,EAAE,KAAK,YAAY,EAAE,SAAS,UAAU;AAAA,IAC/F;AACA,QAAI,gBAAgB;AAClB,aAAO,eAAe;AAAA,IACxB;AAEA,UAAM,UAAU,OAAO,KAAK,KAAK,KAAK;AACtC,WAAO,QAAQ,SAAS,IAAI,QAAQ,CAAC,IAAI;AAAA,EAC3C,GAAG,CAAC,eAAe,IAAI,CAAC;AAGxB,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAwB,MAAM,qBAAqB,CAAC;AAE9F,QAAM,uBAAuB,MAAM,OAAiC,IAAI;AAExE,QAAM,CAAC,aAAa,cAAc,IAAI,SAAoB,CAAC,CAAC;AAC5D,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,EAAE;AACzC,QAAM,CAAC,kBAAkB,mBAAmB,IAAI,SAAS,CAAC;AAC1D,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,CAAC;AAGpD,QAAM,CAAC,oBAAoB,qBAAqB,IAAI,SAM1C,IAAI;AAGd,QAAM,sBAAsB,QAAQ,MAAM;AACxC,QAAI,CAAC,cAAe,QAAO,CAAC;AAC5B,UAAM,SAA+C,CAAC;AACtD,kBAAc,QAAQ,CAAC,WAAW;AAChC,aAAO,OAAO,EAAE,IAAI;AAAA,IACtB,CAAC;AACD,WAAO;AAAA,EACT,GAAG,CAAC,aAAa,CAAC;AAElB,QAAM,cAAc,gBAAgB,KAAK,MAAM,aAAa,IAAI;AAChE,QAAM,WAAW,CAAC,iBAA4C;AAC5D,QAAI,CAAC,gBAAgB,CAAC,KAAK,MAAM,YAAY,EAAG,QAAO;AACvD,QAAI,aAAa;AACf,qBAAe,CAAC,SAAS,CAAC,GAAG,MAAM,WAAW,CAAC;AAAA,IACjD;AACA,qBAAiB,YAAY;AAC7B,QAAI,cAAc;AAChB,mBAAa,YAAY;AAAA,IAC3B;AACA,WAAO;AAAA,EACT;AAGA,QAAM,UAAU,MAAM;AACpB,qBAAiB,qBAAqB,CAAC;AACvC,mBAAe,CAAC,CAAC;AAAA,EACnB,GAAG,CAAC,oBAAoB,CAAC;AAEzB,QAAM,UAAU,MAAM;AACpB,QAAI,qBAAqB,SAAS;AAChC,2BAAqB,QAAQ,eAAe;AAAA,QAC1C,UAAU;AAAA,QACV,OAAO;AAAA,QACP,QAAQ;AAAA,MACV,CAAC;AAAA,IACH;AAAA,EACF,GAAG,CAAC,eAAe,WAAW,QAAQ,WAAW,GAAG,WAAW,GAAG,WAAW,KAAK,CAAC;AAGnF,QAAM,cAAc,CAAC,MAAc,WAAmB;AACpD,wBAAoB,CAAC,SAAS,OAAO,CAAC;AACtC,qBAAiB,CAAC,SAAS,OAAO,MAAM;AAAA,EAC1C;AAEA,QAAM,oBAAoB,CAAC,WAAsB;AAE/C,QAAI,eAAe;AACjB,oBAAc,MAAM;AAAA,IACtB;AAEA,UAAM,SAAS,OAAO,gBAAgB,SAClC,OAAO,OAAO,eAAe,MAAM,IACnC;AACJ,UAAM,eAAe,OAAO,gBAAgB,OAAO,gBAAgB;AACnE,UAAM,SAAS,oBAAoB,OAAO,EAAE,GAAG,UAAU;AACzD,UAAM,YACJ,OAAO,gBAAgB,QAAQ,OAAO,WAAW,OAAO,SAAS;AAEnE,UAAM,iBAAiB,MAAM;AAC3B,iBAAW,CAAC,SAAS;AACnB,cAAM,QAAQ,KAAK,KAAK,EAAE,MAAM,KAAK;AACrC,cAAM,IAAI;AACV,cAAM,SAAS,MAAM,KAAK,GAAG;AAC7B,eAAO;AAAA,MACT,CAAC;AAAA,IACH;AAEA,UAAM,sBAAsB,MAAM;AAChC,iBAAW,CAAC,SAAS,KAAK,MAAM,GAAG,EAAE,CAAC;AAAA,IACxC;AAEA,UAAM,aAAa,CAAC,SAAiB;AACnC,YAAM,UAAU,QAAQ,OAAO,SAAS;AACxC,iBAAW,CAAC,SAAS;AACnB,cAAM,aAAa,UACf,QAAQ,OAAO,MAAM,MAAM,UAC3B;AACJ,YAAI,SAAS;AACX,sBAAY,SAAS,MAAM;AAAA,QAC7B;AACA,eAAO;AAAA,MACT,CAAC;AAAA,IACH;AAGA,QAAI,WAAW,iBAAiB,SAAS,YAAY,GAAG;AACtD;AAAA,IACF;AAEA,YAAQ,QAAQ;AAAA,MACd,KAAK;AACH,mBAAW;AACX;AAAA,MACF,KAAK;AACH,YAAI,KAAK,UAAU,SAAS,KAAK,MAAM,EAAG;AAC1C;AAAA,MACF,KAAK;AACH,uBAAe;AACf;AAAA,MACF,KAAK;AACH,4BAAoB;AACpB;AAAA,MACF,KAAK;AACH,qBAAa;AACb;AAAA,MACF,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AACH,mBAAW,SAAS;AACpB;AAAA,MACF;AACE;AAAA,IACJ;AAGA,QAAI,gBAAgB,SAAS,YAAY,GAAG;AAC1C;AAAA,IACF;AAGA,eAAW,SAAS;AAAA,EACtB;AAEA,QAAM,aAAa,MAAM;AACvB,QAAI,YAAY,SAAS,GAAG;AAC1B,YAAM,eAAe,YAAY,YAAY,SAAS,CAAC;AACvD,qBAAe,CAAC,SAAS,KAAK,MAAM,GAAG,EAAE,CAAC;AAC1C,uBAAiB,aAAa,EAAE;AAChC,UAAI,cAAc;AAChB,qBAAa,aAAa,EAAE;AAAA,MAC9B;AAAA,IACF;AAAA,EACF;AAEA,QAAM,eAAe,MAAM;AACzB,eAAW,EAAE;AACb,wBAAoB,CAAC;AACrB,qBAAiB,CAAC;AAAA,EACpB;AAEA,QAAM,wBAAwB,CAC5B,QACA,UACG;AACH,UAAM,gBAAgB;AACtB,UAAM,cAAc,OAAO,eAAgB,OAAO,YAA2C;AAC7F,QAAI,eAAe,YAAY,SAAS,GAAG;AACzC,4BAAsB;AAAA,QACpB;AAAA,QACA,OAAO,OAAO;AAAA,QACd,UAAU,EAAE,GAAG,MAAM,SAAS,GAAG,MAAM,QAAQ;AAAA,QAC/C;AAAA,QACA,aAAa,CAAC,MAAM,WAAW;AAC7B,gBAAM,UAAU,QAAQ;AACxB,cAAI,SAAS;AACX,uBAAW,CAAC,SAAS;AACnB,oBAAM,aAAa,QAAQ,OAAO,MAAM,MAAM;AAC9C,0BAAY,SAAS,MAAM;AAC3B,qBAAO;AAAA,YACT,CAAC;AAAA,UACH;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAEA,QAAM,eAAe,CAAC,oBAA6B;AACjD,QAAI,CAAC,gBAAiB,QAAO;AAG7B,UAAM,MAAM,gBAAgB,QAAQ,KAAK,EAAE;AAC3C,QAAI,IAAI,WAAW,GAAG;AACpB,YAAM,IAAI,SAAS,IAAI,UAAU,GAAG,CAAC,GAAG,EAAE;AAC1C,YAAM,IAAI,SAAS,IAAI,UAAU,GAAG,CAAC,GAAG,EAAE;AAC1C,YAAM,IAAI,SAAS,IAAI,UAAU,GAAG,CAAC,GAAG,EAAE;AAC1C,YAAM,cAAc,IAAI,MAAM,IAAI,MAAM,IAAI,OAAO;AACnD,aAAO,cAAc,MAAM,YAAY;AAAA,IACzC;AAEA,WAAO;AAAA,EACT;AAEA,MAAI,CAAC,aAAa;AAChB,WACE,oBAAC,SAAI,WAAW,iFAAiF,SAAS,IACxG,8BAAC,SAAI,WAAU,eACb,8BAAC,OAAE,WAAU,oCAAmC,gCAAkB,GACpE,GACF;AAAA,EAEJ;AAGA,QAAM,cAAc,KAAK,YAAY,KAAK,MAAM,KAAK,SAAS,IAAI;AAGlE,QAAM,WAAW,YAAY,KAAK;AAClC,QAAM,WAAW,WAAW,IAAI,YAAY,KAAK,CAAC,EAAE,SAAS;AAE7D,SACE,qBAAC,SAAI,WAAW,kEAAkE,SAAS,IAExF;AAAA,sBACC,oBAAC,SAAI,WAAU,oFACb,+BAAC,SAAI,WAAU,0CACb;AAAA,0BAAC,SAAI,WAAU,kBACZ,oBACC,qBAAC,SAAI,WAAU,aACb;AAAA,4BAAC,OAAE,WAAU,qDAAqD,mBAAQ;AAAA,QAC1E,qBAAC,SAAI,WAAU,sBACb;AAAA,+BAAC,SAAI,WAAU,oCACZ;AAAA;AAAA,YAAiB;AAAA,YAAE,qBAAqB,IAAI,SAAS;AAAA,aACxD;AAAA,UACC,iBACC,iCACE;AAAA,iCAAC,SAAI,WAAU,oCAAmC;AAAA;AAAA,cACxC,oBAAC,UAAK,WAAU,eAAe,wBAAc,QAAQ,CAAC,GAAE;AAAA,eAClE;AAAA,YACA,qBAAC,SAAI,WAAU,oCAAmC;AAAA;AAAA,cAC3C;AAAA,cACL,oBAAC,UAAK,WAAU,eACb,6BAAmB,KACf,gBAAgB,kBAAkB,QAAQ,CAAC,IAC5C,QACN;AAAA,eACF;AAAA,aACF;AAAA,WAEJ;AAAA,SACF,IAEA,oBAAC,OAAE,WAAU,2CAA0C,gDAEvD,GAEJ;AAAA,MACC,WACC;AAAA,QAAC;AAAA;AAAA,UACC,SAAS;AAAA,UACT,WAAU;AAAA,UACV,cAAW;AAAA,UAEX;AAAA,YAAC;AAAA;AAAA,cACC,WAAU;AAAA,cACV,MAAK;AAAA,cACL,QAAO;AAAA,cACP,SAAQ;AAAA,cAER;AAAA,gBAAC;AAAA;AAAA,kBACC,eAAc;AAAA,kBACd,gBAAe;AAAA,kBACf,aAAa;AAAA,kBACb,GAAE;AAAA;AAAA,cACJ;AAAA;AAAA,UACF;AAAA;AAAA,MACF;AAAA,OAEJ,GACF;AAAA,IAIF,qBAAC,SAAI,WAAU,QAEZ;AAAA,qBACC,oBAAC,SAAI,WAAU,6FACb,+BAAC,SAAI,WAAU,OACb;AAAA,4BAAC,OAAE,WAAU,+EAA8E,qBAE3F;AAAA,QACA,oBAAC,SAAI,WAAU,cACZ,sBAAY,KAAK;AAAA,UAAI,CAAC,KAAK,cAC1B,IAAI,IAAI,CAAC,QAAQ,cAAc;AAC7B,gBAAI,CAAC,OAAQ,QAAO;AAEpB,kBAAM,eAAe,oBAAoB,OAAO,EAAE;AAClD,kBAAM,SAAS,cAAc,UAAU;AAEvC,mBACE;AAAA,cAAC;AAAA;AAAA,gBAEC,SAAS,MAAM,kBAAkB,MAAM;AAAA,gBACvC,WAAU;AAAA,gBACV,OAAO;AAAA,kBACL,iBAAiB,OAAO,OAAO,mBAAmB;AAAA,kBAClD,aAAa,OAAO,OAAO,eAAe;AAAA,kBAC1C,OAAO,OAAO,OAAO,aAAa,aAAa,OAAO,OAAO,eAAe;AAAA,gBAC9E;AAAA,gBACA,OAAO,GAAG,OAAO,KAAK;AAAA,EAAK,OAAO,WAAW,EAAE;AAAA,gBAE9C;AAAA,kCAAgB,oBAAoB,SAAS,KAC5C,oBAAC,SAAI,WAAU,8FACZ,iBAAO,QAAQ,CAAC,GACnB;AAAA,kBAEF;AAAA,oBAAC;AAAA;AAAA,sBACC,WAAU;AAAA,sBAET,iBAAO;AAAA;AAAA,kBACV;AAAA;AAAA;AAAA,cAnBK,OAAO;AAAA,YAoBd;AAAA,UAEJ,CAAC;AAAA,QACH,GACF;AAAA,SACF,GACF;AAAA,MAIF,qBAAC,SAAI,WAAU,UAEb;AAAA,6BAAC,SAAI,WAAU,uFACb;AAAA,+BAAC,SAAI,WAAU,2BACZ;AAAA,wBAAY,SAAS,KACpB;AAAA,cAAC;AAAA;AAAA,gBACC,SAAS;AAAA,gBACT,WAAU;AAAA,gBACV,cAAW;AAAA,gBAEX;AAAA,kBAAC;AAAA;AAAA,oBACC,WAAU;AAAA,oBACV,MAAK;AAAA,oBACL,QAAO;AAAA,oBACP,SAAQ;AAAA,oBAER;AAAA,sBAAC;AAAA;AAAA,wBACC,eAAc;AAAA,wBACd,gBAAe;AAAA,wBACf,aAAa;AAAA,wBACb,GAAE;AAAA;AAAA,oBACJ;AAAA;AAAA,gBACF;AAAA;AAAA,YACF;AAAA,YAEF,oBAAC,QAAG,WAAU,uDACX,sBAAY,MACf;AAAA,aACF;AAAA,UACA,qBAAC,SAAI,WAAU,4CACZ;AAAA;AAAA,YAAS;AAAA,YAAE;AAAA,YAAS;AAAA,aACvB;AAAA,WACF;AAAA,QAGA;AAAA,UAAC;AAAA;AAAA,YACC,WAAU;AAAA,YACV,OAAO;AAAA,cACL,SAAS;AAAA,cACT,qBAAqB,UAAU,QAAQ;AAAA,YACzC;AAAA,YAEE,iBAAM;AACN,oBAAM,WAAW,oBAAI,IAAY;AACjC,qBAAO,YAAY,KAAK;AAAA,gBAAQ,CAAC,KAAK,aACpC,IAAI,IAAI,CAAC,QAAQ,aAAa;AAC5B,sBAAI,CAAC,QAAQ;AACX,2BAAO,oBAAC,SAA0C,WAAU,mBAA3C,SAAS,QAAQ,IAAI,QAAQ,EAA8B;AAAA,kBAC9E;AAEA,sBAAI,SAAS,IAAI,OAAO,EAAE,GAAG;AAC3B,2BAAO;AAAA,kBACT;AACA,2BAAS,IAAI,OAAO,EAAE;AAEtB,wBAAM,eAAe,oBAAoB,OAAO,EAAE;AAClD,wBAAM,SAAS,cAAc,UAAU;AACvC,wBAAM,eAAe,OAAO,gBAAgB,OAAO,gBAAgB;AACnE,wBAAM,UAAU,gBAAgB,KAAK,MAAM,YAAY;AACvD,wBAAM,UAAU,OAAO,cAAc;AACrC,wBAAM,UAAU,OAAO,WAAW;AAClC,wBAAM,cACJ,OAAO,eAAgB,OAAO,YAA2C;AAC3E,wBAAM,iBAAiB,eAAe,YAAY,SAAS;AAC3D,wBAAM,mBACJ,OAAO,gBAAgB,kBACtB,OAAO,kBAAkB,IAAI,YAAY,MAAM;AAClD,wBAAM,cAAc,OAAO,gBAAgB;AAE3C,wBAAM,gBACJ,aACA,UAAU,WAAW,mBACnB,UAAU,MAAM,UAAa,UAAU,MAAM,SAC3C,OAAO,MAAM,UAAU,KAAK,OAAO,MAAM,UAAU,IACnD,UACD,UAAU,SAAS,OAAO,UAAU,UAAU;AAInD,wBAAM,YACH,OAAO,sBAAsB,CAAC,OAAO,OAAO,kBAAkB,EAAE,WAAW,GAAG,IAC3E,OAAO,qBACP,UACH,OAAO,SAAS,CAAC,OAAO,OAAO,KAAK,EAAE,WAAW,GAAG,IAAI,OAAO,QAAQ;AAI1E,wBAAM,SAAS,OAAO;AAKtB,sBAAI,SAA6B;AACjC,0BAAQ,IAAI,yBAAyB,OAAO,OAAO,WAAW,QAAQ,uBAAuB,OAAO,kBAAkB;AACtH,sBAAI,QAAQ;AACV,4BAAQ,IAAI,wDAAwD;AAEpE,wBAAI,OAAO,SAAS,OAAO,MAAM,SAAS,OAAQ,OAAO,UAAU;AACjE,+BAAS,cAAc,MAAM,IAAI,OAAO,QAAQ;AAChD,8BAAQ,IAAI,yCAAyC,MAAM;AAAA,oBAC7D,WAES,OAAO,sBAAsB,CAAC,OAAO,mBAAmB,WAAW,GAAG,GAAG;AAEhF,4BAAM,YAAY,OAAO;AACzB,4BAAM,YAAY,UAAU,MAAM,oBAAoB;AACtD,0BAAI,WAAW;AACb,iCAAS,cAAc,MAAM,IAAI,UAAU,CAAC,CAAC;AAC7C,gCAAQ,IAAI,yBAAyB,OAAO,OAAO,uBAAuB,WAAW,WAAW,MAAM;AAAA,sBACxG,OAAO;AACL,gCAAQ,IAAI,yBAAyB,OAAO,OAAO,gCAAgC,SAAS;AAAA,sBAC9F;AAAA,oBACF,OAAO;AACL,8BAAQ,IAAI,0DAA0D;AAAA,oBACxE;AAAA,kBACF,OAAO;AACL,4BAAQ,IAAI,wCAAwC;AAAA,kBACtD;AAEA,sBAAI,aAAa;AACf,2BACE;AAAA,sBAAC;AAAA;AAAA,wBAEC,WAAU;AAAA,wBACV,OAAO;AAAA,0BACL,aAAa,OAAO,OAAO,eAAe;AAAA,0BAC1C,YAAY,GAAG,WAAW,CAAC,WAAW,OAAO;AAAA,0BAC7C,SAAS,GAAG,WAAW,CAAC,WAAW,OAAO;AAAA,wBAC5C;AAAA,wBAEA;AAAA,8CAAC,SAAI,WAAU,yBAAyB,iBAAO,SAAS,aAAY;AAAA,0BACpE,oBAAC,SAAI,WAAU,kCACZ,qBAAW,qBACd;AAAA;AAAA;AAAA,sBAXK,OAAO;AAAA,oBAYd;AAAA,kBAEJ;AAEA,yBACE;AAAA,oBAAC;AAAA;AAAA,sBAEC,SAAS,MAAM,kBAAkB,MAAM;AAAA,sBACvC,KAAK,gBAAgB,uBAAuB;AAAA,sBAC5C,WAAW,8JACT,gBACI,0DACA,EACN;AAAA,sBACA,OAAO;AAAA,wBACL,iBAAiB,OAAO,OAAO,mBAAmB;AAAA,wBAClD,aAAa,OAAO,OAAO,eAAe;AAAA,wBAC1C,OAAO,OAAO,OAAO,aAAa,aAAa,OAAO,OAAO,eAAe;AAAA,wBAC5E,YAAY,GAAG,WAAW,CAAC,WAAW,OAAO;AAAA,wBAC7C,SAAS,GAAG,WAAW,CAAC,WAAW,OAAO;AAAA,sBAC5C;AAAA,sBAGC;AAAA,wCAAgB,oBACf,oBAAC,SAAI,WAAU,uGACZ,iBAAO,QAAQ,CAAC,GACnB;AAAA,wBAID,WAAW,sBACV,oBAAC,SAAI,WAAU,qEAAoE;AAAA,wBAIpF,kBACC;AAAA,0BAAC;AAAA;AAAA,4BACC,SAAS,CAAC,MAAM,sBAAsB,QAAQ,CAAC;AAAA,4BAC/C,WAAU;AAAA,4BACV,OAAO,OAAO,aAAa,MAAM,aAAa,eAAe,YAAY,SAAS,IAAI,MAAM,EAAE;AAAA,4BAE7F,uBAAa;AAAA;AAAA,wBAChB;AAAA,yBAIA,YAAY,WACZ;AAAA,0BAAC;AAAA;AAAA,4BACC,MAAM,UAAU,aAAa;AAAA,4BAC7B,KAAK,OAAO;AAAA,4BACZ,WAAU;AAAA;AAAA,wBACZ;AAAA,wBAIF,qBAAC,SAAI,WAAU,6CACb;AAAA;AAAA,4BAAC;AAAA;AAAA,8BACC,WAAU;AAAA,8BAET,iBAAO;AAAA;AAAA,0BACV;AAAA,0BACC,oBAAoB,eAAe,YAAY,SAAS,KACvD,qBAAC,SAAI,WAAU,kEACZ;AAAA,wCAAY,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,QAC/B,oBAAC,SACE,eADO,GAAG,OAAO,EAAE,SAAS,GAAG,EAElC,CACD;AAAA,4BACA,YAAY,SAAS,KAAK,oBAAC,SAAI,oBAAC;AAAA,6BACnC;AAAA,2BAEJ;AAAA,wBAGC,OAAO,WAAW,OAAO,YAAY,OAAO,SAAS,CAAC,oBACrD;AAAA,0BAAC;AAAA;AAAA,4BACC,WAAU;AAAA,4BAET,iBAAO;AAAA;AAAA,wBACV;AAAA;AAAA;AAAA,oBAzEG,OAAO;AAAA,kBA2Ed;AAAA,gBAEJ,CAAC;AAAA,cACH;AAAA,YACF,GAAG;AAAA;AAAA,QACL;AAAA,QAGC,OAAO,KAAK,KAAK,KAAK,EAAE,SAAS,KAChC,oBAAC,SAAI,WAAU,qDACb,+BAAC,OAAE,WAAU,iDACV;AAAA,iBAAO,KAAK,KAAK,KAAK,EAAE;AAAA,UAAO;AAAA,WAClC,GACF;AAAA,SAEJ;AAAA,OACF;AAAA,IAGC,sBACC;AAAA,MAAC;AAAA;AAAA,QACC,aAAa,mBAAmB;AAAA,QAChC,OAAO,mBAAmB;AAAA,QAC1B,UAAU,mBAAmB;AAAA,QAC7B,qBAAqB,mBAAmB;AAAA,QACxC,aAAa,mBAAmB;AAAA,QAChC,SAAS,MAAM,sBAAsB,IAAI;AAAA;AAAA,IAC3C;AAAA,KAEJ;AAEJ;","names":[]}
|
package/dist/index.js
CHANGED
|
@@ -128,6 +128,8 @@ function BoardViewer({
|
|
|
128
128
|
className = "",
|
|
129
129
|
loadId
|
|
130
130
|
}) {
|
|
131
|
+
console.log("[BoardViewer] COMPONENT LOADED - Running updated code!");
|
|
132
|
+
console.log("[BoardViewer] loadId:", loadId);
|
|
131
133
|
const resolveInitialPageId = (0, import_react.useCallback)(() => {
|
|
132
134
|
if (initialPageId && tree.pages[initialPageId]) {
|
|
133
135
|
return initialPageId;
|
|
@@ -474,12 +476,26 @@ ${button.message || ""}`,
|
|
|
474
476
|
const imageSrc = (button.resolvedImageEntry && !String(button.resolvedImageEntry).startsWith("[") ? button.resolvedImageEntry : null) || (button.image && !String(button.image).startsWith("[") ? button.image : null);
|
|
475
477
|
const params = button.parameters;
|
|
476
478
|
let apiUrl = void 0;
|
|
477
|
-
|
|
478
|
-
|
|
479
|
+
console.log("[BoardViewer] Button:", button.label, "loadId:", loadId, "resolvedImageEntry:", button.resolvedImageEntry);
|
|
480
|
+
if (loadId) {
|
|
481
|
+
console.log("[BoardViewer] loadId exists, checking image conditions");
|
|
482
|
+
if (button.image && button.image.length > 1e3 && params.image_id) {
|
|
479
483
|
apiUrl = `/api/image/${loadId}/${params.image_id}`;
|
|
480
|
-
|
|
481
|
-
|
|
484
|
+
console.log("[BoardViewer] Using OBZ image_id API:", apiUrl);
|
|
485
|
+
} else if (button.resolvedImageEntry && !button.resolvedImageEntry.startsWith("[")) {
|
|
486
|
+
const entryPath = button.resolvedImageEntry;
|
|
487
|
+
const pathMatch = entryPath.match(/^(?:Grids\/)?(.+)$/);
|
|
488
|
+
if (pathMatch) {
|
|
489
|
+
apiUrl = `/api/image/${loadId}/${pathMatch[1]}`;
|
|
490
|
+
console.log("[BoardViewer] Button:", button.label, "resolvedImageEntry:", entryPath, "apiUrl:", apiUrl);
|
|
491
|
+
} else {
|
|
492
|
+
console.log("[BoardViewer] Button:", button.label, "resolvedImageEntry no match:", entryPath);
|
|
493
|
+
}
|
|
494
|
+
} else {
|
|
495
|
+
console.log("[BoardViewer] loadId exists but no valid image condition");
|
|
482
496
|
}
|
|
497
|
+
} else {
|
|
498
|
+
console.log("[BoardViewer] loadId is undefined/null");
|
|
483
499
|
}
|
|
484
500
|
if (isWorkspace) {
|
|
485
501
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
@@ -731,7 +747,12 @@ async function calculateMetrics(tree, options = {}) {
|
|
|
731
747
|
console.warn("MetricsCalculator not available in this environment");
|
|
732
748
|
return [];
|
|
733
749
|
}
|
|
734
|
-
const
|
|
750
|
+
const MetricsCalculatorConstructor = aacProcessors.MetricsCalculator;
|
|
751
|
+
if (!MetricsCalculatorConstructor) {
|
|
752
|
+
console.warn("MetricsCalculator constructor not available");
|
|
753
|
+
return [];
|
|
754
|
+
}
|
|
755
|
+
const calculator = new MetricsCalculatorConstructor();
|
|
735
756
|
let metricsOptions = {};
|
|
736
757
|
if (options.accessMethod === "scanning" && options.scanningConfig) {
|
|
737
758
|
let cellScanningOrder = 0;
|