aac-board-viewer 0.1.7 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +39 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +39 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +12 -12
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import * as
|
|
3
|
-
import { AACTree } from '@willwade/aac-processors';
|
|
2
|
+
import * as _willwade_aac_processors_browser from '@willwade/aac-processors/browser';
|
|
3
|
+
import { AACTree } from '@willwade/aac-processors/browser';
|
|
4
4
|
export { AACButton, AACPage, AACSemanticAction, AACSemanticCategory, AACSemanticIntent, AACTree } from '@willwade/aac-processors';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -25,7 +25,7 @@ interface ButtonMetric {
|
|
|
25
25
|
*/
|
|
26
26
|
interface BoardViewerProps {
|
|
27
27
|
/** The AAC tree containing pages and navigation structure */
|
|
28
|
-
tree:
|
|
28
|
+
tree: _willwade_aac_processors_browser.AACTree;
|
|
29
29
|
/** Optional button metrics to display effort scores */
|
|
30
30
|
buttonMetrics?: ButtonMetric[] | null;
|
|
31
31
|
/** Show the message bar at the top (default: true) */
|
|
@@ -37,7 +37,7 @@ interface BoardViewerProps {
|
|
|
37
37
|
/** Start the viewer on this page id (overrides tree.rootId) */
|
|
38
38
|
initialPageId?: string;
|
|
39
39
|
/** Callback when a button is clicked */
|
|
40
|
-
onButtonClick?: (button:
|
|
40
|
+
onButtonClick?: (button: _willwade_aac_processors_browser.AACButton) => void;
|
|
41
41
|
/** Callback when page changes */
|
|
42
42
|
onPageChange?: (pageId: string) => void;
|
|
43
43
|
/** Custom CSS class name */
|
|
@@ -49,7 +49,7 @@ interface BoardViewerProps {
|
|
|
49
49
|
* Result from loading an AAC file
|
|
50
50
|
*/
|
|
51
51
|
interface LoadAACFileResult {
|
|
52
|
-
tree:
|
|
52
|
+
tree: _willwade_aac_processors_browser.AACTree;
|
|
53
53
|
format: string;
|
|
54
54
|
metadata?: {
|
|
55
55
|
[key: string]: unknown;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import * as
|
|
3
|
-
import { AACTree } from '@willwade/aac-processors';
|
|
2
|
+
import * as _willwade_aac_processors_browser from '@willwade/aac-processors/browser';
|
|
3
|
+
import { AACTree } from '@willwade/aac-processors/browser';
|
|
4
4
|
export { AACButton, AACPage, AACSemanticAction, AACSemanticCategory, AACSemanticIntent, AACTree } from '@willwade/aac-processors';
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -25,7 +25,7 @@ interface ButtonMetric {
|
|
|
25
25
|
*/
|
|
26
26
|
interface BoardViewerProps {
|
|
27
27
|
/** The AAC tree containing pages and navigation structure */
|
|
28
|
-
tree:
|
|
28
|
+
tree: _willwade_aac_processors_browser.AACTree;
|
|
29
29
|
/** Optional button metrics to display effort scores */
|
|
30
30
|
buttonMetrics?: ButtonMetric[] | null;
|
|
31
31
|
/** Show the message bar at the top (default: true) */
|
|
@@ -37,7 +37,7 @@ interface BoardViewerProps {
|
|
|
37
37
|
/** Start the viewer on this page id (overrides tree.rootId) */
|
|
38
38
|
initialPageId?: string;
|
|
39
39
|
/** Callback when a button is clicked */
|
|
40
|
-
onButtonClick?: (button:
|
|
40
|
+
onButtonClick?: (button: _willwade_aac_processors_browser.AACButton) => void;
|
|
41
41
|
/** Callback when page changes */
|
|
42
42
|
onPageChange?: (pageId: string) => void;
|
|
43
43
|
/** Custom CSS class name */
|
|
@@ -49,7 +49,7 @@ interface BoardViewerProps {
|
|
|
49
49
|
* Result from loading an AAC file
|
|
50
50
|
*/
|
|
51
51
|
interface LoadAACFileResult {
|
|
52
|
-
tree:
|
|
52
|
+
tree: _willwade_aac_processors_browser.AACTree;
|
|
53
53
|
format: string;
|
|
54
54
|
metadata?: {
|
|
55
55
|
[key: string]: unknown;
|
package/dist/index.js
CHANGED
|
@@ -582,7 +582,7 @@ function isBrowserEnvironment() {
|
|
|
582
582
|
return typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
583
583
|
}
|
|
584
584
|
async function importProcessors() {
|
|
585
|
-
return import("@willwade/aac-processors");
|
|
585
|
+
return import("@willwade/aac-processors/browser");
|
|
586
586
|
}
|
|
587
587
|
async function getProcessorForFile(filepath, options) {
|
|
588
588
|
const { getProcessor } = await importProcessors();
|
|
@@ -601,7 +601,13 @@ async function getProcessorForFile(filepath, options) {
|
|
|
601
601
|
throw new Error(`Unsupported file type: ${extension}`);
|
|
602
602
|
}
|
|
603
603
|
if (extension === ".sps" || extension === ".spb") {
|
|
604
|
-
|
|
604
|
+
if (isBrowserEnvironment()) {
|
|
605
|
+
throw new Error(
|
|
606
|
+
`SNAP files (.sps, .spb) require server-side processing. Please use the server API or upload a browser-compatible format. Browser supports: ${BROWSER_EXTENSIONS.join(", ")}`
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
const nodeProcessors = await import("@willwade/aac-processors");
|
|
610
|
+
const { SnapProcessor } = nodeProcessors;
|
|
605
611
|
return {
|
|
606
612
|
processor: options ? new SnapProcessor(null, options) : new SnapProcessor(),
|
|
607
613
|
extension
|
|
@@ -987,6 +993,37 @@ function useSentenceBuilder() {
|
|
|
987
993
|
clear
|
|
988
994
|
};
|
|
989
995
|
}
|
|
996
|
+
|
|
997
|
+
// src/index.ts
|
|
998
|
+
if (typeof window !== "undefined" && typeof window.Buffer === "undefined") {
|
|
999
|
+
window.Buffer = {
|
|
1000
|
+
from: (data) => {
|
|
1001
|
+
if (typeof data === "string") {
|
|
1002
|
+
const encoder = new TextEncoder();
|
|
1003
|
+
return encoder.encode(data);
|
|
1004
|
+
}
|
|
1005
|
+
if (data instanceof ArrayBuffer) {
|
|
1006
|
+
return new Uint8Array(data);
|
|
1007
|
+
}
|
|
1008
|
+
if (data instanceof Uint8Array) {
|
|
1009
|
+
return data;
|
|
1010
|
+
}
|
|
1011
|
+
return new Uint8Array(data);
|
|
1012
|
+
},
|
|
1013
|
+
alloc: (size) => new Uint8Array(size),
|
|
1014
|
+
allocUnsafe: (size) => new Uint8Array(size),
|
|
1015
|
+
concat: (list, totalLength) => {
|
|
1016
|
+
const result = new Uint8Array(totalLength || list.reduce((sum, arr) => sum + arr.length, 0));
|
|
1017
|
+
let offset = 0;
|
|
1018
|
+
for (const arr of list) {
|
|
1019
|
+
result.set(arr, offset);
|
|
1020
|
+
offset += arr.length;
|
|
1021
|
+
}
|
|
1022
|
+
return result;
|
|
1023
|
+
},
|
|
1024
|
+
isBuffer: (obj) => obj instanceof Uint8Array
|
|
1025
|
+
};
|
|
1026
|
+
}
|
|
990
1027
|
// Annotate the CommonJS export names for ESM import in node:
|
|
991
1028
|
0 && (module.exports = {
|
|
992
1029
|
BoardViewer,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/components/BoardViewer.tsx","../src/hooks/useAACFile.ts","../src/utils/loaders.ts"],"sourcesContent":["/**\n * AAC Board Viewer\n *\n * Universal AAC board viewer component for React\n */\n\n// Main component\nexport { BoardViewer } from './components/BoardViewer';\n\n// Hooks\nexport {\n useAACFile,\n useAACFileFromFile,\n useAACFileWithMetrics,\n useAACFileFromFileWithMetrics,\n useMetrics,\n useSentenceBuilder,\n} from './hooks/useAACFile';\n\n// Utilities\nexport {\n loadAACFile,\n loadAACFileFromURL,\n loadAACFileWithMetadata,\n calculateMetrics,\n getSupportedFormats,\n isBrowserCompatible,\n getBrowserExtensions,\n getNodeOnlyExtensions,\n} from './utils/loaders';\n\n// Types\nexport type {\n BoardViewerProps,\n ButtonMetric,\n LoadAACFileResult,\n MetricsOptions,\n} from './types';\n\n// Re-export AAC processor types\nexport type {\n AACTree,\n AACPage,\n AACButton,\n AACSemanticAction,\n AACSemanticCategory,\n AACSemanticIntent,\n} from '@willwade/aac-processors';\n","import React, { useState, useMemo, useCallback } from 'react';\nimport type {\n AACPage,\n AACButton,\n} from '@willwade/aac-processors';\nimport type { BoardViewerProps, ButtonMetric } from '../types';\n\n/**\n * Predictions Tooltip Component\n *\n * Shows a tooltip with predicted word forms when clicking the predictions indicator\n */\ninterface PredictionsTooltipProps {\n predictions: string[];\n label: string;\n position: { x: number; y: number };\n buttonMetricsLookup: { [buttonId: string]: ButtonMetric };\n onClose: () => void;\n onWordClick?: (word: string, effort: number) => void;\n}\n\nfunction PredictionsTooltip({ predictions, label, position, buttonMetricsLookup, onClose, onWordClick }: PredictionsTooltipProps) {\n // Close tooltip when clicking outside\n React.useEffect(() => {\n const handleClickOutside = (e: MouseEvent) => {\n if (e.target instanceof HTMLElement && !e.target.closest('.predictions-tooltip')) {\n onClose();\n }\n };\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, [onClose]);\n\n return (\n <div\n className=\"predictions-tooltip fixed z-50 bg-white dark:bg-gray-800 rounded-lg shadow-xl border-2 border-purple-500 p-3 max-w-xs\"\n style={{\n left: `${Math.min(position.x, window.innerWidth - 200)}px`,\n top: `${Math.min(position.y, window.innerHeight - 150)}px`,\n }}\n onClick={(e) => e.stopPropagation()} // Prevent clicks from bubbling to underlying button\n >\n <div className=\"flex items-center justify-between mb-2\">\n <h4 className=\"text-sm font-semibold text-gray-900 dark:text-white\">\n Word forms for "{label}"\n </h4>\n <button\n onClick={onClose}\n className=\"p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded\"\n aria-label=\"Close\"\n >\n <svg className=\"h-4 w-4 text-gray-600 dark:text-gray-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n <div className=\"flex flex-wrap gap-1\">\n {predictions.map((word, idx) => {\n // Try to find metrics for this word form by label\n const metricForWord = Object.values(buttonMetricsLookup).find(m => m.label === word);\n const effort = metricForWord?.effort;\n\n return (\n <span\n key={idx}\n className=\"px-2 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded text-xs font-medium relative cursor-pointer hover:bg-purple-200 dark:hover:bg-purple-800 transition\"\n onClick={(e) => {\n e.stopPropagation(); // Prevent bubbling\n if (onWordClick) {\n onWordClick(word, effort || 1.0);\n onClose();\n }\n }}\n title={`Click to add \"${word}\" to message`}\n >\n {effort !== undefined && (\n <span className=\"absolute -top-1 -right-1 px-1 py-0 text-[8px] font-semibold rounded bg-blue-600 text-white shadow-xs\">\n {effort.toFixed(1)}\n </span>\n )}\n {word}\n </span>\n );\n })}\n </div>\n </div>\n );\n}\n\n/**\n * AAC Board Viewer Component\n *\n * Displays AAC boards with interactive navigation, sentence building,\n * and optional effort metrics.\n *\n * @param props - BoardViewerProps\n */\nexport function BoardViewer({\n tree,\n buttonMetrics,\n showMessageBar = true,\n showEffortBadges = true,\n showLinkIndicators = true,\n initialPageId,\n 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 [pageHistory, setPageHistory] = useState<AACPage[]>([]);\n const [message, setMessage] = useState('');\n const [currentWordCount, setCurrentWordCount] = useState(0);\n const [currentEffort, setCurrentEffort] = useState(0);\n\n // Predictions tooltip state\n const [predictionsTooltip, setPredictionsTooltip] = useState<{\n predictions: string[];\n label: string;\n position: { x: number; y: number };\n buttonMetricsLookup: { [buttonId: string]: ButtonMetric };\n onWordClick?: (word: string, effort: number) => void;\n } | null>(null);\n\n // Convert button metrics array to lookup object for easy access\n const buttonMetricsLookup = useMemo(() => {\n if (!buttonMetrics) return {};\n const lookup: { [buttonId: string]: ButtonMetric } = {};\n buttonMetrics.forEach((metric) => {\n lookup[metric.id] = metric;\n });\n return lookup;\n }, [buttonMetrics]);\n\n const currentPage = currentPageId ? tree.pages[currentPageId] : null;\n const goToPage = (targetPageId: string | undefined | null) => {\n if (!targetPageId || !tree.pages[targetPageId]) return false;\n if (currentPage) {\n setPageHistory((prev) => [...prev, currentPage]);\n }\n setCurrentPageId(targetPageId);\n if (onPageChange) {\n onPageChange(targetPageId);\n }\n return true;\n };\n\n // Sync when tree or initialPageId changes\n React.useEffect(() => {\n setCurrentPageId(resolveInitialPageId());\n setPageHistory([]);\n }, [resolveInitialPageId]);\n\n // Calculate total stats for current word\n const updateStats = (word: string, effort: number) => {\n setCurrentWordCount((prev) => prev + 1);\n setCurrentEffort((prev) => prev + effort);\n };\n\n const handleButtonClick = (button: AACButton) => {\n // Call external callback if provided\n if (onButtonClick) {\n onButtonClick(button);\n }\n\n const intent = button.semanticAction?.intent\n ? String(button.semanticAction.intent)\n : undefined;\n const targetPageId = button.targetPageId || button.semanticAction?.targetId;\n const effort = buttonMetricsLookup[button.id]?.effort || 1;\n const textValue =\n button.semanticAction?.text || button.message || button.label || '';\n\n const deleteLastWord = () => {\n setMessage((prev) => {\n const parts = prev.trim().split(/\\s+/);\n parts.pop();\n const newMsg = parts.join(' ');\n return newMsg;\n });\n };\n\n const deleteLastCharacter = () => {\n setMessage((prev) => prev.slice(0, -1));\n };\n\n const appendText = (word: string) => {\n const trimmed = word || button.label || '';\n setMessage((prev) => {\n const newMessage = trimmed\n ? prev + (prev ? ' ' : '') + trimmed\n : prev;\n if (trimmed) {\n updateStats(trimmed, effort);\n }\n return newMessage;\n });\n };\n\n // Navigation takes precedence\n if (intent === 'NAVIGATE_TO' && goToPage(targetPageId)) {\n return;\n }\n\n switch (intent) {\n case 'GO_BACK':\n handleBack();\n return;\n case 'GO_HOME':\n if (tree.rootId && goToPage(tree.rootId)) return;\n break;\n case 'DELETE_WORD':\n deleteLastWord();\n return;\n case 'DELETE_CHARACTER':\n deleteLastCharacter();\n return;\n case 'CLEAR_TEXT':\n clearMessage();\n return;\n case 'SPEAK_IMMEDIATE':\n case 'SPEAK_TEXT':\n case 'INSERT_TEXT':\n appendText(textValue);\n return;\n default:\n break;\n }\n\n // Fallback navigation if intent not set but target exists\n if (targetPageId && goToPage(targetPageId)) {\n return;\n }\n\n // Otherwise add to message\n appendText(textValue);\n };\n\n const handleBack = () => {\n if (pageHistory.length > 0) {\n const previousPage = pageHistory[pageHistory.length - 1];\n setPageHistory((prev) => prev.slice(0, -1));\n setCurrentPageId(previousPage.id);\n if (onPageChange) {\n onPageChange(previousPage.id);\n }\n }\n };\n\n const clearMessage = () => {\n setMessage('');\n setCurrentWordCount(0);\n setCurrentEffort(0);\n };\n\n const handleShowPredictions = (\n button: AACButton,\n event: React.MouseEvent<HTMLDivElement>\n ) => {\n event.stopPropagation(); // Prevent button click\n const predictions = button.predictions || (button.parameters as { predictions?: string[] })?.predictions;\n if (predictions && predictions.length > 0) {\n setPredictionsTooltip({\n predictions,\n label: button.label,\n position: { x: event.clientX, y: event.clientY },\n buttonMetricsLookup,\n onWordClick: (word, effort) => {\n const trimmed = word || '';\n if (trimmed) {\n setMessage((prev) => {\n const newMessage = prev + (prev ? ' ' : '') + trimmed;\n updateStats(trimmed, effort);\n return newMessage;\n });\n }\n },\n });\n }\n };\n\n const getTextColor = (backgroundColor?: string) => {\n if (!backgroundColor) return 'text-gray-900 dark:text-gray-100';\n\n // Convert hex to rgb for brightness calculation\n const hex = backgroundColor.replace('#', '');\n if (hex.length === 6) {\n const r = parseInt(hex.substring(0, 2), 16);\n const g = parseInt(hex.substring(2, 4), 16);\n const b = parseInt(hex.substring(4, 6), 16);\n const brightness = (r * 299 + g * 587 + b * 114) / 1000;\n return brightness >= 128 ? 'text-gray-900' : 'text-white';\n }\n\n return 'text-gray-900 dark:text-gray-100';\n };\n\n if (!currentPage) {\n return (\n <div className={`flex items-center justify-center h-96 bg-gray-100 dark:bg-gray-800 rounded-lg ${className}`}>\n <div className=\"text-center\">\n <p className=\"text-gray-600 dark:text-gray-400\">No pages available</p>\n </div>\n </div>\n );\n }\n\n // Get toolbar page if it exists\n const toolbarPage = tree.toolbarId ? tree.pages[tree.toolbarId] : null;\n\n // Get grid dimensions\n const gridRows = currentPage.grid.length;\n const gridCols = gridRows > 0 ? currentPage.grid[0].length : 0;\n\n return (\n <div className={`bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden ${className}`}>\n {/* Message Bar */}\n {showMessageBar && (\n <div className=\"p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50\">\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex-1 min-w-0\">\n {message ? (\n <div className=\"space-y-2\">\n <p className=\"text-lg text-gray-900 dark:text-white break-words\">{message}</p>\n <div className=\"flex gap-4 text-sm\">\n <div className=\"text-gray-600 dark:text-gray-400\">\n {currentWordCount} {currentWordCount === 1 ? 'word' : 'words'}\n </div>\n {buttonMetrics && (\n <>\n <div className=\"text-gray-600 dark:text-gray-400\">\n Effort: <span className=\"font-medium\">{currentEffort.toFixed(2)}</span>\n </div>\n <div className=\"text-gray-600 dark:text-gray-400\">\n Avg:{' '}\n <span className=\"font-medium\">\n {currentWordCount > 0\n ? (currentEffort / currentWordCount).toFixed(2)\n : '0.00'}\n </span>\n </div>\n </>\n )}\n </div>\n </div>\n ) : (\n <p className=\"text-gray-500 dark:text-gray-400 italic\">\n Tap buttons to build a sentence...\n </p>\n )}\n </div>\n {message && (\n <button\n onClick={clearMessage}\n className=\"p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition\"\n aria-label=\"Clear message\"\n >\n <svg\n className=\"h-5 w-5 text-gray-600 dark:text-gray-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M6 18L18 6M6 6l12 12\"\n />\n </svg>\n </button>\n )}\n </div>\n </div>\n )}\n\n {/* Main Content Area */}\n <div className=\"flex\">\n {/* Toolbar Sidebar (if exists) */}\n {toolbarPage && (\n <div className=\"w-16 sm:w-20 border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50\">\n <div className=\"p-2\">\n <p className=\"text-[10px] text-gray-500 dark:text-gray-400 text-center mb-2 font-semibold\">\n TOOLBAR\n </p>\n <div className=\"grid gap-1\">\n {toolbarPage.grid.map((row, _rowIndex) =>\n row.map((button, _colIndex) => {\n if (!button) return null;\n\n const buttonMetric = buttonMetricsLookup[button.id];\n const effort = buttonMetric?.effort || 0;\n\n return (\n <button\n key={button.id}\n onClick={() => handleButtonClick(button)}\n className=\"aspect-square p-1 rounded border border-gray-200 dark:border-gray-700 transition flex flex-col items-center justify-center gap-0.5 hover:bg-gray-200 dark:hover:bg-gray-700 relative\"\n style={{\n backgroundColor: button.style?.backgroundColor || '#f3f4f6',\n borderColor: button.style?.borderColor || '#e5e7eb',\n }}\n title={`${button.label}\\n${button.message || ''}`}\n >\n {buttonMetric && showEffortBadges && effort > 0 && (\n <div className=\"absolute top-0 right-0 px-0.5 py-0 text-[8px] font-semibold rounded bg-blue-600 text-white\">\n {effort.toFixed(1)}\n </div>\n )}\n <span\n className={`text-[8px] sm:text-[9px] text-center font-medium leading-tight line-clamp-2 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.label}\n </span>\n </button>\n );\n })\n )}\n </div>\n </div>\n </div>\n )}\n\n {/* Main Page Content */}\n <div className=\"flex-1\">\n {/* Header */}\n <div className=\"flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700\">\n <div className=\"flex items-center gap-2\">\n {pageHistory.length > 0 && (\n <button\n onClick={handleBack}\n className=\"p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition\"\n aria-label=\"Go back\"\n >\n <svg\n className=\"h-5 w-5 text-gray-600 dark:text-gray-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 19l-7-7 7-7\"\n />\n </svg>\n </button>\n )}\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white\">\n {currentPage.name}\n </h3>\n </div>\n <div className=\"text-sm text-gray-500 dark:text-gray-400\">\n {gridRows}×{gridCols} grid\n </div>\n </div>\n\n {/* Grid */}\n <div\n className=\"p-4 gap-2 overflow-auto max-h-[600px]\"\n style={{\n display: 'grid',\n gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`,\n }}\n >\n {(() => {\n const rendered = new Set<string>();\n return currentPage.grid.flatMap((row, rowIndex) =>\n row.map((button, colIndex) => {\n if (!button) {\n return <div key={`empty-${rowIndex}-${colIndex}`} className=\"aspect-square\" />;\n }\n\n if (rendered.has(button.id)) {\n return null;\n }\n rendered.add(button.id);\n\n const buttonMetric = buttonMetricsLookup[button.id];\n const effort = buttonMetric?.effort || 0;\n const targetPageId = button.targetPageId || button.semanticAction?.targetId;\n const hasLink = targetPageId && tree.pages[targetPageId];\n const colSpan = button.columnSpan || 1;\n const rowSpan = button.rowSpan || 1;\n const predictions =\n button.predictions || (button.parameters as { predictions?: string[] })?.predictions;\n const hasPredictions = predictions && predictions.length > 0;\n const isPredictionCell =\n button.contentType === 'AutoContent' &&\n (button.contentSubType || '').toLowerCase() === 'prediction';\n const isWorkspace = button.contentType === 'Workspace';\n\n const imageSrc =\n (button.resolvedImageEntry && !String(button.resolvedImageEntry).startsWith('[')\n ? button.resolvedImageEntry\n : null) ||\n (button.image && !String(button.image).startsWith('[') ? button.image : null);\n\n // 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 className=\"relative aspect-square p-2 rounded-lg border-2 transition flex flex-col items-center justify-center gap-1 hover:opacity-80 hover:scale-105 active:scale-95\"\n style={{\n backgroundColor: button.style?.backgroundColor || '#f3f4f6',\n borderColor: button.style?.borderColor || '#e5e7eb',\n color: button.style?.fontColor || undefined,\n gridColumn: `${colIndex + 1} / span ${colSpan}`,\n gridRow: `${rowIndex + 1} / span ${rowSpan}`,\n }}\n >\n {/* Effort Badge */}\n {buttonMetric && showEffortBadges && (\n <div className=\"absolute top-1 right-1 px-1.5 py-0.5 text-xs font-semibold rounded bg-blue-600 text-white shadow-sm\">\n {effort.toFixed(1)}\n </div>\n )}\n\n {/* Link Indicator */}\n {hasLink && showLinkIndicators && (\n <div className=\"absolute top-1 left-1 w-2 h-2 bg-green-500 rounded-full shadow-sm\" />\n )}\n\n {/* Predictions Indicator */}\n {hasPredictions && (\n <div\n onClick={(e) => handleShowPredictions(button, e)}\n className=\"absolute bottom-1 right-1 px-1.5 py-0.5 text-xs font-semibold rounded bg-purple-600 text-white shadow-sm cursor-pointer hover:bg-purple-700 transition\"\n title={`Has ${predictions?.length} word form${predictions && predictions.length > 1 ? 's' : ''}`}\n >\n {predictions?.length}\n </div>\n )}\n\n {/* Image */}\n {(imageSrc || 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 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.label}\n </span>\n {isPredictionCell && predictions && predictions.length > 0 && (\n <div className=\"mt-1 text-[10px] sm:text-xs text-center opacity-80 space-y-0.5\">\n {predictions.slice(0, 3).map((p, idx) => (\n <div key={`${button.id}-pred-${idx}`}>\n {p}\n </div>\n ))}\n {predictions.length > 3 && <div>…</div>}\n </div>\n )}\n </div>\n\n {/* Message (if different from label) */}\n {button.message && button.message !== button.label && !isPredictionCell && (\n <span\n className={`text-[10px] sm:text-xs text-center opacity-75 line-clamp-2 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.message}\n </span>\n )}\n </button>\n );\n })\n );\n })()}\n </div>\n\n {/* Page Navigation (if multiple pages) */}\n {Object.keys(tree.pages).length > 1 && (\n <div className=\"p-4 border-t border-gray-200 dark:border-gray-700\">\n <p className=\"text-sm text-gray-600 dark:text-gray-400 mb-2\">\n {Object.keys(tree.pages).length} pages in this vocabulary\n </p>\n </div>\n )}\n </div>\n </div>\n\n {/* Predictions Tooltip */}\n {predictionsTooltip && (\n <PredictionsTooltip\n predictions={predictionsTooltip.predictions}\n label={predictionsTooltip.label}\n position={predictionsTooltip.position}\n buttonMetricsLookup={predictionsTooltip.buttonMetricsLookup}\n onWordClick={predictionsTooltip.onWordClick}\n onClose={() => setPredictionsTooltip(null)}\n />\n )}\n </div>\n );\n}\n","/**\n * React hooks for AAC Board Viewer\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { loadAACFile, loadAACFileFromURL, calculateMetrics } from '../utils/loaders';\nimport type { AACTree } from '@willwade/aac-processors';\nimport type { ButtonMetric, MetricsOptions } from '../types';\n\n/**\n * Hook to load an AAC file from a URL\n *\n * @param url - URL to the AAC file\n * @param options - Processor options (e.g., pageLayoutPreference for SNAP)\n * @returns Object with tree, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer() {\n * const { tree, loading, error, reload } = useAACFile('/files/board.sps');\n *\n * if (loading) return <div>Loading...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n *\n * return <BoardViewer tree={tree} />;\n * }\n * ```\n */\nexport function useAACFile(\n url: string,\n options?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (options?.enabled === false) {\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFileFromURL(url, options?.processorOptions);\n setTree(loadedTree);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [url, options]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to load an AAC file from a File object (Browser only)\n *\n * @param file - File object from file input\n * @param options - Processor options\n * @returns Object with tree, loading state, and error\n *\n * @example\n * ```tsx\n * function MyUploader() {\n * const [file, setFile] = useState<File | null>(null);\n * const { tree, loading, error } = useAACFileFromFile(file);\n *\n * return (\n * <div>\n * <input type=\"file\" onChange={(e) => setFile(e.target.files?.[0])} />\n * {loading && <div>Loading...</div>}\n * {tree && <BoardViewer tree={tree} />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useAACFileFromFile(\n file: File | Blob | null,\n options?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (!file || options?.enabled === false) {\n setTree(null);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFile(file, options?.processorOptions);\n setTree(loadedTree);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [file, options]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to load an AAC file and calculate metrics\n *\n * @param url - URL to the AAC file\n * @param metricsOptions - Metrics calculation options\n * @returns Object with tree, metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer() {\n * const { tree, metrics, loading, error } = useAACFileWithMetrics(\n * '/files/board.sps',\n * { accessMethod: 'direct' }\n * );\n *\n * if (loading) return <div>Loading...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n *\n * return <BoardViewer tree={tree} buttonMetrics={metrics} />;\n * }\n * ```\n */\nexport function useAACFileWithMetrics(\n url: string,\n metricsOptions?: MetricsOptions,\n fileOptions?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [metrics, setMetrics] = useState<ButtonMetric[] | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (fileOptions?.enabled === false) {\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFileFromURL(url, fileOptions?.processorOptions);\n setTree(loadedTree);\n\n // Calculate metrics\n const calculatedMetrics = await calculateMetrics(loadedTree, metricsOptions || {});\n setMetrics(calculatedMetrics);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [url, metricsOptions, fileOptions]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n metrics,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to load an AAC file from a File object and calculate metrics\n *\n * @param file - File object from file input\n * @param metricsOptions - Metrics calculation options\n * @returns Object with tree, metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyUploader() {\n * const [file, setFile] = useState<File | null>(null);\n * const { tree, metrics, loading, error } = useAACFileFromFileWithMetrics(\n * file,\n * { accessMethod: 'direct' }\n * );\n *\n * return (\n * <div>\n * <input type=\"file\" onChange={(e) => setFile(e.target.files?.[0])} />\n * {loading && <div>Loading...</div>}\n * {tree && <BoardViewer tree={tree} buttonMetrics={metrics} />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useAACFileFromFileWithMetrics(\n file: File | Blob | null,\n metricsOptions?: MetricsOptions,\n fileOptions?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [metrics, setMetrics] = useState<ButtonMetric[] | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (!file || fileOptions?.enabled === false) {\n setTree(null);\n setMetrics(null);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFile(file, fileOptions?.processorOptions);\n setTree(loadedTree);\n\n // Calculate metrics\n const calculatedMetrics = await calculateMetrics(loadedTree, metricsOptions || {});\n setMetrics(calculatedMetrics);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [file, metricsOptions, fileOptions]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n metrics,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to calculate metrics for a tree\n *\n * @param tree - The AAC tree\n * @param options - Metrics calculation options\n * @returns Object with metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer({ tree }) {\n * const { metrics, loading } = useMetrics(tree, {\n * accessMethod: 'scanning',\n * scanningConfig: { pattern: 'row-column' }\n * });\n *\n * return <BoardViewer tree={tree} buttonMetrics={metrics} />;\n * }\n * ```\n */\nexport function useMetrics(\n tree: AACTree | null,\n options?: MetricsOptions\n) {\n const [metrics, setMetrics] = useState<ButtonMetric[] | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const calculate = useCallback(async () => {\n if (!tree) {\n setMetrics(null);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const calculatedMetrics = await calculateMetrics(tree, options || {});\n setMetrics(calculatedMetrics);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to calculate metrics'));\n } finally {\n setLoading(false);\n }\n }, [tree, options]);\n\n useEffect(() => {\n calculate();\n }, [calculate]);\n\n return {\n metrics,\n loading,\n error,\n recalculate: calculate,\n };\n}\n\n/**\n * Hook for sentence building state\n *\n * @returns Object with message state and handlers\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { message, wordCount, effort, addWord, clear } = useSentenceBuilder();\n *\n * return (\n * <div>\n * <p>{message || 'Start building...'}</p>\n * <p>{wordCount} words, {effort.toFixed(2)} effort</p>\n * <button onClick={clear}>Clear</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useSentenceBuilder() {\n const [message, setMessage] = useState('');\n const [wordCount, setWordCount] = useState(0);\n const [effort, setEffort] = useState(0);\n\n const addWord = useCallback((word: string, wordEffort: number = 1) => {\n setMessage((prev) => {\n const newMessage = prev + (prev ? ' ' : '') + word;\n setWordCount((prev) => prev + 1);\n setEffort((prev) => prev + wordEffort);\n return newMessage;\n });\n }, []);\n\n const clear = useCallback(() => {\n setMessage('');\n setWordCount(0);\n setEffort(0);\n }, []);\n\n return {\n message,\n wordCount,\n effort,\n addWord,\n clear,\n };\n}\n","/**\n * AAC File Loading Utilities\n *\n * Provides utilities for loading AAC files from various sources\n * (File/Blob objects, file paths, URLs) in both browser and server contexts.\n *\n * Browser: Supports .obf, .obz, .gridset, .plist, .grd, .opml, .dot\n * Node.js: Supports all formats including .sps, .spb, .ce (Node-only processors)\n */\n\nimport type { AACTree } from '@willwade/aac-processors';\nimport type { LoadAACFileResult } from '../types';\n\ntype ProcessorOptions = Record<string, unknown> | undefined;\n\ntype ProcessorModule = typeof import('@willwade/aac-processors');\n\n// Node-only file extensions that require server-side processing\nconst NODE_ONLY_EXTENSIONS = ['.sps', '.spb', '.ce'];\n\n// Browser-compatible file extensions\nconst BROWSER_EXTENSIONS = ['.obf', '.obz', '.gridset', '.plist', '.grd', '.opml', '.dot'];\n\n/**\n * Detect if running in browser environment\n */\nfunction isBrowserEnvironment(): boolean {\n return typeof window !== 'undefined' && typeof window.document !== 'undefined';\n}\n\n/**\n * Lazily load processors so browser bundles avoid pulling in Node APIs\n */\nasync function importProcessors(): Promise<ProcessorModule> {\n return import('@willwade/aac-processors');\n}\n\n/**\n * Get the appropriate processor for a file based on its extension\n */\nasync function getProcessorForFile(\n filepath: string,\n options?: ProcessorOptions\n): Promise<{ processor: any; extension: string }> {\n const { getProcessor } = await importProcessors();\n\n const ext = filepath.toLowerCase().split('.').pop();\n if (!ext) {\n throw new Error('Invalid file path: no extension found');\n }\n\n const extension = '.' + ext;\n\n // Check if this is a Node-only processor in browser environment\n if (isBrowserEnvironment() && NODE_ONLY_EXTENSIONS.includes(extension)) {\n throw new Error(\n `File type ${extension} requires server-side processing. ` +\n `Please use the server API or upload a browser-compatible format. ` +\n `Browser supports: ${BROWSER_EXTENSIONS.join(', ')}`\n );\n }\n\n // Use the factory function from v0.1.0\n const processor = getProcessor(extension);\n\n if (!processor) {\n throw new Error(`Unsupported file type: ${extension}`);\n }\n\n // For SNAP processor, it needs special options\n if (extension === '.sps' || extension === '.spb') {\n const { SnapProcessor } = await importProcessors();\n return {\n processor: options ? new SnapProcessor(null, options) : new SnapProcessor(),\n extension,\n };\n }\n\n return { processor, extension };\n}\n\n/**\n * Load an AAC file from a file path (Node.js only)\n *\n * @param filepath - Path to the AAC file\n * @param options - Processor options (e.g., pageLayoutPreference for SNAP)\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const tree = await loadAACFile('/path/to/file.sps');\n * ```\n */\nexport async function loadAACFile(\n filepath: string,\n options?: ProcessorOptions\n): Promise<AACTree>;\n\n/**\n * Load an AAC file from a File or Blob object (Browser only)\n *\n * @param file - File or Blob object\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const input = document.querySelector('input[type=\"file\"]');\n * const file = input.files[0];\n * const tree = await loadAACFile(file);\n * ```\n */\nexport async function loadAACFile(\n file: File | Blob,\n options?: ProcessorOptions\n): Promise<AACTree>;\n\n/**\n * Load an AAC file from an ArrayBuffer (Browser only)\n *\n * @param buffer - ArrayBuffer containing file data\n * @param filename - Filename with extension to detect processor\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const arrayBuffer = await file.arrayBuffer();\n * const tree = await loadAACFile(arrayBuffer, 'board.obf');\n * ```\n */\nexport async function loadAACFile(\n buffer: ArrayBuffer,\n filename: string,\n options?: ProcessorOptions\n): Promise<AACTree>;\n\n/**\n * Unified AAC file loading function that works in both browser and Node.js\n */\nexport async function loadAACFile(\n input: string | File | Blob | ArrayBuffer,\n optionsOrFilename?: ProcessorOptions | string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n // Handle ArrayBuffer case (requires filename as second parameter)\n if (input instanceof ArrayBuffer) {\n const filename = typeof optionsOrFilename === 'string' ? optionsOrFilename : 'unknown.bin';\n return loadFromArrayBuffer(input, filename, options);\n }\n\n // Handle File/Blob case (browser)\n if (input instanceof File || input instanceof Blob) {\n return loadAACFileFromFile(input, undefined, optionsOrFilename as ProcessorOptions);\n }\n\n // Handle string path case (Node.js or URL)\n if (typeof input === 'string') {\n // Check if it's a URL\n if (input.startsWith('http://') || input.startsWith('https://')) {\n return loadAACFileFromURL(input, optionsOrFilename as ProcessorOptions);\n }\n // It's a file path (Node.js only)\n return loadFromFilePath(input, optionsOrFilename as ProcessorOptions);\n }\n\n throw new Error('Invalid input type. Expected File, Blob, ArrayBuffer, or file path string.');\n}\n\n/**\n * Load from file path (Node.js only)\n */\nasync function loadFromFilePath(\n filepath: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const { processor } = await getProcessorForFile(filepath, options);\n return processor.loadIntoTree(filepath);\n}\n\n/**\n * Load from File or Blob (Browser only)\n */\nasync function loadAACFileFromFile(\n file: File | Blob,\n filename?: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n // Extract filename from File object if not provided\n const actualFilename = filename || (file instanceof File ? file.name : 'unknown.bin');\n\n // Check if this is a Node-only format\n const ext = '.' + actualFilename.toLowerCase().split('.').pop();\n if (NODE_ONLY_EXTENSIONS.includes(ext)) {\n throw new Error(\n `File type ${ext} requires server-side processing. ` +\n `Please use the server API or upload a browser-compatible format. ` +\n `Browser supports: ${BROWSER_EXTENSIONS.join(', ')}`\n );\n }\n\n // Read file as ArrayBuffer\n const arrayBuffer = await file.arrayBuffer();\n\n // Load using ArrayBuffer\n return loadFromArrayBuffer(arrayBuffer, actualFilename, options);\n}\n\n/**\n * Load from ArrayBuffer (Browser only)\n */\nasync function loadFromArrayBuffer(\n arrayBuffer: ArrayBuffer,\n filename: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const { processor } = await getProcessorForFile(filename, options);\n\n // AACProcessors v0.1.0+ supports loading from ArrayBuffer in browser\n return processor.loadIntoTree(arrayBuffer);\n}\n\n/**\n * Load an AAC file from a URL (Browser only)\n *\n * Note: This requires the server to provide the file with appropriate CORS headers.\n * For better performance, consider using loadAACFileFromFile() with direct file upload instead.\n *\n * @param url - URL to the AAC file\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const tree = await loadAACFileFromURL('https://example.com/file.obf');\n * ```\n */\nexport async function loadAACFileFromURL(\n url: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`Failed to load file: ${response.statusText}`);\n }\n\n const blob = await response.blob();\n const filename = getFilenameFromURL(url);\n\n return loadAACFileFromFile(blob, filename, options);\n}\n\n/**\n * Load an AAC file and return extended result with format info\n *\n * @param input - File path (Node.js), File/Blob (Browser), or URL string\n * @param options - Processor options\n * @returns Promise resolving to LoadAACFileResult\n */\nexport async function loadAACFileWithMetadata(\n input: string | File | Blob,\n options?: ProcessorOptions\n): Promise<LoadAACFileResult> {\n // Call loadAACFile with type assertion to handle the union type\n const tree = await loadAACFile(input as any, options);\n\n // Detect format from file extension\n let filepath = '';\n if (typeof input === 'string') {\n filepath = input;\n } else if (input instanceof File) {\n filepath = input.name;\n }\n\n const ext = filepath.toLowerCase();\n let format = 'unknown';\n\n if (ext.endsWith('.gridset')) format = 'gridset';\n else if (ext.endsWith('.sps') || ext.endsWith('.spb')) format = 'snap';\n else if (ext.endsWith('.ce')) format = 'touchchat';\n else if (ext.endsWith('.obf')) format = 'openboard';\n else if (ext.endsWith('.obz')) format = 'openboard';\n else if (ext.endsWith('.grd')) format = 'asterics-grid';\n else if (ext.endsWith('.plist')) format = 'apple-panels';\n else if (ext.endsWith('.opml')) format = 'opml';\n else if (ext.endsWith('.xlsx') || ext.endsWith('.xls')) format = 'excel';\n else if (ext.endsWith('.dot')) format = 'dot';\n\n return {\n tree,\n format,\n metadata: tree.metadata,\n };\n}\n\n/**\n * Extract filename from URL\n */\nfunction getFilenameFromURL(url: string): string {\n try {\n const urlObj = new URL(url);\n const pathname = urlObj.pathname;\n const parts = pathname.split('/');\n return parts[parts.length - 1] || 'unknown';\n } catch {\n return 'unknown';\n }\n}\n\n/**\n * Calculate cognitive effort metrics for an AAC tree\n *\n * @param tree - The AAC tree\n * @param options - Metrics calculation options\n * @returns Promise resolving to array of ButtonMetrics\n *\n * @example\n * ```ts\n * import { calculateMetrics } from 'aac-board-viewer';\n *\n * const metrics = await calculateMetrics(tree, {\n * accessMethod: 'direct',\n * });\n * ```\n */\nexport async function calculateMetrics(\n tree: AACTree,\n options: {\n accessMethod?: 'direct' | 'scanning';\n scanningConfig?: {\n pattern?: 'linear' | 'row-column' | 'block';\n selectionMethod?: string;\n errorCorrection?: boolean;\n };\n } = {}\n) {\n // Import MetricsCalculator dynamically to avoid circular dependencies\n const aacProcessors = await import('@willwade/aac-processors');\n\n // Use the calculator if available, otherwise return empty metrics\n if (!aacProcessors.MetricsCalculator) {\n console.warn('MetricsCalculator not available in this environment');\n return [];\n }\n\n const calculator = new aacProcessors.MetricsCalculator();\n\n let metricsOptions: Record<string, unknown> = {};\n\n if (options.accessMethod === 'scanning' && options.scanningConfig) {\n // Use string literals instead of enums to avoid import issues\n let cellScanningOrder = 0; // SimpleScan\n let blockScanEnabled = false;\n let selectionMethod = 0; // AutoScan\n\n switch (options.scanningConfig.pattern) {\n case 'linear':\n cellScanningOrder = 0; // SimpleScan\n break;\n case 'row-column':\n cellScanningOrder = 1; // RowColumnScan\n break;\n case 'block':\n cellScanningOrder = 1; // RowColumnScan\n blockScanEnabled = true;\n break;\n }\n\n metricsOptions = {\n scanningConfig: {\n cellScanningOrder,\n blockScanEnabled,\n selectionMethod,\n errorCorrectionEnabled: options.scanningConfig.errorCorrection || false,\n errorRate: 0.1,\n },\n };\n }\n\n const metricsResult = calculator.analyze(tree, metricsOptions);\n\n // Convert to the format expected by BoardViewer\n type MetricsButton = {\n id: string;\n label: string;\n effort: number;\n count?: number;\n level?: number;\n semantic_id?: string;\n clone_id?: string;\n };\n\n return metricsResult.buttons.map((btn: MetricsButton) => ({\n id: btn.id,\n label: btn.label,\n effort: btn.effort,\n count: btn.count ?? 0,\n is_word: true,\n level: btn.level,\n semantic_id: btn.semantic_id,\n clone_id: btn.clone_id,\n }));\n}\n\n/**\n * Get a list of supported file formats\n *\n * @returns Array of format information\n */\nexport function getSupportedFormats(): Array<{\n name: string;\n extensions: string[];\n description: string;\n browserCompatible: boolean;\n}> {\n return [\n {\n name: 'Grid 3',\n extensions: ['.gridset'],\n description: 'Smartbox Grid 3 communication boards',\n browserCompatible: true,\n },\n {\n name: 'TD Snap',\n extensions: ['.sps', '.spb'],\n description: 'Tobii Dynavox Snap files',\n browserCompatible: false,\n },\n {\n name: 'TouchChat',\n extensions: ['.ce'],\n description: 'Saltillo TouchChat files',\n browserCompatible: false,\n },\n {\n name: 'OpenBoard',\n extensions: ['.obf', '.obz'],\n description: 'OpenBoard Format (OBZ/OBF)',\n browserCompatible: true,\n },\n {\n name: 'Asterics Grid',\n extensions: ['.grd'],\n description: 'Asterics Grid files (.grd)',\n browserCompatible: true,\n },\n {\n name: 'Apple Panels',\n extensions: ['.plist'],\n description: 'Apple iOS Panels files',\n browserCompatible: true,\n },\n {\n name: 'OPML',\n extensions: ['.opml'],\n description: 'OPML outline files',\n browserCompatible: true,\n },\n {\n name: 'Excel',\n extensions: ['.xlsx', '.xls'],\n description: 'Excel spreadsheet boards',\n browserCompatible: false,\n },\n {\n name: 'DOT',\n extensions: ['.dot'],\n description: 'DOT graph visualization files',\n browserCompatible: true,\n },\n ];\n}\n\n/**\n * Check if a file format is compatible with browser processing\n *\n * @param extension - File extension (e.g., '.obf', 'obf')\n * @returns true if browser-compatible, false if server-only\n */\nexport function isBrowserCompatible(extension: string): boolean {\n const ext = extension.startsWith('.') ? extension.toLowerCase() : '.' + extension.toLowerCase();\n return BROWSER_EXTENSIONS.includes(ext);\n}\n\n/**\n * Get list of browser-compatible file extensions\n */\nexport function getBrowserExtensions(): string[] {\n return [...BROWSER_EXTENSIONS];\n}\n\n/**\n * Get list of Node-only file extensions\n */\nexport function getNodeOnlyExtensions(): string[] {\n return [...NODE_ONLY_EXTENSIONS];\n}\n"],"mappings":"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","names":["React","import_react","prev"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/components/BoardViewer.tsx","../src/hooks/useAACFile.ts","../src/utils/loaders.ts"],"sourcesContent":["/**\n * AAC Board Viewer\n *\n * Universal AAC board viewer component for React\n */\n\n// Polyfill Buffer for browser environment (required by aac-processors)\nif (typeof window !== 'undefined' && typeof (window as any).Buffer === 'undefined') {\n (window as any).Buffer = {\n from: (data: any) => {\n if (typeof data === 'string') {\n const encoder = new TextEncoder();\n return encoder.encode(data);\n }\n if (data instanceof ArrayBuffer) {\n return new Uint8Array(data);\n }\n if (data instanceof Uint8Array) {\n return data;\n }\n return new Uint8Array(data);\n },\n alloc: (size: number) => new Uint8Array(size),\n allocUnsafe: (size: number) => new Uint8Array(size),\n concat: (list: Uint8Array[], totalLength?: number) => {\n const result = new Uint8Array(totalLength || list.reduce((sum, arr) => sum + arr.length, 0));\n let offset = 0;\n for (const arr of list) {\n result.set(arr, offset);\n offset += arr.length;\n }\n return result;\n },\n isBuffer: (obj: any) => obj instanceof Uint8Array,\n };\n}\n\n// Main component\nexport { BoardViewer } from './components/BoardViewer';\n\n// Hooks\nexport {\n useAACFile,\n useAACFileFromFile,\n useAACFileWithMetrics,\n useAACFileFromFileWithMetrics,\n useMetrics,\n useSentenceBuilder,\n} from './hooks/useAACFile';\n\n// Utilities\nexport {\n loadAACFile,\n loadAACFileFromURL,\n loadAACFileWithMetadata,\n calculateMetrics,\n getSupportedFormats,\n isBrowserCompatible,\n getBrowserExtensions,\n getNodeOnlyExtensions,\n} from './utils/loaders';\n\n// Types\nexport type {\n BoardViewerProps,\n ButtonMetric,\n LoadAACFileResult,\n MetricsOptions,\n} from './types';\n\n// Re-export AAC processor types\nexport type {\n AACTree,\n AACPage,\n AACButton,\n AACSemanticAction,\n AACSemanticCategory,\n AACSemanticIntent,\n} from '@willwade/aac-processors';\n","import React, { useState, useMemo, useCallback } from 'react';\nimport type {\n AACPage,\n AACButton,\n} from '@willwade/aac-processors';\nimport type { BoardViewerProps, ButtonMetric } from '../types';\n\n/**\n * Predictions Tooltip Component\n *\n * Shows a tooltip with predicted word forms when clicking the predictions indicator\n */\ninterface PredictionsTooltipProps {\n predictions: string[];\n label: string;\n position: { x: number; y: number };\n buttonMetricsLookup: { [buttonId: string]: ButtonMetric };\n onClose: () => void;\n onWordClick?: (word: string, effort: number) => void;\n}\n\nfunction PredictionsTooltip({ predictions, label, position, buttonMetricsLookup, onClose, onWordClick }: PredictionsTooltipProps) {\n // Close tooltip when clicking outside\n React.useEffect(() => {\n const handleClickOutside = (e: MouseEvent) => {\n if (e.target instanceof HTMLElement && !e.target.closest('.predictions-tooltip')) {\n onClose();\n }\n };\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, [onClose]);\n\n return (\n <div\n className=\"predictions-tooltip fixed z-50 bg-white dark:bg-gray-800 rounded-lg shadow-xl border-2 border-purple-500 p-3 max-w-xs\"\n style={{\n left: `${Math.min(position.x, window.innerWidth - 200)}px`,\n top: `${Math.min(position.y, window.innerHeight - 150)}px`,\n }}\n onClick={(e) => e.stopPropagation()} // Prevent clicks from bubbling to underlying button\n >\n <div className=\"flex items-center justify-between mb-2\">\n <h4 className=\"text-sm font-semibold text-gray-900 dark:text-white\">\n Word forms for "{label}"\n </h4>\n <button\n onClick={onClose}\n className=\"p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded\"\n aria-label=\"Close\"\n >\n <svg className=\"h-4 w-4 text-gray-600 dark:text-gray-400\" fill=\"none\" stroke=\"currentColor\" viewBox=\"0 0 24 24\">\n <path strokeLinecap=\"round\" strokeLinejoin=\"round\" strokeWidth={2} d=\"M6 18L18 6M6 6l12 12\" />\n </svg>\n </button>\n </div>\n <div className=\"flex flex-wrap gap-1\">\n {predictions.map((word, idx) => {\n // Try to find metrics for this word form by label\n const metricForWord = Object.values(buttonMetricsLookup).find(m => m.label === word);\n const effort = metricForWord?.effort;\n\n return (\n <span\n key={idx}\n className=\"px-2 py-1 bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 rounded text-xs font-medium relative cursor-pointer hover:bg-purple-200 dark:hover:bg-purple-800 transition\"\n onClick={(e) => {\n e.stopPropagation(); // Prevent bubbling\n if (onWordClick) {\n onWordClick(word, effort || 1.0);\n onClose();\n }\n }}\n title={`Click to add \"${word}\" to message`}\n >\n {effort !== undefined && (\n <span className=\"absolute -top-1 -right-1 px-1 py-0 text-[8px] font-semibold rounded bg-blue-600 text-white shadow-xs\">\n {effort.toFixed(1)}\n </span>\n )}\n {word}\n </span>\n );\n })}\n </div>\n </div>\n );\n}\n\n/**\n * AAC Board Viewer Component\n *\n * Displays AAC boards with interactive navigation, sentence building,\n * and optional effort metrics.\n *\n * @param props - BoardViewerProps\n */\nexport function BoardViewer({\n tree,\n buttonMetrics,\n showMessageBar = true,\n showEffortBadges = true,\n showLinkIndicators = true,\n initialPageId,\n 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 [pageHistory, setPageHistory] = useState<AACPage[]>([]);\n const [message, setMessage] = useState('');\n const [currentWordCount, setCurrentWordCount] = useState(0);\n const [currentEffort, setCurrentEffort] = useState(0);\n\n // Predictions tooltip state\n const [predictionsTooltip, setPredictionsTooltip] = useState<{\n predictions: string[];\n label: string;\n position: { x: number; y: number };\n buttonMetricsLookup: { [buttonId: string]: ButtonMetric };\n onWordClick?: (word: string, effort: number) => void;\n } | null>(null);\n\n // Convert button metrics array to lookup object for easy access\n const buttonMetricsLookup = useMemo(() => {\n if (!buttonMetrics) return {};\n const lookup: { [buttonId: string]: ButtonMetric } = {};\n buttonMetrics.forEach((metric) => {\n lookup[metric.id] = metric;\n });\n return lookup;\n }, [buttonMetrics]);\n\n const currentPage = currentPageId ? tree.pages[currentPageId] : null;\n const goToPage = (targetPageId: string | undefined | null) => {\n if (!targetPageId || !tree.pages[targetPageId]) return false;\n if (currentPage) {\n setPageHistory((prev) => [...prev, currentPage]);\n }\n setCurrentPageId(targetPageId);\n if (onPageChange) {\n onPageChange(targetPageId);\n }\n return true;\n };\n\n // Sync when tree or initialPageId changes\n React.useEffect(() => {\n setCurrentPageId(resolveInitialPageId());\n setPageHistory([]);\n }, [resolveInitialPageId]);\n\n // Calculate total stats for current word\n const updateStats = (word: string, effort: number) => {\n setCurrentWordCount((prev) => prev + 1);\n setCurrentEffort((prev) => prev + effort);\n };\n\n const handleButtonClick = (button: AACButton) => {\n // Call external callback if provided\n if (onButtonClick) {\n onButtonClick(button);\n }\n\n const intent = button.semanticAction?.intent\n ? String(button.semanticAction.intent)\n : undefined;\n const targetPageId = button.targetPageId || button.semanticAction?.targetId;\n const effort = buttonMetricsLookup[button.id]?.effort || 1;\n const textValue =\n button.semanticAction?.text || button.message || button.label || '';\n\n const deleteLastWord = () => {\n setMessage((prev) => {\n const parts = prev.trim().split(/\\s+/);\n parts.pop();\n const newMsg = parts.join(' ');\n return newMsg;\n });\n };\n\n const deleteLastCharacter = () => {\n setMessage((prev) => prev.slice(0, -1));\n };\n\n const appendText = (word: string) => {\n const trimmed = word || button.label || '';\n setMessage((prev) => {\n const newMessage = trimmed\n ? prev + (prev ? ' ' : '') + trimmed\n : prev;\n if (trimmed) {\n updateStats(trimmed, effort);\n }\n return newMessage;\n });\n };\n\n // Navigation takes precedence\n if (intent === 'NAVIGATE_TO' && goToPage(targetPageId)) {\n return;\n }\n\n switch (intent) {\n case 'GO_BACK':\n handleBack();\n return;\n case 'GO_HOME':\n if (tree.rootId && goToPage(tree.rootId)) return;\n break;\n case 'DELETE_WORD':\n deleteLastWord();\n return;\n case 'DELETE_CHARACTER':\n deleteLastCharacter();\n return;\n case 'CLEAR_TEXT':\n clearMessage();\n return;\n case 'SPEAK_IMMEDIATE':\n case 'SPEAK_TEXT':\n case 'INSERT_TEXT':\n appendText(textValue);\n return;\n default:\n break;\n }\n\n // Fallback navigation if intent not set but target exists\n if (targetPageId && goToPage(targetPageId)) {\n return;\n }\n\n // Otherwise add to message\n appendText(textValue);\n };\n\n const handleBack = () => {\n if (pageHistory.length > 0) {\n const previousPage = pageHistory[pageHistory.length - 1];\n setPageHistory((prev) => prev.slice(0, -1));\n setCurrentPageId(previousPage.id);\n if (onPageChange) {\n onPageChange(previousPage.id);\n }\n }\n };\n\n const clearMessage = () => {\n setMessage('');\n setCurrentWordCount(0);\n setCurrentEffort(0);\n };\n\n const handleShowPredictions = (\n button: AACButton,\n event: React.MouseEvent<HTMLDivElement>\n ) => {\n event.stopPropagation(); // Prevent button click\n const predictions = button.predictions || (button.parameters as { predictions?: string[] })?.predictions;\n if (predictions && predictions.length > 0) {\n setPredictionsTooltip({\n predictions,\n label: button.label,\n position: { x: event.clientX, y: event.clientY },\n buttonMetricsLookup,\n onWordClick: (word, effort) => {\n const trimmed = word || '';\n if (trimmed) {\n setMessage((prev) => {\n const newMessage = prev + (prev ? ' ' : '') + trimmed;\n updateStats(trimmed, effort);\n return newMessage;\n });\n }\n },\n });\n }\n };\n\n const getTextColor = (backgroundColor?: string) => {\n if (!backgroundColor) return 'text-gray-900 dark:text-gray-100';\n\n // Convert hex to rgb for brightness calculation\n const hex = backgroundColor.replace('#', '');\n if (hex.length === 6) {\n const r = parseInt(hex.substring(0, 2), 16);\n const g = parseInt(hex.substring(2, 4), 16);\n const b = parseInt(hex.substring(4, 6), 16);\n const brightness = (r * 299 + g * 587 + b * 114) / 1000;\n return brightness >= 128 ? 'text-gray-900' : 'text-white';\n }\n\n return 'text-gray-900 dark:text-gray-100';\n };\n\n if (!currentPage) {\n return (\n <div className={`flex items-center justify-center h-96 bg-gray-100 dark:bg-gray-800 rounded-lg ${className}`}>\n <div className=\"text-center\">\n <p className=\"text-gray-600 dark:text-gray-400\">No pages available</p>\n </div>\n </div>\n );\n }\n\n // Get toolbar page if it exists\n const toolbarPage = tree.toolbarId ? tree.pages[tree.toolbarId] : null;\n\n // Get grid dimensions\n const gridRows = currentPage.grid.length;\n const gridCols = gridRows > 0 ? currentPage.grid[0].length : 0;\n\n return (\n <div className={`bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden ${className}`}>\n {/* Message Bar */}\n {showMessageBar && (\n <div className=\"p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50\">\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex-1 min-w-0\">\n {message ? (\n <div className=\"space-y-2\">\n <p className=\"text-lg text-gray-900 dark:text-white break-words\">{message}</p>\n <div className=\"flex gap-4 text-sm\">\n <div className=\"text-gray-600 dark:text-gray-400\">\n {currentWordCount} {currentWordCount === 1 ? 'word' : 'words'}\n </div>\n {buttonMetrics && (\n <>\n <div className=\"text-gray-600 dark:text-gray-400\">\n Effort: <span className=\"font-medium\">{currentEffort.toFixed(2)}</span>\n </div>\n <div className=\"text-gray-600 dark:text-gray-400\">\n Avg:{' '}\n <span className=\"font-medium\">\n {currentWordCount > 0\n ? (currentEffort / currentWordCount).toFixed(2)\n : '0.00'}\n </span>\n </div>\n </>\n )}\n </div>\n </div>\n ) : (\n <p className=\"text-gray-500 dark:text-gray-400 italic\">\n Tap buttons to build a sentence...\n </p>\n )}\n </div>\n {message && (\n <button\n onClick={clearMessage}\n className=\"p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition\"\n aria-label=\"Clear message\"\n >\n <svg\n className=\"h-5 w-5 text-gray-600 dark:text-gray-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M6 18L18 6M6 6l12 12\"\n />\n </svg>\n </button>\n )}\n </div>\n </div>\n )}\n\n {/* Main Content Area */}\n <div className=\"flex\">\n {/* Toolbar Sidebar (if exists) */}\n {toolbarPage && (\n <div className=\"w-16 sm:w-20 border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50\">\n <div className=\"p-2\">\n <p className=\"text-[10px] text-gray-500 dark:text-gray-400 text-center mb-2 font-semibold\">\n TOOLBAR\n </p>\n <div className=\"grid gap-1\">\n {toolbarPage.grid.map((row, _rowIndex) =>\n row.map((button, _colIndex) => {\n if (!button) return null;\n\n const buttonMetric = buttonMetricsLookup[button.id];\n const effort = buttonMetric?.effort || 0;\n\n return (\n <button\n key={button.id}\n onClick={() => handleButtonClick(button)}\n className=\"aspect-square p-1 rounded border border-gray-200 dark:border-gray-700 transition flex flex-col items-center justify-center gap-0.5 hover:bg-gray-200 dark:hover:bg-gray-700 relative\"\n style={{\n backgroundColor: button.style?.backgroundColor || '#f3f4f6',\n borderColor: button.style?.borderColor || '#e5e7eb',\n }}\n title={`${button.label}\\n${button.message || ''}`}\n >\n {buttonMetric && showEffortBadges && effort > 0 && (\n <div className=\"absolute top-0 right-0 px-0.5 py-0 text-[8px] font-semibold rounded bg-blue-600 text-white\">\n {effort.toFixed(1)}\n </div>\n )}\n <span\n className={`text-[8px] sm:text-[9px] text-center font-medium leading-tight line-clamp-2 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.label}\n </span>\n </button>\n );\n })\n )}\n </div>\n </div>\n </div>\n )}\n\n {/* Main Page Content */}\n <div className=\"flex-1\">\n {/* Header */}\n <div className=\"flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700\">\n <div className=\"flex items-center gap-2\">\n {pageHistory.length > 0 && (\n <button\n onClick={handleBack}\n className=\"p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition\"\n aria-label=\"Go back\"\n >\n <svg\n className=\"h-5 w-5 text-gray-600 dark:text-gray-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 19l-7-7 7-7\"\n />\n </svg>\n </button>\n )}\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white\">\n {currentPage.name}\n </h3>\n </div>\n <div className=\"text-sm text-gray-500 dark:text-gray-400\">\n {gridRows}×{gridCols} grid\n </div>\n </div>\n\n {/* Grid */}\n <div\n className=\"p-4 gap-2 overflow-auto max-h-[600px]\"\n style={{\n display: 'grid',\n gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`,\n }}\n >\n {(() => {\n const rendered = new Set<string>();\n return currentPage.grid.flatMap((row, rowIndex) =>\n row.map((button, colIndex) => {\n if (!button) {\n return <div key={`empty-${rowIndex}-${colIndex}`} className=\"aspect-square\" />;\n }\n\n if (rendered.has(button.id)) {\n return null;\n }\n rendered.add(button.id);\n\n const buttonMetric = buttonMetricsLookup[button.id];\n const effort = buttonMetric?.effort || 0;\n const targetPageId = button.targetPageId || button.semanticAction?.targetId;\n const hasLink = targetPageId && tree.pages[targetPageId];\n const colSpan = button.columnSpan || 1;\n const rowSpan = button.rowSpan || 1;\n const predictions =\n button.predictions || (button.parameters as { predictions?: string[] })?.predictions;\n const hasPredictions = predictions && predictions.length > 0;\n const isPredictionCell =\n button.contentType === 'AutoContent' &&\n (button.contentSubType || '').toLowerCase() === 'prediction';\n const isWorkspace = button.contentType === 'Workspace';\n\n const imageSrc =\n (button.resolvedImageEntry && !String(button.resolvedImageEntry).startsWith('[')\n ? button.resolvedImageEntry\n : null) ||\n (button.image && !String(button.image).startsWith('[') ? button.image : null);\n\n // 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 className=\"relative aspect-square p-2 rounded-lg border-2 transition flex flex-col items-center justify-center gap-1 hover:opacity-80 hover:scale-105 active:scale-95\"\n style={{\n backgroundColor: button.style?.backgroundColor || '#f3f4f6',\n borderColor: button.style?.borderColor || '#e5e7eb',\n color: button.style?.fontColor || undefined,\n gridColumn: `${colIndex + 1} / span ${colSpan}`,\n gridRow: `${rowIndex + 1} / span ${rowSpan}`,\n }}\n >\n {/* Effort Badge */}\n {buttonMetric && showEffortBadges && (\n <div className=\"absolute top-1 right-1 px-1.5 py-0.5 text-xs font-semibold rounded bg-blue-600 text-white shadow-sm\">\n {effort.toFixed(1)}\n </div>\n )}\n\n {/* Link Indicator */}\n {hasLink && showLinkIndicators && (\n <div className=\"absolute top-1 left-1 w-2 h-2 bg-green-500 rounded-full shadow-sm\" />\n )}\n\n {/* Predictions Indicator */}\n {hasPredictions && (\n <div\n onClick={(e) => handleShowPredictions(button, e)}\n className=\"absolute bottom-1 right-1 px-1.5 py-0.5 text-xs font-semibold rounded bg-purple-600 text-white shadow-sm cursor-pointer hover:bg-purple-700 transition\"\n title={`Has ${predictions?.length} word form${predictions && predictions.length > 1 ? 's' : ''}`}\n >\n {predictions?.length}\n </div>\n )}\n\n {/* Image */}\n {(imageSrc || 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 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.label}\n </span>\n {isPredictionCell && predictions && predictions.length > 0 && (\n <div className=\"mt-1 text-[10px] sm:text-xs text-center opacity-80 space-y-0.5\">\n {predictions.slice(0, 3).map((p, idx) => (\n <div key={`${button.id}-pred-${idx}`}>\n {p}\n </div>\n ))}\n {predictions.length > 3 && <div>…</div>}\n </div>\n )}\n </div>\n\n {/* Message (if different from label) */}\n {button.message && button.message !== button.label && !isPredictionCell && (\n <span\n className={`text-[10px] sm:text-xs text-center opacity-75 line-clamp-2 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.message}\n </span>\n )}\n </button>\n );\n })\n );\n })()}\n </div>\n\n {/* Page Navigation (if multiple pages) */}\n {Object.keys(tree.pages).length > 1 && (\n <div className=\"p-4 border-t border-gray-200 dark:border-gray-700\">\n <p className=\"text-sm text-gray-600 dark:text-gray-400 mb-2\">\n {Object.keys(tree.pages).length} pages in this vocabulary\n </p>\n </div>\n )}\n </div>\n </div>\n\n {/* Predictions Tooltip */}\n {predictionsTooltip && (\n <PredictionsTooltip\n predictions={predictionsTooltip.predictions}\n label={predictionsTooltip.label}\n position={predictionsTooltip.position}\n buttonMetricsLookup={predictionsTooltip.buttonMetricsLookup}\n onWordClick={predictionsTooltip.onWordClick}\n onClose={() => setPredictionsTooltip(null)}\n />\n )}\n </div>\n );\n}\n","/**\n * React hooks for AAC Board Viewer\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { loadAACFile, loadAACFileFromURL, calculateMetrics } from '../utils/loaders';\nimport type { AACTree } from '@willwade/aac-processors/browser';\nimport type { ButtonMetric, MetricsOptions } from '../types';\n\n/**\n * Hook to load an AAC file from a URL\n *\n * @param url - URL to the AAC file\n * @param options - Processor options (e.g., pageLayoutPreference for SNAP)\n * @returns Object with tree, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer() {\n * const { tree, loading, error, reload } = useAACFile('/files/board.sps');\n *\n * if (loading) return <div>Loading...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n *\n * return <BoardViewer tree={tree} />;\n * }\n * ```\n */\nexport function useAACFile(\n url: string,\n options?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (options?.enabled === false) {\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFileFromURL(url, options?.processorOptions);\n setTree(loadedTree);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [url, options]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to load an AAC file from a File object (Browser only)\n *\n * @param file - File object from file input\n * @param options - Processor options\n * @returns Object with tree, loading state, and error\n *\n * @example\n * ```tsx\n * function MyUploader() {\n * const [file, setFile] = useState<File | null>(null);\n * const { tree, loading, error } = useAACFileFromFile(file);\n *\n * return (\n * <div>\n * <input type=\"file\" onChange={(e) => setFile(e.target.files?.[0])} />\n * {loading && <div>Loading...</div>}\n * {tree && <BoardViewer tree={tree} />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useAACFileFromFile(\n file: File | Blob | null,\n options?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (!file || options?.enabled === false) {\n setTree(null);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFile(file, options?.processorOptions);\n setTree(loadedTree);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [file, options]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to load an AAC file and calculate metrics\n *\n * @param url - URL to the AAC file\n * @param metricsOptions - Metrics calculation options\n * @returns Object with tree, metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer() {\n * const { tree, metrics, loading, error } = useAACFileWithMetrics(\n * '/files/board.sps',\n * { accessMethod: 'direct' }\n * );\n *\n * if (loading) return <div>Loading...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n *\n * return <BoardViewer tree={tree} buttonMetrics={metrics} />;\n * }\n * ```\n */\nexport function useAACFileWithMetrics(\n url: string,\n metricsOptions?: MetricsOptions,\n fileOptions?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [metrics, setMetrics] = useState<ButtonMetric[] | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (fileOptions?.enabled === false) {\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFileFromURL(url, fileOptions?.processorOptions);\n setTree(loadedTree);\n\n // Calculate metrics\n const calculatedMetrics = await calculateMetrics(loadedTree, metricsOptions || {});\n setMetrics(calculatedMetrics);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [url, metricsOptions, fileOptions]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n metrics,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to load an AAC file from a File object and calculate metrics\n *\n * @param file - File object from file input\n * @param metricsOptions - Metrics calculation options\n * @returns Object with tree, metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyUploader() {\n * const [file, setFile] = useState<File | null>(null);\n * const { tree, metrics, loading, error } = useAACFileFromFileWithMetrics(\n * file,\n * { accessMethod: 'direct' }\n * );\n *\n * return (\n * <div>\n * <input type=\"file\" onChange={(e) => setFile(e.target.files?.[0])} />\n * {loading && <div>Loading...</div>}\n * {tree && <BoardViewer tree={tree} buttonMetrics={metrics} />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useAACFileFromFileWithMetrics(\n file: File | Blob | null,\n metricsOptions?: MetricsOptions,\n fileOptions?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [metrics, setMetrics] = useState<ButtonMetric[] | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (!file || fileOptions?.enabled === false) {\n setTree(null);\n setMetrics(null);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFile(file, fileOptions?.processorOptions);\n setTree(loadedTree);\n\n // Calculate metrics\n const calculatedMetrics = await calculateMetrics(loadedTree, metricsOptions || {});\n setMetrics(calculatedMetrics);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [file, metricsOptions, fileOptions]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n metrics,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to calculate metrics for a tree\n *\n * @param tree - The AAC tree\n * @param options - Metrics calculation options\n * @returns Object with metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer({ tree }) {\n * const { metrics, loading } = useMetrics(tree, {\n * accessMethod: 'scanning',\n * scanningConfig: { pattern: 'row-column' }\n * });\n *\n * return <BoardViewer tree={tree} buttonMetrics={metrics} />;\n * }\n * ```\n */\nexport function useMetrics(\n tree: AACTree | null,\n options?: MetricsOptions\n) {\n const [metrics, setMetrics] = useState<ButtonMetric[] | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const calculate = useCallback(async () => {\n if (!tree) {\n setMetrics(null);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const calculatedMetrics = await calculateMetrics(tree, options || {});\n setMetrics(calculatedMetrics);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to calculate metrics'));\n } finally {\n setLoading(false);\n }\n }, [tree, options]);\n\n useEffect(() => {\n calculate();\n }, [calculate]);\n\n return {\n metrics,\n loading,\n error,\n recalculate: calculate,\n };\n}\n\n/**\n * Hook for sentence building state\n *\n * @returns Object with message state and handlers\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { message, wordCount, effort, addWord, clear } = useSentenceBuilder();\n *\n * return (\n * <div>\n * <p>{message || 'Start building...'}</p>\n * <p>{wordCount} words, {effort.toFixed(2)} effort</p>\n * <button onClick={clear}>Clear</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useSentenceBuilder() {\n const [message, setMessage] = useState('');\n const [wordCount, setWordCount] = useState(0);\n const [effort, setEffort] = useState(0);\n\n const addWord = useCallback((word: string, wordEffort: number = 1) => {\n setMessage((prev) => {\n const newMessage = prev + (prev ? ' ' : '') + word;\n setWordCount((prev) => prev + 1);\n setEffort((prev) => prev + wordEffort);\n return newMessage;\n });\n }, []);\n\n const clear = useCallback(() => {\n setMessage('');\n setWordCount(0);\n setEffort(0);\n }, []);\n\n return {\n message,\n wordCount,\n effort,\n addWord,\n clear,\n };\n}\n","/**\n * AAC File Loading Utilities\n *\n * Provides utilities for loading AAC files from various sources\n * (File/Blob objects, file paths, URLs) in both browser and server contexts.\n *\n * Browser: Supports .obf, .obz, .gridset, .plist, .grd, .opml, .dot\n * Node.js: Supports all formats including .sps, .spb, .ce (Node-only processors)\n */\n\nimport type { AACTree } from '@willwade/aac-processors/browser';\nimport type { LoadAACFileResult } from '../types';\n\ntype ProcessorOptions = Record<string, unknown> | undefined;\n\ntype ProcessorModule = typeof import('@willwade/aac-processors/browser');\n\n// Node-only file extensions that require server-side processing\nconst NODE_ONLY_EXTENSIONS = ['.sps', '.spb', '.ce'];\n\n// Browser-compatible file extensions\nconst BROWSER_EXTENSIONS = ['.obf', '.obz', '.gridset', '.plist', '.grd', '.opml', '.dot'];\n\n/**\n * Detect if running in browser environment\n */\nfunction isBrowserEnvironment(): boolean {\n return typeof window !== 'undefined' && typeof window.document !== 'undefined';\n}\n\n/**\n * Lazily load processors so browser bundles avoid pulling in Node APIs\n */\nasync function importProcessors(): Promise<ProcessorModule> {\n return import('@willwade/aac-processors/browser');\n}\n\n/**\n * Get the appropriate processor for a file based on its extension\n */\nasync function getProcessorForFile(\n filepath: string,\n options?: ProcessorOptions\n): Promise<{ processor: any; extension: string }> {\n const { getProcessor } = await importProcessors();\n\n const ext = filepath.toLowerCase().split('.').pop();\n if (!ext) {\n throw new Error('Invalid file path: no extension found');\n }\n\n const extension = '.' + ext;\n\n // Check if this is a Node-only processor in browser environment\n if (isBrowserEnvironment() && NODE_ONLY_EXTENSIONS.includes(extension)) {\n throw new Error(\n `File type ${extension} requires server-side processing. ` +\n `Please use the server API or upload a browser-compatible format. ` +\n `Browser supports: ${BROWSER_EXTENSIONS.join(', ')}`\n );\n }\n\n // Use the factory function from v0.1.0\n const processor = getProcessor(extension);\n\n if (!processor) {\n throw new Error(`Unsupported file type: ${extension}`);\n }\n\n // For SNAP processor, it needs special options and is Node-only\n if (extension === '.sps' || extension === '.spb') {\n if (isBrowserEnvironment()) {\n throw new Error(\n `SNAP files (.sps, .spb) require server-side processing. ` +\n `Please use the server API or upload a browser-compatible format. ` +\n `Browser supports: ${BROWSER_EXTENSIONS.join(', ')}`\n );\n }\n // In Node.js, import from the main package for full processor support\n const nodeProcessors = await import('@willwade/aac-processors');\n const { SnapProcessor } = nodeProcessors;\n return {\n processor: options ? new SnapProcessor(null, options) : new SnapProcessor(),\n extension,\n };\n }\n\n return { processor, extension };\n}\n\n/**\n * Load an AAC file from a file path (Node.js only)\n *\n * @param filepath - Path to the AAC file\n * @param options - Processor options (e.g., pageLayoutPreference for SNAP)\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const tree = await loadAACFile('/path/to/file.sps');\n * ```\n */\nexport async function loadAACFile(\n filepath: string,\n options?: ProcessorOptions\n): Promise<AACTree>;\n\n/**\n * Load an AAC file from a File or Blob object (Browser only)\n *\n * @param file - File or Blob object\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const input = document.querySelector('input[type=\"file\"]');\n * const file = input.files[0];\n * const tree = await loadAACFile(file);\n * ```\n */\nexport async function loadAACFile(\n file: File | Blob,\n options?: ProcessorOptions\n): Promise<AACTree>;\n\n/**\n * Load an AAC file from an ArrayBuffer (Browser only)\n *\n * @param buffer - ArrayBuffer containing file data\n * @param filename - Filename with extension to detect processor\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const arrayBuffer = await file.arrayBuffer();\n * const tree = await loadAACFile(arrayBuffer, 'board.obf');\n * ```\n */\nexport async function loadAACFile(\n buffer: ArrayBuffer,\n filename: string,\n options?: ProcessorOptions\n): Promise<AACTree>;\n\n/**\n * Unified AAC file loading function that works in both browser and Node.js\n */\nexport async function loadAACFile(\n input: string | File | Blob | ArrayBuffer,\n optionsOrFilename?: ProcessorOptions | string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n // Handle ArrayBuffer case (requires filename as second parameter)\n if (input instanceof ArrayBuffer) {\n const filename = typeof optionsOrFilename === 'string' ? optionsOrFilename : 'unknown.bin';\n return loadFromArrayBuffer(input, filename, options);\n }\n\n // Handle File/Blob case (browser)\n if (input instanceof File || input instanceof Blob) {\n return loadAACFileFromFile(input, undefined, optionsOrFilename as ProcessorOptions);\n }\n\n // Handle string path case (Node.js or URL)\n if (typeof input === 'string') {\n // Check if it's a URL\n if (input.startsWith('http://') || input.startsWith('https://')) {\n return loadAACFileFromURL(input, optionsOrFilename as ProcessorOptions);\n }\n // It's a file path (Node.js only)\n return loadFromFilePath(input, optionsOrFilename as ProcessorOptions);\n }\n\n throw new Error('Invalid input type. Expected File, Blob, ArrayBuffer, or file path string.');\n}\n\n/**\n * Load from file path (Node.js only)\n */\nasync function loadFromFilePath(\n filepath: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const { processor } = await getProcessorForFile(filepath, options);\n return processor.loadIntoTree(filepath);\n}\n\n/**\n * Load from File or Blob (Browser only)\n */\nasync function loadAACFileFromFile(\n file: File | Blob,\n filename?: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n // Extract filename from File object if not provided\n const actualFilename = filename || (file instanceof File ? file.name : 'unknown.bin');\n\n // Check if this is a Node-only format\n const ext = '.' + actualFilename.toLowerCase().split('.').pop();\n if (NODE_ONLY_EXTENSIONS.includes(ext)) {\n throw new Error(\n `File type ${ext} requires server-side processing. ` +\n `Please use the server API or upload a browser-compatible format. ` +\n `Browser supports: ${BROWSER_EXTENSIONS.join(', ')}`\n );\n }\n\n // Read file as ArrayBuffer\n const arrayBuffer = await file.arrayBuffer();\n\n // Load using ArrayBuffer\n return loadFromArrayBuffer(arrayBuffer, actualFilename, options);\n}\n\n/**\n * Load from ArrayBuffer (Browser only)\n */\nasync function loadFromArrayBuffer(\n arrayBuffer: ArrayBuffer,\n filename: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const { processor } = await getProcessorForFile(filename, options);\n\n // AACProcessors v0.1.0+ supports loading from ArrayBuffer in browser\n return processor.loadIntoTree(arrayBuffer);\n}\n\n/**\n * Load an AAC file from a URL (Browser only)\n *\n * Note: This requires the server to provide the file with appropriate CORS headers.\n * For better performance, consider using loadAACFileFromFile() with direct file upload instead.\n *\n * @param url - URL to the AAC file\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const tree = await loadAACFileFromURL('https://example.com/file.obf');\n * ```\n */\nexport async function loadAACFileFromURL(\n url: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`Failed to load file: ${response.statusText}`);\n }\n\n const blob = await response.blob();\n const filename = getFilenameFromURL(url);\n\n return loadAACFileFromFile(blob, filename, options);\n}\n\n/**\n * Load an AAC file and return extended result with format info\n *\n * @param input - File path (Node.js), File/Blob (Browser), or URL string\n * @param options - Processor options\n * @returns Promise resolving to LoadAACFileResult\n */\nexport async function loadAACFileWithMetadata(\n input: string | File | Blob,\n options?: ProcessorOptions\n): Promise<LoadAACFileResult> {\n // Call loadAACFile with type assertion to handle the union type\n const tree = await loadAACFile(input as any, options);\n\n // Detect format from file extension\n let filepath = '';\n if (typeof input === 'string') {\n filepath = input;\n } else if (input instanceof File) {\n filepath = input.name;\n }\n\n const ext = filepath.toLowerCase();\n let format = 'unknown';\n\n if (ext.endsWith('.gridset')) format = 'gridset';\n else if (ext.endsWith('.sps') || ext.endsWith('.spb')) format = 'snap';\n else if (ext.endsWith('.ce')) format = 'touchchat';\n else if (ext.endsWith('.obf')) format = 'openboard';\n else if (ext.endsWith('.obz')) format = 'openboard';\n else if (ext.endsWith('.grd')) format = 'asterics-grid';\n else if (ext.endsWith('.plist')) format = 'apple-panels';\n else if (ext.endsWith('.opml')) format = 'opml';\n else if (ext.endsWith('.xlsx') || ext.endsWith('.xls')) format = 'excel';\n else if (ext.endsWith('.dot')) format = 'dot';\n\n return {\n tree,\n format,\n metadata: tree.metadata,\n };\n}\n\n/**\n * Extract filename from URL\n */\nfunction getFilenameFromURL(url: string): string {\n try {\n const urlObj = new URL(url);\n const pathname = urlObj.pathname;\n const parts = pathname.split('/');\n return parts[parts.length - 1] || 'unknown';\n } catch {\n return 'unknown';\n }\n}\n\n/**\n * Calculate cognitive effort metrics for an AAC tree\n *\n * @param tree - The AAC tree\n * @param options - Metrics calculation options\n * @returns Promise resolving to array of ButtonMetrics\n *\n * @example\n * ```ts\n * import { calculateMetrics } from 'aac-board-viewer';\n *\n * const metrics = await calculateMetrics(tree, {\n * accessMethod: 'direct',\n * });\n * ```\n */\nexport async function calculateMetrics(\n tree: AACTree,\n options: {\n accessMethod?: 'direct' | 'scanning';\n scanningConfig?: {\n pattern?: 'linear' | 'row-column' | 'block';\n selectionMethod?: string;\n errorCorrection?: boolean;\n };\n } = {}\n) {\n // Import MetricsCalculator dynamically to avoid circular dependencies\n const aacProcessors = await import('@willwade/aac-processors');\n\n // Use the calculator if available, otherwise return empty metrics\n if (!aacProcessors.MetricsCalculator) {\n console.warn('MetricsCalculator not available in this environment');\n return [];\n }\n\n const calculator = new aacProcessors.MetricsCalculator();\n\n let metricsOptions: Record<string, unknown> = {};\n\n if (options.accessMethod === 'scanning' && options.scanningConfig) {\n // Use string literals instead of enums to avoid import issues\n let cellScanningOrder = 0; // SimpleScan\n let blockScanEnabled = false;\n let selectionMethod = 0; // AutoScan\n\n switch (options.scanningConfig.pattern) {\n case 'linear':\n cellScanningOrder = 0; // SimpleScan\n break;\n case 'row-column':\n cellScanningOrder = 1; // RowColumnScan\n break;\n case 'block':\n cellScanningOrder = 1; // RowColumnScan\n blockScanEnabled = true;\n break;\n }\n\n metricsOptions = {\n scanningConfig: {\n cellScanningOrder,\n blockScanEnabled,\n selectionMethod,\n errorCorrectionEnabled: options.scanningConfig.errorCorrection || false,\n errorRate: 0.1,\n },\n };\n }\n\n const metricsResult = calculator.analyze(tree, metricsOptions);\n\n // Convert to the format expected by BoardViewer\n type MetricsButton = {\n id: string;\n label: string;\n effort: number;\n count?: number;\n level?: number;\n semantic_id?: string;\n clone_id?: string;\n };\n\n return metricsResult.buttons.map((btn: MetricsButton) => ({\n id: btn.id,\n label: btn.label,\n effort: btn.effort,\n count: btn.count ?? 0,\n is_word: true,\n level: btn.level,\n semantic_id: btn.semantic_id,\n clone_id: btn.clone_id,\n }));\n}\n\n/**\n * Get a list of supported file formats\n *\n * @returns Array of format information\n */\nexport function getSupportedFormats(): Array<{\n name: string;\n extensions: string[];\n description: string;\n browserCompatible: boolean;\n}> {\n return [\n {\n name: 'Grid 3',\n extensions: ['.gridset'],\n description: 'Smartbox Grid 3 communication boards',\n browserCompatible: true,\n },\n {\n name: 'TD Snap',\n extensions: ['.sps', '.spb'],\n description: 'Tobii Dynavox Snap files',\n browserCompatible: false,\n },\n {\n name: 'TouchChat',\n extensions: ['.ce'],\n description: 'Saltillo TouchChat files',\n browserCompatible: false,\n },\n {\n name: 'OpenBoard',\n extensions: ['.obf', '.obz'],\n description: 'OpenBoard Format (OBZ/OBF)',\n browserCompatible: true,\n },\n {\n name: 'Asterics Grid',\n extensions: ['.grd'],\n description: 'Asterics Grid files (.grd)',\n browserCompatible: true,\n },\n {\n name: 'Apple Panels',\n extensions: ['.plist'],\n description: 'Apple iOS Panels files',\n browserCompatible: true,\n },\n {\n name: 'OPML',\n extensions: ['.opml'],\n description: 'OPML outline files',\n browserCompatible: true,\n },\n {\n name: 'Excel',\n extensions: ['.xlsx', '.xls'],\n description: 'Excel spreadsheet boards',\n browserCompatible: false,\n },\n {\n name: 'DOT',\n extensions: ['.dot'],\n description: 'DOT graph visualization files',\n browserCompatible: true,\n },\n ];\n}\n\n/**\n * Check if a file format is compatible with browser processing\n *\n * @param extension - File extension (e.g., '.obf', 'obf')\n * @returns true if browser-compatible, false if server-only\n */\nexport function isBrowserCompatible(extension: string): boolean {\n const ext = extension.startsWith('.') ? extension.toLowerCase() : '.' + extension.toLowerCase();\n return BROWSER_EXTENSIONS.includes(ext);\n}\n\n/**\n * Get list of browser-compatible file extensions\n */\nexport function getBrowserExtensions(): string[] {\n return [...BROWSER_EXTENSIONS];\n}\n\n/**\n * Get list of Node-only file extensions\n */\nexport function getNodeOnlyExtensions(): string[] {\n return [...NODE_ONLY_EXTENSIONS];\n}\n"],"mappings":"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","names":["React","import_react","prev"]}
|
package/dist/index.mjs
CHANGED
|
@@ -532,7 +532,7 @@ function isBrowserEnvironment() {
|
|
|
532
532
|
return typeof window !== "undefined" && typeof window.document !== "undefined";
|
|
533
533
|
}
|
|
534
534
|
async function importProcessors() {
|
|
535
|
-
return import("@willwade/aac-processors");
|
|
535
|
+
return import("@willwade/aac-processors/browser");
|
|
536
536
|
}
|
|
537
537
|
async function getProcessorForFile(filepath, options) {
|
|
538
538
|
const { getProcessor } = await importProcessors();
|
|
@@ -551,7 +551,13 @@ async function getProcessorForFile(filepath, options) {
|
|
|
551
551
|
throw new Error(`Unsupported file type: ${extension}`);
|
|
552
552
|
}
|
|
553
553
|
if (extension === ".sps" || extension === ".spb") {
|
|
554
|
-
|
|
554
|
+
if (isBrowserEnvironment()) {
|
|
555
|
+
throw new Error(
|
|
556
|
+
`SNAP files (.sps, .spb) require server-side processing. Please use the server API or upload a browser-compatible format. Browser supports: ${BROWSER_EXTENSIONS.join(", ")}`
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
const nodeProcessors = await import("@willwade/aac-processors");
|
|
560
|
+
const { SnapProcessor } = nodeProcessors;
|
|
555
561
|
return {
|
|
556
562
|
processor: options ? new SnapProcessor(null, options) : new SnapProcessor(),
|
|
557
563
|
extension
|
|
@@ -937,6 +943,37 @@ function useSentenceBuilder() {
|
|
|
937
943
|
clear
|
|
938
944
|
};
|
|
939
945
|
}
|
|
946
|
+
|
|
947
|
+
// src/index.ts
|
|
948
|
+
if (typeof window !== "undefined" && typeof window.Buffer === "undefined") {
|
|
949
|
+
window.Buffer = {
|
|
950
|
+
from: (data) => {
|
|
951
|
+
if (typeof data === "string") {
|
|
952
|
+
const encoder = new TextEncoder();
|
|
953
|
+
return encoder.encode(data);
|
|
954
|
+
}
|
|
955
|
+
if (data instanceof ArrayBuffer) {
|
|
956
|
+
return new Uint8Array(data);
|
|
957
|
+
}
|
|
958
|
+
if (data instanceof Uint8Array) {
|
|
959
|
+
return data;
|
|
960
|
+
}
|
|
961
|
+
return new Uint8Array(data);
|
|
962
|
+
},
|
|
963
|
+
alloc: (size) => new Uint8Array(size),
|
|
964
|
+
allocUnsafe: (size) => new Uint8Array(size),
|
|
965
|
+
concat: (list, totalLength) => {
|
|
966
|
+
const result = new Uint8Array(totalLength || list.reduce((sum, arr) => sum + arr.length, 0));
|
|
967
|
+
let offset = 0;
|
|
968
|
+
for (const arr of list) {
|
|
969
|
+
result.set(arr, offset);
|
|
970
|
+
offset += arr.length;
|
|
971
|
+
}
|
|
972
|
+
return result;
|
|
973
|
+
},
|
|
974
|
+
isBuffer: (obj) => obj instanceof Uint8Array
|
|
975
|
+
};
|
|
976
|
+
}
|
|
940
977
|
export {
|
|
941
978
|
BoardViewer,
|
|
942
979
|
calculateMetrics,
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/BoardViewer.tsx","../src/hooks/useAACFile.ts","../src/utils/loaders.ts"],"sourcesContent":["import React, { useState, useMemo, useCallback } from 'react';\nimport type {\n AACPage,\n AACButton,\n} from '@willwade/aac-processors';\nimport type { BoardViewerProps, ButtonMetric } from '../types';\n\n/**\n * Predictions Tooltip Component\n *\n * Shows a tooltip with predicted word forms when clicking the predictions indicator\n */\ninterface PredictionsTooltipProps {\n predictions: string[];\n label: string;\n position: { x: number; y: number };\n buttonMetricsLookup: { [buttonId: string]: ButtonMetric };\n onClose: () => void;\n 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 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 [pageHistory, setPageHistory] = useState<AACPage[]>([]);\n const [message, setMessage] = useState('');\n const [currentWordCount, setCurrentWordCount] = useState(0);\n const [currentEffort, setCurrentEffort] = useState(0);\n\n // Predictions tooltip state\n const [predictionsTooltip, setPredictionsTooltip] = useState<{\n predictions: string[];\n label: string;\n position: { x: number; y: number };\n buttonMetricsLookup: { [buttonId: string]: ButtonMetric };\n onWordClick?: (word: string, effort: number) => void;\n } | null>(null);\n\n // Convert button metrics array to lookup object for easy access\n const buttonMetricsLookup = useMemo(() => {\n if (!buttonMetrics) return {};\n const lookup: { [buttonId: string]: ButtonMetric } = {};\n buttonMetrics.forEach((metric) => {\n lookup[metric.id] = metric;\n });\n return lookup;\n }, [buttonMetrics]);\n\n const currentPage = currentPageId ? tree.pages[currentPageId] : null;\n const goToPage = (targetPageId: string | undefined | null) => {\n if (!targetPageId || !tree.pages[targetPageId]) return false;\n if (currentPage) {\n setPageHistory((prev) => [...prev, currentPage]);\n }\n setCurrentPageId(targetPageId);\n if (onPageChange) {\n onPageChange(targetPageId);\n }\n return true;\n };\n\n // Sync when tree or initialPageId changes\n React.useEffect(() => {\n setCurrentPageId(resolveInitialPageId());\n setPageHistory([]);\n }, [resolveInitialPageId]);\n\n // Calculate total stats for current word\n const updateStats = (word: string, effort: number) => {\n setCurrentWordCount((prev) => prev + 1);\n setCurrentEffort((prev) => prev + effort);\n };\n\n const handleButtonClick = (button: AACButton) => {\n // Call external callback if provided\n if (onButtonClick) {\n onButtonClick(button);\n }\n\n const intent = button.semanticAction?.intent\n ? String(button.semanticAction.intent)\n : undefined;\n const targetPageId = button.targetPageId || button.semanticAction?.targetId;\n const effort = buttonMetricsLookup[button.id]?.effort || 1;\n const textValue =\n button.semanticAction?.text || button.message || button.label || '';\n\n const deleteLastWord = () => {\n setMessage((prev) => {\n const parts = prev.trim().split(/\\s+/);\n parts.pop();\n const newMsg = parts.join(' ');\n return newMsg;\n });\n };\n\n const deleteLastCharacter = () => {\n setMessage((prev) => prev.slice(0, -1));\n };\n\n const appendText = (word: string) => {\n const trimmed = word || button.label || '';\n setMessage((prev) => {\n const newMessage = trimmed\n ? prev + (prev ? ' ' : '') + trimmed\n : prev;\n if (trimmed) {\n updateStats(trimmed, effort);\n }\n return newMessage;\n });\n };\n\n // Navigation takes precedence\n if (intent === 'NAVIGATE_TO' && goToPage(targetPageId)) {\n return;\n }\n\n switch (intent) {\n case 'GO_BACK':\n handleBack();\n return;\n case 'GO_HOME':\n if (tree.rootId && goToPage(tree.rootId)) return;\n break;\n case 'DELETE_WORD':\n deleteLastWord();\n return;\n case 'DELETE_CHARACTER':\n deleteLastCharacter();\n return;\n case 'CLEAR_TEXT':\n clearMessage();\n return;\n case 'SPEAK_IMMEDIATE':\n case 'SPEAK_TEXT':\n case 'INSERT_TEXT':\n appendText(textValue);\n return;\n default:\n break;\n }\n\n // Fallback navigation if intent not set but target exists\n if (targetPageId && goToPage(targetPageId)) {\n return;\n }\n\n // Otherwise add to message\n appendText(textValue);\n };\n\n const handleBack = () => {\n if (pageHistory.length > 0) {\n const previousPage = pageHistory[pageHistory.length - 1];\n setPageHistory((prev) => prev.slice(0, -1));\n setCurrentPageId(previousPage.id);\n if (onPageChange) {\n onPageChange(previousPage.id);\n }\n }\n };\n\n const clearMessage = () => {\n setMessage('');\n setCurrentWordCount(0);\n setCurrentEffort(0);\n };\n\n const handleShowPredictions = (\n button: AACButton,\n event: React.MouseEvent<HTMLDivElement>\n ) => {\n event.stopPropagation(); // Prevent button click\n const predictions = button.predictions || (button.parameters as { predictions?: string[] })?.predictions;\n if (predictions && predictions.length > 0) {\n setPredictionsTooltip({\n predictions,\n label: button.label,\n position: { x: event.clientX, y: event.clientY },\n buttonMetricsLookup,\n onWordClick: (word, effort) => {\n const trimmed = word || '';\n if (trimmed) {\n setMessage((prev) => {\n const newMessage = prev + (prev ? ' ' : '') + trimmed;\n updateStats(trimmed, effort);\n return newMessage;\n });\n }\n },\n });\n }\n };\n\n const getTextColor = (backgroundColor?: string) => {\n if (!backgroundColor) return 'text-gray-900 dark:text-gray-100';\n\n // Convert hex to rgb for brightness calculation\n const hex = backgroundColor.replace('#', '');\n if (hex.length === 6) {\n const r = parseInt(hex.substring(0, 2), 16);\n const g = parseInt(hex.substring(2, 4), 16);\n const b = parseInt(hex.substring(4, 6), 16);\n const brightness = (r * 299 + g * 587 + b * 114) / 1000;\n return brightness >= 128 ? 'text-gray-900' : 'text-white';\n }\n\n return 'text-gray-900 dark:text-gray-100';\n };\n\n if (!currentPage) {\n return (\n <div className={`flex items-center justify-center h-96 bg-gray-100 dark:bg-gray-800 rounded-lg ${className}`}>\n <div className=\"text-center\">\n <p className=\"text-gray-600 dark:text-gray-400\">No pages available</p>\n </div>\n </div>\n );\n }\n\n // Get toolbar page if it exists\n const toolbarPage = tree.toolbarId ? tree.pages[tree.toolbarId] : null;\n\n // Get grid dimensions\n const gridRows = currentPage.grid.length;\n const gridCols = gridRows > 0 ? currentPage.grid[0].length : 0;\n\n return (\n <div className={`bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden ${className}`}>\n {/* Message Bar */}\n {showMessageBar && (\n <div className=\"p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50\">\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex-1 min-w-0\">\n {message ? (\n <div className=\"space-y-2\">\n <p className=\"text-lg text-gray-900 dark:text-white break-words\">{message}</p>\n <div className=\"flex gap-4 text-sm\">\n <div className=\"text-gray-600 dark:text-gray-400\">\n {currentWordCount} {currentWordCount === 1 ? 'word' : 'words'}\n </div>\n {buttonMetrics && (\n <>\n <div className=\"text-gray-600 dark:text-gray-400\">\n Effort: <span className=\"font-medium\">{currentEffort.toFixed(2)}</span>\n </div>\n <div className=\"text-gray-600 dark:text-gray-400\">\n Avg:{' '}\n <span className=\"font-medium\">\n {currentWordCount > 0\n ? (currentEffort / currentWordCount).toFixed(2)\n : '0.00'}\n </span>\n </div>\n </>\n )}\n </div>\n </div>\n ) : (\n <p className=\"text-gray-500 dark:text-gray-400 italic\">\n Tap buttons to build a sentence...\n </p>\n )}\n </div>\n {message && (\n <button\n onClick={clearMessage}\n className=\"p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition\"\n aria-label=\"Clear message\"\n >\n <svg\n className=\"h-5 w-5 text-gray-600 dark:text-gray-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M6 18L18 6M6 6l12 12\"\n />\n </svg>\n </button>\n )}\n </div>\n </div>\n )}\n\n {/* Main Content Area */}\n <div className=\"flex\">\n {/* Toolbar Sidebar (if exists) */}\n {toolbarPage && (\n <div className=\"w-16 sm:w-20 border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50\">\n <div className=\"p-2\">\n <p className=\"text-[10px] text-gray-500 dark:text-gray-400 text-center mb-2 font-semibold\">\n TOOLBAR\n </p>\n <div className=\"grid gap-1\">\n {toolbarPage.grid.map((row, _rowIndex) =>\n row.map((button, _colIndex) => {\n if (!button) return null;\n\n const buttonMetric = buttonMetricsLookup[button.id];\n const effort = buttonMetric?.effort || 0;\n\n return (\n <button\n key={button.id}\n onClick={() => handleButtonClick(button)}\n className=\"aspect-square p-1 rounded border border-gray-200 dark:border-gray-700 transition flex flex-col items-center justify-center gap-0.5 hover:bg-gray-200 dark:hover:bg-gray-700 relative\"\n style={{\n backgroundColor: button.style?.backgroundColor || '#f3f4f6',\n borderColor: button.style?.borderColor || '#e5e7eb',\n }}\n title={`${button.label}\\n${button.message || ''}`}\n >\n {buttonMetric && showEffortBadges && effort > 0 && (\n <div className=\"absolute top-0 right-0 px-0.5 py-0 text-[8px] font-semibold rounded bg-blue-600 text-white\">\n {effort.toFixed(1)}\n </div>\n )}\n <span\n className={`text-[8px] sm:text-[9px] text-center font-medium leading-tight line-clamp-2 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.label}\n </span>\n </button>\n );\n })\n )}\n </div>\n </div>\n </div>\n )}\n\n {/* Main Page Content */}\n <div className=\"flex-1\">\n {/* Header */}\n <div className=\"flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700\">\n <div className=\"flex items-center gap-2\">\n {pageHistory.length > 0 && (\n <button\n onClick={handleBack}\n className=\"p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition\"\n aria-label=\"Go back\"\n >\n <svg\n className=\"h-5 w-5 text-gray-600 dark:text-gray-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 19l-7-7 7-7\"\n />\n </svg>\n </button>\n )}\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white\">\n {currentPage.name}\n </h3>\n </div>\n <div className=\"text-sm text-gray-500 dark:text-gray-400\">\n {gridRows}×{gridCols} grid\n </div>\n </div>\n\n {/* Grid */}\n <div\n className=\"p-4 gap-2 overflow-auto max-h-[600px]\"\n style={{\n display: 'grid',\n gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`,\n }}\n >\n {(() => {\n const rendered = new Set<string>();\n return currentPage.grid.flatMap((row, rowIndex) =>\n row.map((button, colIndex) => {\n if (!button) {\n return <div key={`empty-${rowIndex}-${colIndex}`} className=\"aspect-square\" />;\n }\n\n if (rendered.has(button.id)) {\n return null;\n }\n rendered.add(button.id);\n\n const buttonMetric = buttonMetricsLookup[button.id];\n const effort = buttonMetric?.effort || 0;\n const targetPageId = button.targetPageId || button.semanticAction?.targetId;\n const hasLink = targetPageId && tree.pages[targetPageId];\n const colSpan = button.columnSpan || 1;\n const rowSpan = button.rowSpan || 1;\n const predictions =\n button.predictions || (button.parameters as { predictions?: string[] })?.predictions;\n const hasPredictions = predictions && predictions.length > 0;\n const isPredictionCell =\n button.contentType === 'AutoContent' &&\n (button.contentSubType || '').toLowerCase() === 'prediction';\n const isWorkspace = button.contentType === 'Workspace';\n\n const imageSrc =\n (button.resolvedImageEntry && !String(button.resolvedImageEntry).startsWith('[')\n ? button.resolvedImageEntry\n : null) ||\n (button.image && !String(button.image).startsWith('[') ? button.image : null);\n\n // 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 className=\"relative aspect-square p-2 rounded-lg border-2 transition flex flex-col items-center justify-center gap-1 hover:opacity-80 hover:scale-105 active:scale-95\"\n style={{\n backgroundColor: button.style?.backgroundColor || '#f3f4f6',\n borderColor: button.style?.borderColor || '#e5e7eb',\n color: button.style?.fontColor || undefined,\n gridColumn: `${colIndex + 1} / span ${colSpan}`,\n gridRow: `${rowIndex + 1} / span ${rowSpan}`,\n }}\n >\n {/* Effort Badge */}\n {buttonMetric && showEffortBadges && (\n <div className=\"absolute top-1 right-1 px-1.5 py-0.5 text-xs font-semibold rounded bg-blue-600 text-white shadow-sm\">\n {effort.toFixed(1)}\n </div>\n )}\n\n {/* Link Indicator */}\n {hasLink && showLinkIndicators && (\n <div className=\"absolute top-1 left-1 w-2 h-2 bg-green-500 rounded-full shadow-sm\" />\n )}\n\n {/* Predictions Indicator */}\n {hasPredictions && (\n <div\n onClick={(e) => handleShowPredictions(button, e)}\n className=\"absolute bottom-1 right-1 px-1.5 py-0.5 text-xs font-semibold rounded bg-purple-600 text-white shadow-sm cursor-pointer hover:bg-purple-700 transition\"\n title={`Has ${predictions?.length} word form${predictions && predictions.length > 1 ? 's' : ''}`}\n >\n {predictions?.length}\n </div>\n )}\n\n {/* Image */}\n {(imageSrc || 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 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.label}\n </span>\n {isPredictionCell && predictions && predictions.length > 0 && (\n <div className=\"mt-1 text-[10px] sm:text-xs text-center opacity-80 space-y-0.5\">\n {predictions.slice(0, 3).map((p, idx) => (\n <div key={`${button.id}-pred-${idx}`}>\n {p}\n </div>\n ))}\n {predictions.length > 3 && <div>…</div>}\n </div>\n )}\n </div>\n\n {/* Message (if different from label) */}\n {button.message && button.message !== button.label && !isPredictionCell && (\n <span\n className={`text-[10px] sm:text-xs text-center opacity-75 line-clamp-2 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.message}\n </span>\n )}\n </button>\n );\n })\n );\n })()}\n </div>\n\n {/* Page Navigation (if multiple pages) */}\n {Object.keys(tree.pages).length > 1 && (\n <div className=\"p-4 border-t border-gray-200 dark:border-gray-700\">\n <p className=\"text-sm text-gray-600 dark:text-gray-400 mb-2\">\n {Object.keys(tree.pages).length} pages in this vocabulary\n </p>\n </div>\n )}\n </div>\n </div>\n\n {/* Predictions Tooltip */}\n {predictionsTooltip && (\n <PredictionsTooltip\n predictions={predictionsTooltip.predictions}\n label={predictionsTooltip.label}\n position={predictionsTooltip.position}\n buttonMetricsLookup={predictionsTooltip.buttonMetricsLookup}\n onWordClick={predictionsTooltip.onWordClick}\n onClose={() => setPredictionsTooltip(null)}\n />\n )}\n </div>\n );\n}\n","/**\n * React hooks for AAC Board Viewer\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { loadAACFile, loadAACFileFromURL, calculateMetrics } from '../utils/loaders';\nimport type { AACTree } from '@willwade/aac-processors';\nimport type { ButtonMetric, MetricsOptions } from '../types';\n\n/**\n * Hook to load an AAC file from a URL\n *\n * @param url - URL to the AAC file\n * @param options - Processor options (e.g., pageLayoutPreference for SNAP)\n * @returns Object with tree, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer() {\n * const { tree, loading, error, reload } = useAACFile('/files/board.sps');\n *\n * if (loading) return <div>Loading...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n *\n * return <BoardViewer tree={tree} />;\n * }\n * ```\n */\nexport function useAACFile(\n url: string,\n options?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (options?.enabled === false) {\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFileFromURL(url, options?.processorOptions);\n setTree(loadedTree);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [url, options]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to load an AAC file from a File object (Browser only)\n *\n * @param file - File object from file input\n * @param options - Processor options\n * @returns Object with tree, loading state, and error\n *\n * @example\n * ```tsx\n * function MyUploader() {\n * const [file, setFile] = useState<File | null>(null);\n * const { tree, loading, error } = useAACFileFromFile(file);\n *\n * return (\n * <div>\n * <input type=\"file\" onChange={(e) => setFile(e.target.files?.[0])} />\n * {loading && <div>Loading...</div>}\n * {tree && <BoardViewer tree={tree} />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useAACFileFromFile(\n file: File | Blob | null,\n options?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (!file || options?.enabled === false) {\n setTree(null);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFile(file, options?.processorOptions);\n setTree(loadedTree);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [file, options]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to load an AAC file and calculate metrics\n *\n * @param url - URL to the AAC file\n * @param metricsOptions - Metrics calculation options\n * @returns Object with tree, metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer() {\n * const { tree, metrics, loading, error } = useAACFileWithMetrics(\n * '/files/board.sps',\n * { accessMethod: 'direct' }\n * );\n *\n * if (loading) return <div>Loading...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n *\n * return <BoardViewer tree={tree} buttonMetrics={metrics} />;\n * }\n * ```\n */\nexport function useAACFileWithMetrics(\n url: string,\n metricsOptions?: MetricsOptions,\n fileOptions?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [metrics, setMetrics] = useState<ButtonMetric[] | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (fileOptions?.enabled === false) {\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFileFromURL(url, fileOptions?.processorOptions);\n setTree(loadedTree);\n\n // Calculate metrics\n const calculatedMetrics = await calculateMetrics(loadedTree, metricsOptions || {});\n setMetrics(calculatedMetrics);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [url, metricsOptions, fileOptions]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n metrics,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to load an AAC file from a File object and calculate metrics\n *\n * @param file - File object from file input\n * @param metricsOptions - Metrics calculation options\n * @returns Object with tree, metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyUploader() {\n * const [file, setFile] = useState<File | null>(null);\n * const { tree, metrics, loading, error } = useAACFileFromFileWithMetrics(\n * file,\n * { accessMethod: 'direct' }\n * );\n *\n * return (\n * <div>\n * <input type=\"file\" onChange={(e) => setFile(e.target.files?.[0])} />\n * {loading && <div>Loading...</div>}\n * {tree && <BoardViewer tree={tree} buttonMetrics={metrics} />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useAACFileFromFileWithMetrics(\n file: File | Blob | null,\n metricsOptions?: MetricsOptions,\n fileOptions?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [metrics, setMetrics] = useState<ButtonMetric[] | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (!file || fileOptions?.enabled === false) {\n setTree(null);\n setMetrics(null);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFile(file, fileOptions?.processorOptions);\n setTree(loadedTree);\n\n // Calculate metrics\n const calculatedMetrics = await calculateMetrics(loadedTree, metricsOptions || {});\n setMetrics(calculatedMetrics);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [file, metricsOptions, fileOptions]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n metrics,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to calculate metrics for a tree\n *\n * @param tree - The AAC tree\n * @param options - Metrics calculation options\n * @returns Object with metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer({ tree }) {\n * const { metrics, loading } = useMetrics(tree, {\n * accessMethod: 'scanning',\n * scanningConfig: { pattern: 'row-column' }\n * });\n *\n * return <BoardViewer tree={tree} buttonMetrics={metrics} />;\n * }\n * ```\n */\nexport function useMetrics(\n tree: AACTree | null,\n options?: MetricsOptions\n) {\n const [metrics, setMetrics] = useState<ButtonMetric[] | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const calculate = useCallback(async () => {\n if (!tree) {\n setMetrics(null);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const calculatedMetrics = await calculateMetrics(tree, options || {});\n setMetrics(calculatedMetrics);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to calculate metrics'));\n } finally {\n setLoading(false);\n }\n }, [tree, options]);\n\n useEffect(() => {\n calculate();\n }, [calculate]);\n\n return {\n metrics,\n loading,\n error,\n recalculate: calculate,\n };\n}\n\n/**\n * Hook for sentence building state\n *\n * @returns Object with message state and handlers\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { message, wordCount, effort, addWord, clear } = useSentenceBuilder();\n *\n * return (\n * <div>\n * <p>{message || 'Start building...'}</p>\n * <p>{wordCount} words, {effort.toFixed(2)} effort</p>\n * <button onClick={clear}>Clear</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useSentenceBuilder() {\n const [message, setMessage] = useState('');\n const [wordCount, setWordCount] = useState(0);\n const [effort, setEffort] = useState(0);\n\n const addWord = useCallback((word: string, wordEffort: number = 1) => {\n setMessage((prev) => {\n const newMessage = prev + (prev ? ' ' : '') + word;\n setWordCount((prev) => prev + 1);\n setEffort((prev) => prev + wordEffort);\n return newMessage;\n });\n }, []);\n\n const clear = useCallback(() => {\n setMessage('');\n setWordCount(0);\n setEffort(0);\n }, []);\n\n return {\n message,\n wordCount,\n effort,\n addWord,\n clear,\n };\n}\n","/**\n * AAC File Loading Utilities\n *\n * Provides utilities for loading AAC files from various sources\n * (File/Blob objects, file paths, URLs) in both browser and server contexts.\n *\n * Browser: Supports .obf, .obz, .gridset, .plist, .grd, .opml, .dot\n * Node.js: Supports all formats including .sps, .spb, .ce (Node-only processors)\n */\n\nimport type { AACTree } from '@willwade/aac-processors';\nimport type { LoadAACFileResult } from '../types';\n\ntype ProcessorOptions = Record<string, unknown> | undefined;\n\ntype ProcessorModule = typeof import('@willwade/aac-processors');\n\n// Node-only file extensions that require server-side processing\nconst NODE_ONLY_EXTENSIONS = ['.sps', '.spb', '.ce'];\n\n// Browser-compatible file extensions\nconst BROWSER_EXTENSIONS = ['.obf', '.obz', '.gridset', '.plist', '.grd', '.opml', '.dot'];\n\n/**\n * Detect if running in browser environment\n */\nfunction isBrowserEnvironment(): boolean {\n return typeof window !== 'undefined' && typeof window.document !== 'undefined';\n}\n\n/**\n * Lazily load processors so browser bundles avoid pulling in Node APIs\n */\nasync function importProcessors(): Promise<ProcessorModule> {\n return import('@willwade/aac-processors');\n}\n\n/**\n * Get the appropriate processor for a file based on its extension\n */\nasync function getProcessorForFile(\n filepath: string,\n options?: ProcessorOptions\n): Promise<{ processor: any; extension: string }> {\n const { getProcessor } = await importProcessors();\n\n const ext = filepath.toLowerCase().split('.').pop();\n if (!ext) {\n throw new Error('Invalid file path: no extension found');\n }\n\n const extension = '.' + ext;\n\n // Check if this is a Node-only processor in browser environment\n if (isBrowserEnvironment() && NODE_ONLY_EXTENSIONS.includes(extension)) {\n throw new Error(\n `File type ${extension} requires server-side processing. ` +\n `Please use the server API or upload a browser-compatible format. ` +\n `Browser supports: ${BROWSER_EXTENSIONS.join(', ')}`\n );\n }\n\n // Use the factory function from v0.1.0\n const processor = getProcessor(extension);\n\n if (!processor) {\n throw new Error(`Unsupported file type: ${extension}`);\n }\n\n // For SNAP processor, it needs special options\n if (extension === '.sps' || extension === '.spb') {\n const { SnapProcessor } = await importProcessors();\n return {\n processor: options ? new SnapProcessor(null, options) : new SnapProcessor(),\n extension,\n };\n }\n\n return { processor, extension };\n}\n\n/**\n * Load an AAC file from a file path (Node.js only)\n *\n * @param filepath - Path to the AAC file\n * @param options - Processor options (e.g., pageLayoutPreference for SNAP)\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const tree = await loadAACFile('/path/to/file.sps');\n * ```\n */\nexport async function loadAACFile(\n filepath: string,\n options?: ProcessorOptions\n): Promise<AACTree>;\n\n/**\n * Load an AAC file from a File or Blob object (Browser only)\n *\n * @param file - File or Blob object\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const input = document.querySelector('input[type=\"file\"]');\n * const file = input.files[0];\n * const tree = await loadAACFile(file);\n * ```\n */\nexport async function loadAACFile(\n file: File | Blob,\n options?: ProcessorOptions\n): Promise<AACTree>;\n\n/**\n * Load an AAC file from an ArrayBuffer (Browser only)\n *\n * @param buffer - ArrayBuffer containing file data\n * @param filename - Filename with extension to detect processor\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const arrayBuffer = await file.arrayBuffer();\n * const tree = await loadAACFile(arrayBuffer, 'board.obf');\n * ```\n */\nexport async function loadAACFile(\n buffer: ArrayBuffer,\n filename: string,\n options?: ProcessorOptions\n): Promise<AACTree>;\n\n/**\n * Unified AAC file loading function that works in both browser and Node.js\n */\nexport async function loadAACFile(\n input: string | File | Blob | ArrayBuffer,\n optionsOrFilename?: ProcessorOptions | string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n // Handle ArrayBuffer case (requires filename as second parameter)\n if (input instanceof ArrayBuffer) {\n const filename = typeof optionsOrFilename === 'string' ? optionsOrFilename : 'unknown.bin';\n return loadFromArrayBuffer(input, filename, options);\n }\n\n // Handle File/Blob case (browser)\n if (input instanceof File || input instanceof Blob) {\n return loadAACFileFromFile(input, undefined, optionsOrFilename as ProcessorOptions);\n }\n\n // Handle string path case (Node.js or URL)\n if (typeof input === 'string') {\n // Check if it's a URL\n if (input.startsWith('http://') || input.startsWith('https://')) {\n return loadAACFileFromURL(input, optionsOrFilename as ProcessorOptions);\n }\n // It's a file path (Node.js only)\n return loadFromFilePath(input, optionsOrFilename as ProcessorOptions);\n }\n\n throw new Error('Invalid input type. Expected File, Blob, ArrayBuffer, or file path string.');\n}\n\n/**\n * Load from file path (Node.js only)\n */\nasync function loadFromFilePath(\n filepath: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const { processor } = await getProcessorForFile(filepath, options);\n return processor.loadIntoTree(filepath);\n}\n\n/**\n * Load from File or Blob (Browser only)\n */\nasync function loadAACFileFromFile(\n file: File | Blob,\n filename?: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n // Extract filename from File object if not provided\n const actualFilename = filename || (file instanceof File ? file.name : 'unknown.bin');\n\n // Check if this is a Node-only format\n const ext = '.' + actualFilename.toLowerCase().split('.').pop();\n if (NODE_ONLY_EXTENSIONS.includes(ext)) {\n throw new Error(\n `File type ${ext} requires server-side processing. ` +\n `Please use the server API or upload a browser-compatible format. ` +\n `Browser supports: ${BROWSER_EXTENSIONS.join(', ')}`\n );\n }\n\n // Read file as ArrayBuffer\n const arrayBuffer = await file.arrayBuffer();\n\n // Load using ArrayBuffer\n return loadFromArrayBuffer(arrayBuffer, actualFilename, options);\n}\n\n/**\n * Load from ArrayBuffer (Browser only)\n */\nasync function loadFromArrayBuffer(\n arrayBuffer: ArrayBuffer,\n filename: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const { processor } = await getProcessorForFile(filename, options);\n\n // AACProcessors v0.1.0+ supports loading from ArrayBuffer in browser\n return processor.loadIntoTree(arrayBuffer);\n}\n\n/**\n * Load an AAC file from a URL (Browser only)\n *\n * Note: This requires the server to provide the file with appropriate CORS headers.\n * For better performance, consider using loadAACFileFromFile() with direct file upload instead.\n *\n * @param url - URL to the AAC file\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const tree = await loadAACFileFromURL('https://example.com/file.obf');\n * ```\n */\nexport async function loadAACFileFromURL(\n url: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`Failed to load file: ${response.statusText}`);\n }\n\n const blob = await response.blob();\n const filename = getFilenameFromURL(url);\n\n return loadAACFileFromFile(blob, filename, options);\n}\n\n/**\n * Load an AAC file and return extended result with format info\n *\n * @param input - File path (Node.js), File/Blob (Browser), or URL string\n * @param options - Processor options\n * @returns Promise resolving to LoadAACFileResult\n */\nexport async function loadAACFileWithMetadata(\n input: string | File | Blob,\n options?: ProcessorOptions\n): Promise<LoadAACFileResult> {\n // Call loadAACFile with type assertion to handle the union type\n const tree = await loadAACFile(input as any, options);\n\n // Detect format from file extension\n let filepath = '';\n if (typeof input === 'string') {\n filepath = input;\n } else if (input instanceof File) {\n filepath = input.name;\n }\n\n const ext = filepath.toLowerCase();\n let format = 'unknown';\n\n if (ext.endsWith('.gridset')) format = 'gridset';\n else if (ext.endsWith('.sps') || ext.endsWith('.spb')) format = 'snap';\n else if (ext.endsWith('.ce')) format = 'touchchat';\n else if (ext.endsWith('.obf')) format = 'openboard';\n else if (ext.endsWith('.obz')) format = 'openboard';\n else if (ext.endsWith('.grd')) format = 'asterics-grid';\n else if (ext.endsWith('.plist')) format = 'apple-panels';\n else if (ext.endsWith('.opml')) format = 'opml';\n else if (ext.endsWith('.xlsx') || ext.endsWith('.xls')) format = 'excel';\n else if (ext.endsWith('.dot')) format = 'dot';\n\n return {\n tree,\n format,\n metadata: tree.metadata,\n };\n}\n\n/**\n * Extract filename from URL\n */\nfunction getFilenameFromURL(url: string): string {\n try {\n const urlObj = new URL(url);\n const pathname = urlObj.pathname;\n const parts = pathname.split('/');\n return parts[parts.length - 1] || 'unknown';\n } catch {\n return 'unknown';\n }\n}\n\n/**\n * Calculate cognitive effort metrics for an AAC tree\n *\n * @param tree - The AAC tree\n * @param options - Metrics calculation options\n * @returns Promise resolving to array of ButtonMetrics\n *\n * @example\n * ```ts\n * import { calculateMetrics } from 'aac-board-viewer';\n *\n * const metrics = await calculateMetrics(tree, {\n * accessMethod: 'direct',\n * });\n * ```\n */\nexport async function calculateMetrics(\n tree: AACTree,\n options: {\n accessMethod?: 'direct' | 'scanning';\n scanningConfig?: {\n pattern?: 'linear' | 'row-column' | 'block';\n selectionMethod?: string;\n errorCorrection?: boolean;\n };\n } = {}\n) {\n // Import MetricsCalculator dynamically to avoid circular dependencies\n const aacProcessors = await import('@willwade/aac-processors');\n\n // Use the calculator if available, otherwise return empty metrics\n if (!aacProcessors.MetricsCalculator) {\n console.warn('MetricsCalculator not available in this environment');\n return [];\n }\n\n const calculator = new aacProcessors.MetricsCalculator();\n\n let metricsOptions: Record<string, unknown> = {};\n\n if (options.accessMethod === 'scanning' && options.scanningConfig) {\n // Use string literals instead of enums to avoid import issues\n let cellScanningOrder = 0; // SimpleScan\n let blockScanEnabled = false;\n let selectionMethod = 0; // AutoScan\n\n switch (options.scanningConfig.pattern) {\n case 'linear':\n cellScanningOrder = 0; // SimpleScan\n break;\n case 'row-column':\n cellScanningOrder = 1; // RowColumnScan\n break;\n case 'block':\n cellScanningOrder = 1; // RowColumnScan\n blockScanEnabled = true;\n break;\n }\n\n metricsOptions = {\n scanningConfig: {\n cellScanningOrder,\n blockScanEnabled,\n selectionMethod,\n errorCorrectionEnabled: options.scanningConfig.errorCorrection || false,\n errorRate: 0.1,\n },\n };\n }\n\n const metricsResult = calculator.analyze(tree, metricsOptions);\n\n // Convert to the format expected by BoardViewer\n type MetricsButton = {\n id: string;\n label: string;\n effort: number;\n count?: number;\n level?: number;\n semantic_id?: string;\n clone_id?: string;\n };\n\n return metricsResult.buttons.map((btn: MetricsButton) => ({\n id: btn.id,\n label: btn.label,\n effort: btn.effort,\n count: btn.count ?? 0,\n is_word: true,\n level: btn.level,\n semantic_id: btn.semantic_id,\n clone_id: btn.clone_id,\n }));\n}\n\n/**\n * Get a list of supported file formats\n *\n * @returns Array of format information\n */\nexport function getSupportedFormats(): Array<{\n name: string;\n extensions: string[];\n description: string;\n browserCompatible: boolean;\n}> {\n return [\n {\n name: 'Grid 3',\n extensions: ['.gridset'],\n description: 'Smartbox Grid 3 communication boards',\n browserCompatible: true,\n },\n {\n name: 'TD Snap',\n extensions: ['.sps', '.spb'],\n description: 'Tobii Dynavox Snap files',\n browserCompatible: false,\n },\n {\n name: 'TouchChat',\n extensions: ['.ce'],\n description: 'Saltillo TouchChat files',\n browserCompatible: false,\n },\n {\n name: 'OpenBoard',\n extensions: ['.obf', '.obz'],\n description: 'OpenBoard Format (OBZ/OBF)',\n browserCompatible: true,\n },\n {\n name: 'Asterics Grid',\n extensions: ['.grd'],\n description: 'Asterics Grid files (.grd)',\n browserCompatible: true,\n },\n {\n name: 'Apple Panels',\n extensions: ['.plist'],\n description: 'Apple iOS Panels files',\n browserCompatible: true,\n },\n {\n name: 'OPML',\n extensions: ['.opml'],\n description: 'OPML outline files',\n browserCompatible: true,\n },\n {\n name: 'Excel',\n extensions: ['.xlsx', '.xls'],\n description: 'Excel spreadsheet boards',\n browserCompatible: false,\n },\n {\n name: 'DOT',\n extensions: ['.dot'],\n description: 'DOT graph visualization files',\n browserCompatible: true,\n },\n ];\n}\n\n/**\n * Check if a file format is compatible with browser processing\n *\n * @param extension - File extension (e.g., '.obf', 'obf')\n * @returns true if browser-compatible, false if server-only\n */\nexport function isBrowserCompatible(extension: string): boolean {\n const ext = extension.startsWith('.') ? extension.toLowerCase() : '.' + extension.toLowerCase();\n return BROWSER_EXTENSIONS.includes(ext);\n}\n\n/**\n * Get list of browser-compatible file extensions\n */\nexport function getBrowserExtensions(): string[] {\n return [...BROWSER_EXTENSIONS];\n}\n\n/**\n * Get list of Node-only file extensions\n */\nexport function getNodeOnlyExtensions(): string[] {\n return [...NODE_ONLY_EXTENSIONS];\n}\n"],"mappings":"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","names":["useState","useCallback","useState","useCallback","prev"]}
|
|
1
|
+
{"version":3,"sources":["../src/components/BoardViewer.tsx","../src/hooks/useAACFile.ts","../src/utils/loaders.ts","../src/index.ts"],"sourcesContent":["import React, { useState, useMemo, useCallback } from 'react';\nimport type {\n AACPage,\n AACButton,\n} from '@willwade/aac-processors';\nimport type { BoardViewerProps, ButtonMetric } from '../types';\n\n/**\n * Predictions Tooltip Component\n *\n * Shows a tooltip with predicted word forms when clicking the predictions indicator\n */\ninterface PredictionsTooltipProps {\n predictions: string[];\n label: string;\n position: { x: number; y: number };\n buttonMetricsLookup: { [buttonId: string]: ButtonMetric };\n onClose: () => void;\n onWordClick?: (word: string, effort: number) => void;\n}\n\nfunction PredictionsTooltip({ predictions, label, position, buttonMetricsLookup, onClose, onWordClick }: PredictionsTooltipProps) {\n // Close tooltip when clicking outside\n React.useEffect(() => {\n const handleClickOutside = (e: MouseEvent) => {\n if (e.target instanceof HTMLElement && !e.target.closest('.predictions-tooltip')) {\n onClose();\n }\n };\n document.addEventListener('mousedown', handleClickOutside);\n return () => document.removeEventListener('mousedown', handleClickOutside);\n }, [onClose]);\n\n return (\n <div\n className=\"predictions-tooltip fixed z-50 bg-white dark:bg-gray-800 rounded-lg shadow-xl border-2 border-purple-500 p-3 max-w-xs\"\n style={{\n left: `${Math.min(position.x, window.innerWidth - 200)}px`,\n top: `${Math.min(position.y, window.innerHeight - 150)}px`,\n }}\n onClick={(e) => e.stopPropagation()} // Prevent clicks from bubbling to underlying button\n >\n <div className=\"flex items-center justify-between mb-2\">\n <h4 className=\"text-sm font-semibold text-gray-900 dark:text-white\">\n Word forms for "{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 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 [pageHistory, setPageHistory] = useState<AACPage[]>([]);\n const [message, setMessage] = useState('');\n const [currentWordCount, setCurrentWordCount] = useState(0);\n const [currentEffort, setCurrentEffort] = useState(0);\n\n // Predictions tooltip state\n const [predictionsTooltip, setPredictionsTooltip] = useState<{\n predictions: string[];\n label: string;\n position: { x: number; y: number };\n buttonMetricsLookup: { [buttonId: string]: ButtonMetric };\n onWordClick?: (word: string, effort: number) => void;\n } | null>(null);\n\n // Convert button metrics array to lookup object for easy access\n const buttonMetricsLookup = useMemo(() => {\n if (!buttonMetrics) return {};\n const lookup: { [buttonId: string]: ButtonMetric } = {};\n buttonMetrics.forEach((metric) => {\n lookup[metric.id] = metric;\n });\n return lookup;\n }, [buttonMetrics]);\n\n const currentPage = currentPageId ? tree.pages[currentPageId] : null;\n const goToPage = (targetPageId: string | undefined | null) => {\n if (!targetPageId || !tree.pages[targetPageId]) return false;\n if (currentPage) {\n setPageHistory((prev) => [...prev, currentPage]);\n }\n setCurrentPageId(targetPageId);\n if (onPageChange) {\n onPageChange(targetPageId);\n }\n return true;\n };\n\n // Sync when tree or initialPageId changes\n React.useEffect(() => {\n setCurrentPageId(resolveInitialPageId());\n setPageHistory([]);\n }, [resolveInitialPageId]);\n\n // Calculate total stats for current word\n const updateStats = (word: string, effort: number) => {\n setCurrentWordCount((prev) => prev + 1);\n setCurrentEffort((prev) => prev + effort);\n };\n\n const handleButtonClick = (button: AACButton) => {\n // Call external callback if provided\n if (onButtonClick) {\n onButtonClick(button);\n }\n\n const intent = button.semanticAction?.intent\n ? String(button.semanticAction.intent)\n : undefined;\n const targetPageId = button.targetPageId || button.semanticAction?.targetId;\n const effort = buttonMetricsLookup[button.id]?.effort || 1;\n const textValue =\n button.semanticAction?.text || button.message || button.label || '';\n\n const deleteLastWord = () => {\n setMessage((prev) => {\n const parts = prev.trim().split(/\\s+/);\n parts.pop();\n const newMsg = parts.join(' ');\n return newMsg;\n });\n };\n\n const deleteLastCharacter = () => {\n setMessage((prev) => prev.slice(0, -1));\n };\n\n const appendText = (word: string) => {\n const trimmed = word || button.label || '';\n setMessage((prev) => {\n const newMessage = trimmed\n ? prev + (prev ? ' ' : '') + trimmed\n : prev;\n if (trimmed) {\n updateStats(trimmed, effort);\n }\n return newMessage;\n });\n };\n\n // Navigation takes precedence\n if (intent === 'NAVIGATE_TO' && goToPage(targetPageId)) {\n return;\n }\n\n switch (intent) {\n case 'GO_BACK':\n handleBack();\n return;\n case 'GO_HOME':\n if (tree.rootId && goToPage(tree.rootId)) return;\n break;\n case 'DELETE_WORD':\n deleteLastWord();\n return;\n case 'DELETE_CHARACTER':\n deleteLastCharacter();\n return;\n case 'CLEAR_TEXT':\n clearMessage();\n return;\n case 'SPEAK_IMMEDIATE':\n case 'SPEAK_TEXT':\n case 'INSERT_TEXT':\n appendText(textValue);\n return;\n default:\n break;\n }\n\n // Fallback navigation if intent not set but target exists\n if (targetPageId && goToPage(targetPageId)) {\n return;\n }\n\n // Otherwise add to message\n appendText(textValue);\n };\n\n const handleBack = () => {\n if (pageHistory.length > 0) {\n const previousPage = pageHistory[pageHistory.length - 1];\n setPageHistory((prev) => prev.slice(0, -1));\n setCurrentPageId(previousPage.id);\n if (onPageChange) {\n onPageChange(previousPage.id);\n }\n }\n };\n\n const clearMessage = () => {\n setMessage('');\n setCurrentWordCount(0);\n setCurrentEffort(0);\n };\n\n const handleShowPredictions = (\n button: AACButton,\n event: React.MouseEvent<HTMLDivElement>\n ) => {\n event.stopPropagation(); // Prevent button click\n const predictions = button.predictions || (button.parameters as { predictions?: string[] })?.predictions;\n if (predictions && predictions.length > 0) {\n setPredictionsTooltip({\n predictions,\n label: button.label,\n position: { x: event.clientX, y: event.clientY },\n buttonMetricsLookup,\n onWordClick: (word, effort) => {\n const trimmed = word || '';\n if (trimmed) {\n setMessage((prev) => {\n const newMessage = prev + (prev ? ' ' : '') + trimmed;\n updateStats(trimmed, effort);\n return newMessage;\n });\n }\n },\n });\n }\n };\n\n const getTextColor = (backgroundColor?: string) => {\n if (!backgroundColor) return 'text-gray-900 dark:text-gray-100';\n\n // Convert hex to rgb for brightness calculation\n const hex = backgroundColor.replace('#', '');\n if (hex.length === 6) {\n const r = parseInt(hex.substring(0, 2), 16);\n const g = parseInt(hex.substring(2, 4), 16);\n const b = parseInt(hex.substring(4, 6), 16);\n const brightness = (r * 299 + g * 587 + b * 114) / 1000;\n return brightness >= 128 ? 'text-gray-900' : 'text-white';\n }\n\n return 'text-gray-900 dark:text-gray-100';\n };\n\n if (!currentPage) {\n return (\n <div className={`flex items-center justify-center h-96 bg-gray-100 dark:bg-gray-800 rounded-lg ${className}`}>\n <div className=\"text-center\">\n <p className=\"text-gray-600 dark:text-gray-400\">No pages available</p>\n </div>\n </div>\n );\n }\n\n // Get toolbar page if it exists\n const toolbarPage = tree.toolbarId ? tree.pages[tree.toolbarId] : null;\n\n // Get grid dimensions\n const gridRows = currentPage.grid.length;\n const gridCols = gridRows > 0 ? currentPage.grid[0].length : 0;\n\n return (\n <div className={`bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden ${className}`}>\n {/* Message Bar */}\n {showMessageBar && (\n <div className=\"p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50\">\n <div className=\"flex items-start justify-between gap-4\">\n <div className=\"flex-1 min-w-0\">\n {message ? (\n <div className=\"space-y-2\">\n <p className=\"text-lg text-gray-900 dark:text-white break-words\">{message}</p>\n <div className=\"flex gap-4 text-sm\">\n <div className=\"text-gray-600 dark:text-gray-400\">\n {currentWordCount} {currentWordCount === 1 ? 'word' : 'words'}\n </div>\n {buttonMetrics && (\n <>\n <div className=\"text-gray-600 dark:text-gray-400\">\n Effort: <span className=\"font-medium\">{currentEffort.toFixed(2)}</span>\n </div>\n <div className=\"text-gray-600 dark:text-gray-400\">\n Avg:{' '}\n <span className=\"font-medium\">\n {currentWordCount > 0\n ? (currentEffort / currentWordCount).toFixed(2)\n : '0.00'}\n </span>\n </div>\n </>\n )}\n </div>\n </div>\n ) : (\n <p className=\"text-gray-500 dark:text-gray-400 italic\">\n Tap buttons to build a sentence...\n </p>\n )}\n </div>\n {message && (\n <button\n onClick={clearMessage}\n className=\"p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition\"\n aria-label=\"Clear message\"\n >\n <svg\n className=\"h-5 w-5 text-gray-600 dark:text-gray-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M6 18L18 6M6 6l12 12\"\n />\n </svg>\n </button>\n )}\n </div>\n </div>\n )}\n\n {/* Main Content Area */}\n <div className=\"flex\">\n {/* Toolbar Sidebar (if exists) */}\n {toolbarPage && (\n <div className=\"w-16 sm:w-20 border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50\">\n <div className=\"p-2\">\n <p className=\"text-[10px] text-gray-500 dark:text-gray-400 text-center mb-2 font-semibold\">\n TOOLBAR\n </p>\n <div className=\"grid gap-1\">\n {toolbarPage.grid.map((row, _rowIndex) =>\n row.map((button, _colIndex) => {\n if (!button) return null;\n\n const buttonMetric = buttonMetricsLookup[button.id];\n const effort = buttonMetric?.effort || 0;\n\n return (\n <button\n key={button.id}\n onClick={() => handleButtonClick(button)}\n className=\"aspect-square p-1 rounded border border-gray-200 dark:border-gray-700 transition flex flex-col items-center justify-center gap-0.5 hover:bg-gray-200 dark:hover:bg-gray-700 relative\"\n style={{\n backgroundColor: button.style?.backgroundColor || '#f3f4f6',\n borderColor: button.style?.borderColor || '#e5e7eb',\n }}\n title={`${button.label}\\n${button.message || ''}`}\n >\n {buttonMetric && showEffortBadges && effort > 0 && (\n <div className=\"absolute top-0 right-0 px-0.5 py-0 text-[8px] font-semibold rounded bg-blue-600 text-white\">\n {effort.toFixed(1)}\n </div>\n )}\n <span\n className={`text-[8px] sm:text-[9px] text-center font-medium leading-tight line-clamp-2 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.label}\n </span>\n </button>\n );\n })\n )}\n </div>\n </div>\n </div>\n )}\n\n {/* Main Page Content */}\n <div className=\"flex-1\">\n {/* Header */}\n <div className=\"flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700\">\n <div className=\"flex items-center gap-2\">\n {pageHistory.length > 0 && (\n <button\n onClick={handleBack}\n className=\"p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition\"\n aria-label=\"Go back\"\n >\n <svg\n className=\"h-5 w-5 text-gray-600 dark:text-gray-400\"\n fill=\"none\"\n stroke=\"currentColor\"\n viewBox=\"0 0 24 24\"\n >\n <path\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n strokeWidth={2}\n d=\"M15 19l-7-7 7-7\"\n />\n </svg>\n </button>\n )}\n <h3 className=\"text-lg font-semibold text-gray-900 dark:text-white\">\n {currentPage.name}\n </h3>\n </div>\n <div className=\"text-sm text-gray-500 dark:text-gray-400\">\n {gridRows}×{gridCols} grid\n </div>\n </div>\n\n {/* Grid */}\n <div\n className=\"p-4 gap-2 overflow-auto max-h-[600px]\"\n style={{\n display: 'grid',\n gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`,\n }}\n >\n {(() => {\n const rendered = new Set<string>();\n return currentPage.grid.flatMap((row, rowIndex) =>\n row.map((button, colIndex) => {\n if (!button) {\n return <div key={`empty-${rowIndex}-${colIndex}`} className=\"aspect-square\" />;\n }\n\n if (rendered.has(button.id)) {\n return null;\n }\n rendered.add(button.id);\n\n const buttonMetric = buttonMetricsLookup[button.id];\n const effort = buttonMetric?.effort || 0;\n const targetPageId = button.targetPageId || button.semanticAction?.targetId;\n const hasLink = targetPageId && tree.pages[targetPageId];\n const colSpan = button.columnSpan || 1;\n const rowSpan = button.rowSpan || 1;\n const predictions =\n button.predictions || (button.parameters as { predictions?: string[] })?.predictions;\n const hasPredictions = predictions && predictions.length > 0;\n const isPredictionCell =\n button.contentType === 'AutoContent' &&\n (button.contentSubType || '').toLowerCase() === 'prediction';\n const isWorkspace = button.contentType === 'Workspace';\n\n const imageSrc =\n (button.resolvedImageEntry && !String(button.resolvedImageEntry).startsWith('[')\n ? button.resolvedImageEntry\n : null) ||\n (button.image && !String(button.image).startsWith('[') ? button.image : null);\n\n // 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 className=\"relative aspect-square p-2 rounded-lg border-2 transition flex flex-col items-center justify-center gap-1 hover:opacity-80 hover:scale-105 active:scale-95\"\n style={{\n backgroundColor: button.style?.backgroundColor || '#f3f4f6',\n borderColor: button.style?.borderColor || '#e5e7eb',\n color: button.style?.fontColor || undefined,\n gridColumn: `${colIndex + 1} / span ${colSpan}`,\n gridRow: `${rowIndex + 1} / span ${rowSpan}`,\n }}\n >\n {/* Effort Badge */}\n {buttonMetric && showEffortBadges && (\n <div className=\"absolute top-1 right-1 px-1.5 py-0.5 text-xs font-semibold rounded bg-blue-600 text-white shadow-sm\">\n {effort.toFixed(1)}\n </div>\n )}\n\n {/* Link Indicator */}\n {hasLink && showLinkIndicators && (\n <div className=\"absolute top-1 left-1 w-2 h-2 bg-green-500 rounded-full shadow-sm\" />\n )}\n\n {/* Predictions Indicator */}\n {hasPredictions && (\n <div\n onClick={(e) => handleShowPredictions(button, e)}\n className=\"absolute bottom-1 right-1 px-1.5 py-0.5 text-xs font-semibold rounded bg-purple-600 text-white shadow-sm cursor-pointer hover:bg-purple-700 transition\"\n title={`Has ${predictions?.length} word form${predictions && predictions.length > 1 ? 's' : ''}`}\n >\n {predictions?.length}\n </div>\n )}\n\n {/* Image */}\n {(imageSrc || 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 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.label}\n </span>\n {isPredictionCell && predictions && predictions.length > 0 && (\n <div className=\"mt-1 text-[10px] sm:text-xs text-center opacity-80 space-y-0.5\">\n {predictions.slice(0, 3).map((p, idx) => (\n <div key={`${button.id}-pred-${idx}`}>\n {p}\n </div>\n ))}\n {predictions.length > 3 && <div>…</div>}\n </div>\n )}\n </div>\n\n {/* Message (if different from label) */}\n {button.message && button.message !== button.label && !isPredictionCell && (\n <span\n className={`text-[10px] sm:text-xs text-center opacity-75 line-clamp-2 ${getTextColor(\n button.style?.backgroundColor\n )}`}\n >\n {button.message}\n </span>\n )}\n </button>\n );\n })\n );\n })()}\n </div>\n\n {/* Page Navigation (if multiple pages) */}\n {Object.keys(tree.pages).length > 1 && (\n <div className=\"p-4 border-t border-gray-200 dark:border-gray-700\">\n <p className=\"text-sm text-gray-600 dark:text-gray-400 mb-2\">\n {Object.keys(tree.pages).length} pages in this vocabulary\n </p>\n </div>\n )}\n </div>\n </div>\n\n {/* Predictions Tooltip */}\n {predictionsTooltip && (\n <PredictionsTooltip\n predictions={predictionsTooltip.predictions}\n label={predictionsTooltip.label}\n position={predictionsTooltip.position}\n buttonMetricsLookup={predictionsTooltip.buttonMetricsLookup}\n onWordClick={predictionsTooltip.onWordClick}\n onClose={() => setPredictionsTooltip(null)}\n />\n )}\n </div>\n );\n}\n","/**\n * React hooks for AAC Board Viewer\n */\n\nimport { useState, useEffect, useCallback } from 'react';\nimport { loadAACFile, loadAACFileFromURL, calculateMetrics } from '../utils/loaders';\nimport type { AACTree } from '@willwade/aac-processors/browser';\nimport type { ButtonMetric, MetricsOptions } from '../types';\n\n/**\n * Hook to load an AAC file from a URL\n *\n * @param url - URL to the AAC file\n * @param options - Processor options (e.g., pageLayoutPreference for SNAP)\n * @returns Object with tree, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer() {\n * const { tree, loading, error, reload } = useAACFile('/files/board.sps');\n *\n * if (loading) return <div>Loading...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n *\n * return <BoardViewer tree={tree} />;\n * }\n * ```\n */\nexport function useAACFile(\n url: string,\n options?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (options?.enabled === false) {\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFileFromURL(url, options?.processorOptions);\n setTree(loadedTree);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [url, options]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to load an AAC file from a File object (Browser only)\n *\n * @param file - File object from file input\n * @param options - Processor options\n * @returns Object with tree, loading state, and error\n *\n * @example\n * ```tsx\n * function MyUploader() {\n * const [file, setFile] = useState<File | null>(null);\n * const { tree, loading, error } = useAACFileFromFile(file);\n *\n * return (\n * <div>\n * <input type=\"file\" onChange={(e) => setFile(e.target.files?.[0])} />\n * {loading && <div>Loading...</div>}\n * {tree && <BoardViewer tree={tree} />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useAACFileFromFile(\n file: File | Blob | null,\n options?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (!file || options?.enabled === false) {\n setTree(null);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFile(file, options?.processorOptions);\n setTree(loadedTree);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [file, options]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to load an AAC file and calculate metrics\n *\n * @param url - URL to the AAC file\n * @param metricsOptions - Metrics calculation options\n * @returns Object with tree, metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer() {\n * const { tree, metrics, loading, error } = useAACFileWithMetrics(\n * '/files/board.sps',\n * { accessMethod: 'direct' }\n * );\n *\n * if (loading) return <div>Loading...</div>;\n * if (error) return <div>Error: {error.message}</div>;\n *\n * return <BoardViewer tree={tree} buttonMetrics={metrics} />;\n * }\n * ```\n */\nexport function useAACFileWithMetrics(\n url: string,\n metricsOptions?: MetricsOptions,\n fileOptions?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [metrics, setMetrics] = useState<ButtonMetric[] | null>(null);\n const [loading, setLoading] = useState(true);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (fileOptions?.enabled === false) {\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFileFromURL(url, fileOptions?.processorOptions);\n setTree(loadedTree);\n\n // Calculate metrics\n const calculatedMetrics = await calculateMetrics(loadedTree, metricsOptions || {});\n setMetrics(calculatedMetrics);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [url, metricsOptions, fileOptions]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n metrics,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to load an AAC file from a File object and calculate metrics\n *\n * @param file - File object from file input\n * @param metricsOptions - Metrics calculation options\n * @returns Object with tree, metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyUploader() {\n * const [file, setFile] = useState<File | null>(null);\n * const { tree, metrics, loading, error } = useAACFileFromFileWithMetrics(\n * file,\n * { accessMethod: 'direct' }\n * );\n *\n * return (\n * <div>\n * <input type=\"file\" onChange={(e) => setFile(e.target.files?.[0])} />\n * {loading && <div>Loading...</div>}\n * {tree && <BoardViewer tree={tree} buttonMetrics={metrics} />}\n * </div>\n * );\n * }\n * ```\n */\nexport function useAACFileFromFileWithMetrics(\n file: File | Blob | null,\n metricsOptions?: MetricsOptions,\n fileOptions?: {\n processorOptions?: Record<string, unknown>;\n enabled?: boolean;\n }\n) {\n const [tree, setTree] = useState<AACTree | null>(null);\n const [metrics, setMetrics] = useState<ButtonMetric[] | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const load = useCallback(async () => {\n if (!file || fileOptions?.enabled === false) {\n setTree(null);\n setMetrics(null);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const loadedTree = await loadAACFile(file, fileOptions?.processorOptions);\n setTree(loadedTree);\n\n // Calculate metrics\n const calculatedMetrics = await calculateMetrics(loadedTree, metricsOptions || {});\n setMetrics(calculatedMetrics);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to load file'));\n } finally {\n setLoading(false);\n }\n }, [file, metricsOptions, fileOptions]);\n\n useEffect(() => {\n load();\n }, [load]);\n\n return {\n tree,\n metrics,\n loading,\n error,\n reload: load,\n };\n}\n\n/**\n * Hook to calculate metrics for a tree\n *\n * @param tree - The AAC tree\n * @param options - Metrics calculation options\n * @returns Object with metrics, loading state, and error\n *\n * @example\n * ```tsx\n * function MyViewer({ tree }) {\n * const { metrics, loading } = useMetrics(tree, {\n * accessMethod: 'scanning',\n * scanningConfig: { pattern: 'row-column' }\n * });\n *\n * return <BoardViewer tree={tree} buttonMetrics={metrics} />;\n * }\n * ```\n */\nexport function useMetrics(\n tree: AACTree | null,\n options?: MetricsOptions\n) {\n const [metrics, setMetrics] = useState<ButtonMetric[] | null>(null);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<Error | null>(null);\n\n const calculate = useCallback(async () => {\n if (!tree) {\n setMetrics(null);\n return;\n }\n\n setLoading(true);\n setError(null);\n\n try {\n const calculatedMetrics = await calculateMetrics(tree, options || {});\n setMetrics(calculatedMetrics);\n } catch (err) {\n setError(err instanceof Error ? err : new Error('Failed to calculate metrics'));\n } finally {\n setLoading(false);\n }\n }, [tree, options]);\n\n useEffect(() => {\n calculate();\n }, [calculate]);\n\n return {\n metrics,\n loading,\n error,\n recalculate: calculate,\n };\n}\n\n/**\n * Hook for sentence building state\n *\n * @returns Object with message state and handlers\n *\n * @example\n * ```tsx\n * function MyComponent() {\n * const { message, wordCount, effort, addWord, clear } = useSentenceBuilder();\n *\n * return (\n * <div>\n * <p>{message || 'Start building...'}</p>\n * <p>{wordCount} words, {effort.toFixed(2)} effort</p>\n * <button onClick={clear}>Clear</button>\n * </div>\n * );\n * }\n * ```\n */\nexport function useSentenceBuilder() {\n const [message, setMessage] = useState('');\n const [wordCount, setWordCount] = useState(0);\n const [effort, setEffort] = useState(0);\n\n const addWord = useCallback((word: string, wordEffort: number = 1) => {\n setMessage((prev) => {\n const newMessage = prev + (prev ? ' ' : '') + word;\n setWordCount((prev) => prev + 1);\n setEffort((prev) => prev + wordEffort);\n return newMessage;\n });\n }, []);\n\n const clear = useCallback(() => {\n setMessage('');\n setWordCount(0);\n setEffort(0);\n }, []);\n\n return {\n message,\n wordCount,\n effort,\n addWord,\n clear,\n };\n}\n","/**\n * AAC File Loading Utilities\n *\n * Provides utilities for loading AAC files from various sources\n * (File/Blob objects, file paths, URLs) in both browser and server contexts.\n *\n * Browser: Supports .obf, .obz, .gridset, .plist, .grd, .opml, .dot\n * Node.js: Supports all formats including .sps, .spb, .ce (Node-only processors)\n */\n\nimport type { AACTree } from '@willwade/aac-processors/browser';\nimport type { LoadAACFileResult } from '../types';\n\ntype ProcessorOptions = Record<string, unknown> | undefined;\n\ntype ProcessorModule = typeof import('@willwade/aac-processors/browser');\n\n// Node-only file extensions that require server-side processing\nconst NODE_ONLY_EXTENSIONS = ['.sps', '.spb', '.ce'];\n\n// Browser-compatible file extensions\nconst BROWSER_EXTENSIONS = ['.obf', '.obz', '.gridset', '.plist', '.grd', '.opml', '.dot'];\n\n/**\n * Detect if running in browser environment\n */\nfunction isBrowserEnvironment(): boolean {\n return typeof window !== 'undefined' && typeof window.document !== 'undefined';\n}\n\n/**\n * Lazily load processors so browser bundles avoid pulling in Node APIs\n */\nasync function importProcessors(): Promise<ProcessorModule> {\n return import('@willwade/aac-processors/browser');\n}\n\n/**\n * Get the appropriate processor for a file based on its extension\n */\nasync function getProcessorForFile(\n filepath: string,\n options?: ProcessorOptions\n): Promise<{ processor: any; extension: string }> {\n const { getProcessor } = await importProcessors();\n\n const ext = filepath.toLowerCase().split('.').pop();\n if (!ext) {\n throw new Error('Invalid file path: no extension found');\n }\n\n const extension = '.' + ext;\n\n // Check if this is a Node-only processor in browser environment\n if (isBrowserEnvironment() && NODE_ONLY_EXTENSIONS.includes(extension)) {\n throw new Error(\n `File type ${extension} requires server-side processing. ` +\n `Please use the server API or upload a browser-compatible format. ` +\n `Browser supports: ${BROWSER_EXTENSIONS.join(', ')}`\n );\n }\n\n // Use the factory function from v0.1.0\n const processor = getProcessor(extension);\n\n if (!processor) {\n throw new Error(`Unsupported file type: ${extension}`);\n }\n\n // For SNAP processor, it needs special options and is Node-only\n if (extension === '.sps' || extension === '.spb') {\n if (isBrowserEnvironment()) {\n throw new Error(\n `SNAP files (.sps, .spb) require server-side processing. ` +\n `Please use the server API or upload a browser-compatible format. ` +\n `Browser supports: ${BROWSER_EXTENSIONS.join(', ')}`\n );\n }\n // In Node.js, import from the main package for full processor support\n const nodeProcessors = await import('@willwade/aac-processors');\n const { SnapProcessor } = nodeProcessors;\n return {\n processor: options ? new SnapProcessor(null, options) : new SnapProcessor(),\n extension,\n };\n }\n\n return { processor, extension };\n}\n\n/**\n * Load an AAC file from a file path (Node.js only)\n *\n * @param filepath - Path to the AAC file\n * @param options - Processor options (e.g., pageLayoutPreference for SNAP)\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const tree = await loadAACFile('/path/to/file.sps');\n * ```\n */\nexport async function loadAACFile(\n filepath: string,\n options?: ProcessorOptions\n): Promise<AACTree>;\n\n/**\n * Load an AAC file from a File or Blob object (Browser only)\n *\n * @param file - File or Blob object\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const input = document.querySelector('input[type=\"file\"]');\n * const file = input.files[0];\n * const tree = await loadAACFile(file);\n * ```\n */\nexport async function loadAACFile(\n file: File | Blob,\n options?: ProcessorOptions\n): Promise<AACTree>;\n\n/**\n * Load an AAC file from an ArrayBuffer (Browser only)\n *\n * @param buffer - ArrayBuffer containing file data\n * @param filename - Filename with extension to detect processor\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const arrayBuffer = await file.arrayBuffer();\n * const tree = await loadAACFile(arrayBuffer, 'board.obf');\n * ```\n */\nexport async function loadAACFile(\n buffer: ArrayBuffer,\n filename: string,\n options?: ProcessorOptions\n): Promise<AACTree>;\n\n/**\n * Unified AAC file loading function that works in both browser and Node.js\n */\nexport async function loadAACFile(\n input: string | File | Blob | ArrayBuffer,\n optionsOrFilename?: ProcessorOptions | string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n // Handle ArrayBuffer case (requires filename as second parameter)\n if (input instanceof ArrayBuffer) {\n const filename = typeof optionsOrFilename === 'string' ? optionsOrFilename : 'unknown.bin';\n return loadFromArrayBuffer(input, filename, options);\n }\n\n // Handle File/Blob case (browser)\n if (input instanceof File || input instanceof Blob) {\n return loadAACFileFromFile(input, undefined, optionsOrFilename as ProcessorOptions);\n }\n\n // Handle string path case (Node.js or URL)\n if (typeof input === 'string') {\n // Check if it's a URL\n if (input.startsWith('http://') || input.startsWith('https://')) {\n return loadAACFileFromURL(input, optionsOrFilename as ProcessorOptions);\n }\n // It's a file path (Node.js only)\n return loadFromFilePath(input, optionsOrFilename as ProcessorOptions);\n }\n\n throw new Error('Invalid input type. Expected File, Blob, ArrayBuffer, or file path string.');\n}\n\n/**\n * Load from file path (Node.js only)\n */\nasync function loadFromFilePath(\n filepath: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const { processor } = await getProcessorForFile(filepath, options);\n return processor.loadIntoTree(filepath);\n}\n\n/**\n * Load from File or Blob (Browser only)\n */\nasync function loadAACFileFromFile(\n file: File | Blob,\n filename?: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n // Extract filename from File object if not provided\n const actualFilename = filename || (file instanceof File ? file.name : 'unknown.bin');\n\n // Check if this is a Node-only format\n const ext = '.' + actualFilename.toLowerCase().split('.').pop();\n if (NODE_ONLY_EXTENSIONS.includes(ext)) {\n throw new Error(\n `File type ${ext} requires server-side processing. ` +\n `Please use the server API or upload a browser-compatible format. ` +\n `Browser supports: ${BROWSER_EXTENSIONS.join(', ')}`\n );\n }\n\n // Read file as ArrayBuffer\n const arrayBuffer = await file.arrayBuffer();\n\n // Load using ArrayBuffer\n return loadFromArrayBuffer(arrayBuffer, actualFilename, options);\n}\n\n/**\n * Load from ArrayBuffer (Browser only)\n */\nasync function loadFromArrayBuffer(\n arrayBuffer: ArrayBuffer,\n filename: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const { processor } = await getProcessorForFile(filename, options);\n\n // AACProcessors v0.1.0+ supports loading from ArrayBuffer in browser\n return processor.loadIntoTree(arrayBuffer);\n}\n\n/**\n * Load an AAC file from a URL (Browser only)\n *\n * Note: This requires the server to provide the file with appropriate CORS headers.\n * For better performance, consider using loadAACFileFromFile() with direct file upload instead.\n *\n * @param url - URL to the AAC file\n * @param options - Processor options\n * @returns Promise resolving to AACTree\n *\n * @example\n * ```ts\n * const tree = await loadAACFileFromURL('https://example.com/file.obf');\n * ```\n */\nexport async function loadAACFileFromURL(\n url: string,\n options?: ProcessorOptions\n): Promise<AACTree> {\n const response = await fetch(url);\n if (!response.ok) {\n throw new Error(`Failed to load file: ${response.statusText}`);\n }\n\n const blob = await response.blob();\n const filename = getFilenameFromURL(url);\n\n return loadAACFileFromFile(blob, filename, options);\n}\n\n/**\n * Load an AAC file and return extended result with format info\n *\n * @param input - File path (Node.js), File/Blob (Browser), or URL string\n * @param options - Processor options\n * @returns Promise resolving to LoadAACFileResult\n */\nexport async function loadAACFileWithMetadata(\n input: string | File | Blob,\n options?: ProcessorOptions\n): Promise<LoadAACFileResult> {\n // Call loadAACFile with type assertion to handle the union type\n const tree = await loadAACFile(input as any, options);\n\n // Detect format from file extension\n let filepath = '';\n if (typeof input === 'string') {\n filepath = input;\n } else if (input instanceof File) {\n filepath = input.name;\n }\n\n const ext = filepath.toLowerCase();\n let format = 'unknown';\n\n if (ext.endsWith('.gridset')) format = 'gridset';\n else if (ext.endsWith('.sps') || ext.endsWith('.spb')) format = 'snap';\n else if (ext.endsWith('.ce')) format = 'touchchat';\n else if (ext.endsWith('.obf')) format = 'openboard';\n else if (ext.endsWith('.obz')) format = 'openboard';\n else if (ext.endsWith('.grd')) format = 'asterics-grid';\n else if (ext.endsWith('.plist')) format = 'apple-panels';\n else if (ext.endsWith('.opml')) format = 'opml';\n else if (ext.endsWith('.xlsx') || ext.endsWith('.xls')) format = 'excel';\n else if (ext.endsWith('.dot')) format = 'dot';\n\n return {\n tree,\n format,\n metadata: tree.metadata,\n };\n}\n\n/**\n * Extract filename from URL\n */\nfunction getFilenameFromURL(url: string): string {\n try {\n const urlObj = new URL(url);\n const pathname = urlObj.pathname;\n const parts = pathname.split('/');\n return parts[parts.length - 1] || 'unknown';\n } catch {\n return 'unknown';\n }\n}\n\n/**\n * Calculate cognitive effort metrics for an AAC tree\n *\n * @param tree - The AAC tree\n * @param options - Metrics calculation options\n * @returns Promise resolving to array of ButtonMetrics\n *\n * @example\n * ```ts\n * import { calculateMetrics } from 'aac-board-viewer';\n *\n * const metrics = await calculateMetrics(tree, {\n * accessMethod: 'direct',\n * });\n * ```\n */\nexport async function calculateMetrics(\n tree: AACTree,\n options: {\n accessMethod?: 'direct' | 'scanning';\n scanningConfig?: {\n pattern?: 'linear' | 'row-column' | 'block';\n selectionMethod?: string;\n errorCorrection?: boolean;\n };\n } = {}\n) {\n // Import MetricsCalculator dynamically to avoid circular dependencies\n const aacProcessors = await import('@willwade/aac-processors');\n\n // Use the calculator if available, otherwise return empty metrics\n if (!aacProcessors.MetricsCalculator) {\n console.warn('MetricsCalculator not available in this environment');\n return [];\n }\n\n const calculator = new aacProcessors.MetricsCalculator();\n\n let metricsOptions: Record<string, unknown> = {};\n\n if (options.accessMethod === 'scanning' && options.scanningConfig) {\n // Use string literals instead of enums to avoid import issues\n let cellScanningOrder = 0; // SimpleScan\n let blockScanEnabled = false;\n let selectionMethod = 0; // AutoScan\n\n switch (options.scanningConfig.pattern) {\n case 'linear':\n cellScanningOrder = 0; // SimpleScan\n break;\n case 'row-column':\n cellScanningOrder = 1; // RowColumnScan\n break;\n case 'block':\n cellScanningOrder = 1; // RowColumnScan\n blockScanEnabled = true;\n break;\n }\n\n metricsOptions = {\n scanningConfig: {\n cellScanningOrder,\n blockScanEnabled,\n selectionMethod,\n errorCorrectionEnabled: options.scanningConfig.errorCorrection || false,\n errorRate: 0.1,\n },\n };\n }\n\n const metricsResult = calculator.analyze(tree, metricsOptions);\n\n // Convert to the format expected by BoardViewer\n type MetricsButton = {\n id: string;\n label: string;\n effort: number;\n count?: number;\n level?: number;\n semantic_id?: string;\n clone_id?: string;\n };\n\n return metricsResult.buttons.map((btn: MetricsButton) => ({\n id: btn.id,\n label: btn.label,\n effort: btn.effort,\n count: btn.count ?? 0,\n is_word: true,\n level: btn.level,\n semantic_id: btn.semantic_id,\n clone_id: btn.clone_id,\n }));\n}\n\n/**\n * Get a list of supported file formats\n *\n * @returns Array of format information\n */\nexport function getSupportedFormats(): Array<{\n name: string;\n extensions: string[];\n description: string;\n browserCompatible: boolean;\n}> {\n return [\n {\n name: 'Grid 3',\n extensions: ['.gridset'],\n description: 'Smartbox Grid 3 communication boards',\n browserCompatible: true,\n },\n {\n name: 'TD Snap',\n extensions: ['.sps', '.spb'],\n description: 'Tobii Dynavox Snap files',\n browserCompatible: false,\n },\n {\n name: 'TouchChat',\n extensions: ['.ce'],\n description: 'Saltillo TouchChat files',\n browserCompatible: false,\n },\n {\n name: 'OpenBoard',\n extensions: ['.obf', '.obz'],\n description: 'OpenBoard Format (OBZ/OBF)',\n browserCompatible: true,\n },\n {\n name: 'Asterics Grid',\n extensions: ['.grd'],\n description: 'Asterics Grid files (.grd)',\n browserCompatible: true,\n },\n {\n name: 'Apple Panels',\n extensions: ['.plist'],\n description: 'Apple iOS Panels files',\n browserCompatible: true,\n },\n {\n name: 'OPML',\n extensions: ['.opml'],\n description: 'OPML outline files',\n browserCompatible: true,\n },\n {\n name: 'Excel',\n extensions: ['.xlsx', '.xls'],\n description: 'Excel spreadsheet boards',\n browserCompatible: false,\n },\n {\n name: 'DOT',\n extensions: ['.dot'],\n description: 'DOT graph visualization files',\n browserCompatible: true,\n },\n ];\n}\n\n/**\n * Check if a file format is compatible with browser processing\n *\n * @param extension - File extension (e.g., '.obf', 'obf')\n * @returns true if browser-compatible, false if server-only\n */\nexport function isBrowserCompatible(extension: string): boolean {\n const ext = extension.startsWith('.') ? extension.toLowerCase() : '.' + extension.toLowerCase();\n return BROWSER_EXTENSIONS.includes(ext);\n}\n\n/**\n * Get list of browser-compatible file extensions\n */\nexport function getBrowserExtensions(): string[] {\n return [...BROWSER_EXTENSIONS];\n}\n\n/**\n * Get list of Node-only file extensions\n */\nexport function getNodeOnlyExtensions(): string[] {\n return [...NODE_ONLY_EXTENSIONS];\n}\n","/**\n * AAC Board Viewer\n *\n * Universal AAC board viewer component for React\n */\n\n// Polyfill Buffer for browser environment (required by aac-processors)\nif (typeof window !== 'undefined' && typeof (window as any).Buffer === 'undefined') {\n (window as any).Buffer = {\n from: (data: any) => {\n if (typeof data === 'string') {\n const encoder = new TextEncoder();\n return encoder.encode(data);\n }\n if (data instanceof ArrayBuffer) {\n return new Uint8Array(data);\n }\n if (data instanceof Uint8Array) {\n return data;\n }\n return new Uint8Array(data);\n },\n alloc: (size: number) => new Uint8Array(size),\n allocUnsafe: (size: number) => new Uint8Array(size),\n concat: (list: Uint8Array[], totalLength?: number) => {\n const result = new Uint8Array(totalLength || list.reduce((sum, arr) => sum + arr.length, 0));\n let offset = 0;\n for (const arr of list) {\n result.set(arr, offset);\n offset += arr.length;\n }\n return result;\n },\n isBuffer: (obj: any) => obj instanceof Uint8Array,\n };\n}\n\n// Main component\nexport { BoardViewer } from './components/BoardViewer';\n\n// Hooks\nexport {\n useAACFile,\n useAACFileFromFile,\n useAACFileWithMetrics,\n useAACFileFromFileWithMetrics,\n useMetrics,\n useSentenceBuilder,\n} from './hooks/useAACFile';\n\n// Utilities\nexport {\n loadAACFile,\n loadAACFileFromURL,\n loadAACFileWithMetadata,\n calculateMetrics,\n getSupportedFormats,\n isBrowserCompatible,\n getBrowserExtensions,\n getNodeOnlyExtensions,\n} from './utils/loaders';\n\n// Types\nexport type {\n BoardViewerProps,\n ButtonMetric,\n LoadAACFileResult,\n MetricsOptions,\n} from './types';\n\n// Re-export AAC processor types\nexport type {\n AACTree,\n AACPage,\n AACButton,\n AACSemanticAction,\n AACSemanticCategory,\n AACSemanticIntent,\n} from '@willwade/aac-processors';\n"],"mappings":";AAAA,OAAO,SAAS,UAAU,SAAS,mBAAmB;AA2C9C,SAsUc,UA7TV,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,YAAY;AAAA,EACZ;AACF,GAAqB;AACnB,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,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;AAGzB,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,kBAAkB;AAAA,IAC/C;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,gBAC5C;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,WAAW,+EAA+E;AAAA,wBACxF,OAAO,OAAO;AAAA,sBAChB,CAAC;AAAA,sBAEA,iBAAO;AAAA;AAAA,kBACV;AAAA;AAAA;AAAA,cApBK,OAAO;AAAA,YAqBd;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,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,sBAAI,UAAU,OAAO,OAAO;AAE1B,wBAAI,OAAO,MAAM,SAAS,OAAQ,OAAO,UAAU;AACjD,+BAAS,cAAc,MAAM,IAAI,OAAO,QAAQ;AAAA,oBAClD,WAES,OAAO,gBAAgB,OAAO,MAAM,UAAa,OAAO,MAAM,QAAW;AAChF,+BAAS,cAAc,MAAM,IAAI,OAAO,YAAY,IAAI,OAAO,CAAC,IAAI,OAAO,CAAC;AAAA,oBAC9E;AAAA,kBACF;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,WAAU;AAAA,sBACV,OAAO;AAAA,wBACL,iBAAiB,OAAO,OAAO,mBAAmB;AAAA,wBAClD,aAAa,OAAO,OAAO,eAAe;AAAA,wBAC1C,OAAO,OAAO,OAAO,aAAa;AAAA,wBAClC,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,WAAW,yEAAyE;AAAA,gCAClF,OAAO,OAAO;AAAA,8BAChB,CAAC;AAAA,8BAEA,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,WAAW,8DAA8D;AAAA,8BACvE,OAAO,OAAO;AAAA,4BAChB,CAAC;AAAA,4BAEA,iBAAO;AAAA;AAAA,wBACV;AAAA;AAAA;AAAA,oBAxEG,OAAO;AAAA,kBA0Ed;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;;;AC1qBA,SAAS,YAAAA,WAAU,WAAW,eAAAC,oBAAmB;;;ACcjD,IAAM,uBAAuB,CAAC,QAAQ,QAAQ,KAAK;AAGnD,IAAM,qBAAqB,CAAC,QAAQ,QAAQ,YAAY,UAAU,QAAQ,SAAS,MAAM;AAKzF,SAAS,uBAAgC;AACvC,SAAO,OAAO,WAAW,eAAe,OAAO,OAAO,aAAa;AACrE;AAKA,eAAe,mBAA6C;AAC1D,SAAO,OAAO,kCAAkC;AAClD;AAKA,eAAe,oBACb,UACA,SACgD;AAChD,QAAM,EAAE,aAAa,IAAI,MAAM,iBAAiB;AAEhD,QAAM,MAAM,SAAS,YAAY,EAAE,MAAM,GAAG,EAAE,IAAI;AAClD,MAAI,CAAC,KAAK;AACR,UAAM,IAAI,MAAM,uCAAuC;AAAA,EACzD;AAEA,QAAM,YAAY,MAAM;AAGxB,MAAI,qBAAqB,KAAK,qBAAqB,SAAS,SAAS,GAAG;AACtE,UAAM,IAAI;AAAA,MACR,aAAa,SAAS,wHAED,mBAAmB,KAAK,IAAI,CAAC;AAAA,IACpD;AAAA,EACF;AAGA,QAAM,YAAY,aAAa,SAAS;AAExC,MAAI,CAAC,WAAW;AACd,UAAM,IAAI,MAAM,0BAA0B,SAAS,EAAE;AAAA,EACvD;AAGA,MAAI,cAAc,UAAU,cAAc,QAAQ;AAChD,QAAI,qBAAqB,GAAG;AAC1B,YAAM,IAAI;AAAA,QACR,8IAEqB,mBAAmB,KAAK,IAAI,CAAC;AAAA,MACpD;AAAA,IACF;AAEA,UAAM,iBAAiB,MAAM,OAAO,0BAA0B;AAC9D,UAAM,EAAE,cAAc,IAAI;AAC1B,WAAO;AAAA,MACL,WAAW,UAAU,IAAI,cAAc,MAAM,OAAO,IAAI,IAAI,cAAc;AAAA,MAC1E;AAAA,IACF;AAAA,EACF;AAEA,SAAO,EAAE,WAAW,UAAU;AAChC;AA6DA,eAAsB,YACpB,OACA,mBACA,SACkB;AAElB,MAAI,iBAAiB,aAAa;AAChC,UAAM,WAAW,OAAO,sBAAsB,WAAW,oBAAoB;AAC7E,WAAO,oBAAoB,OAAO,UAAU,OAAO;AAAA,EACrD;AAGA,MAAI,iBAAiB,QAAQ,iBAAiB,MAAM;AAClD,WAAO,oBAAoB,OAAO,QAAW,iBAAqC;AAAA,EACpF;AAGA,MAAI,OAAO,UAAU,UAAU;AAE7B,QAAI,MAAM,WAAW,SAAS,KAAK,MAAM,WAAW,UAAU,GAAG;AAC/D,aAAO,mBAAmB,OAAO,iBAAqC;AAAA,IACxE;AAEA,WAAO,iBAAiB,OAAO,iBAAqC;AAAA,EACtE;AAEA,QAAM,IAAI,MAAM,4EAA4E;AAC9F;AAKA,eAAe,iBACb,UACA,SACkB;AAClB,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB,UAAU,OAAO;AACjE,SAAO,UAAU,aAAa,QAAQ;AACxC;AAKA,eAAe,oBACb,MACA,UACA,SACkB;AAElB,QAAM,iBAAiB,aAAa,gBAAgB,OAAO,KAAK,OAAO;AAGvE,QAAM,MAAM,MAAM,eAAe,YAAY,EAAE,MAAM,GAAG,EAAE,IAAI;AAC9D,MAAI,qBAAqB,SAAS,GAAG,GAAG;AACtC,UAAM,IAAI;AAAA,MACR,aAAa,GAAG,wHAEK,mBAAmB,KAAK,IAAI,CAAC;AAAA,IACpD;AAAA,EACF;AAGA,QAAM,cAAc,MAAM,KAAK,YAAY;AAG3C,SAAO,oBAAoB,aAAa,gBAAgB,OAAO;AACjE;AAKA,eAAe,oBACb,aACA,UACA,SACkB;AAClB,QAAM,EAAE,UAAU,IAAI,MAAM,oBAAoB,UAAU,OAAO;AAGjE,SAAO,UAAU,aAAa,WAAW;AAC3C;AAiBA,eAAsB,mBACpB,KACA,SACkB;AAClB,QAAM,WAAW,MAAM,MAAM,GAAG;AAChC,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,IAAI,MAAM,wBAAwB,SAAS,UAAU,EAAE;AAAA,EAC/D;AAEA,QAAM,OAAO,MAAM,SAAS,KAAK;AACjC,QAAM,WAAW,mBAAmB,GAAG;AAEvC,SAAO,oBAAoB,MAAM,UAAU,OAAO;AACpD;AASA,eAAsB,wBACpB,OACA,SAC4B;AAE5B,QAAM,OAAO,MAAM,YAAY,OAAc,OAAO;AAGpD,MAAI,WAAW;AACf,MAAI,OAAO,UAAU,UAAU;AAC7B,eAAW;AAAA,EACb,WAAW,iBAAiB,MAAM;AAChC,eAAW,MAAM;AAAA,EACnB;AAEA,QAAM,MAAM,SAAS,YAAY;AACjC,MAAI,SAAS;AAEb,MAAI,IAAI,SAAS,UAAU,EAAG,UAAS;AAAA,WAC9B,IAAI,SAAS,MAAM,KAAK,IAAI,SAAS,MAAM,EAAG,UAAS;AAAA,WACvD,IAAI,SAAS,KAAK,EAAG,UAAS;AAAA,WAC9B,IAAI,SAAS,MAAM,EAAG,UAAS;AAAA,WAC/B,IAAI,SAAS,MAAM,EAAG,UAAS;AAAA,WAC/B,IAAI,SAAS,MAAM,EAAG,UAAS;AAAA,WAC/B,IAAI,SAAS,QAAQ,EAAG,UAAS;AAAA,WACjC,IAAI,SAAS,OAAO,EAAG,UAAS;AAAA,WAChC,IAAI,SAAS,OAAO,KAAK,IAAI,SAAS,MAAM,EAAG,UAAS;AAAA,WACxD,IAAI,SAAS,MAAM,EAAG,UAAS;AAExC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,UAAU,KAAK;AAAA,EACjB;AACF;AAKA,SAAS,mBAAmB,KAAqB;AAC/C,MAAI;AACF,UAAM,SAAS,IAAI,IAAI,GAAG;AAC1B,UAAM,WAAW,OAAO;AACxB,UAAM,QAAQ,SAAS,MAAM,GAAG;AAChC,WAAO,MAAM,MAAM,SAAS,CAAC,KAAK;AAAA,EACpC,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAkBA,eAAsB,iBACpB,MACA,UAOI,CAAC,GACL;AAEA,QAAM,gBAAgB,MAAM,OAAO,0BAA0B;AAG7D,MAAI,CAAC,cAAc,mBAAmB;AACpC,YAAQ,KAAK,qDAAqD;AAClE,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,aAAa,IAAI,cAAc,kBAAkB;AAEvD,MAAI,iBAA0C,CAAC;AAE/C,MAAI,QAAQ,iBAAiB,cAAc,QAAQ,gBAAgB;AAEjE,QAAI,oBAAoB;AACxB,QAAI,mBAAmB;AACvB,QAAI,kBAAkB;AAEtB,YAAQ,QAAQ,eAAe,SAAS;AAAA,MACtC,KAAK;AACH,4BAAoB;AACpB;AAAA,MACF,KAAK;AACH,4BAAoB;AACpB;AAAA,MACF,KAAK;AACH,4BAAoB;AACpB,2BAAmB;AACnB;AAAA,IACJ;AAEA,qBAAiB;AAAA,MACf,gBAAgB;AAAA,QACd;AAAA,QACA;AAAA,QACA;AAAA,QACA,wBAAwB,QAAQ,eAAe,mBAAmB;AAAA,QAClE,WAAW;AAAA,MACb;AAAA,IACF;AAAA,EACF;AAEA,QAAM,gBAAgB,WAAW,QAAQ,MAAM,cAAc;AAa7D,SAAO,cAAc,QAAQ,IAAI,CAAC,SAAwB;AAAA,IACxD,IAAI,IAAI;AAAA,IACR,OAAO,IAAI;AAAA,IACX,QAAQ,IAAI;AAAA,IACZ,OAAO,IAAI,SAAS;AAAA,IACpB,SAAS;AAAA,IACT,OAAO,IAAI;AAAA,IACX,aAAa,IAAI;AAAA,IACjB,UAAU,IAAI;AAAA,EAChB,EAAE;AACJ;AAOO,SAAS,sBAKb;AACD,SAAO;AAAA,IACL;AAAA,MACE,MAAM;AAAA,MACN,YAAY,CAAC,UAAU;AAAA,MACvB,aAAa;AAAA,MACb,mBAAmB;AAAA,IACrB;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,YAAY,CAAC,QAAQ,MAAM;AAAA,MAC3B,aAAa;AAAA,MACb,mBAAmB;AAAA,IACrB;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,YAAY,CAAC,KAAK;AAAA,MAClB,aAAa;AAAA,MACb,mBAAmB;AAAA,IACrB;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,YAAY,CAAC,QAAQ,MAAM;AAAA,MAC3B,aAAa;AAAA,MACb,mBAAmB;AAAA,IACrB;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,YAAY,CAAC,MAAM;AAAA,MACnB,aAAa;AAAA,MACb,mBAAmB;AAAA,IACrB;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,YAAY,CAAC,QAAQ;AAAA,MACrB,aAAa;AAAA,MACb,mBAAmB;AAAA,IACrB;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,YAAY,CAAC,OAAO;AAAA,MACpB,aAAa;AAAA,MACb,mBAAmB;AAAA,IACrB;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,YAAY,CAAC,SAAS,MAAM;AAAA,MAC5B,aAAa;AAAA,MACb,mBAAmB;AAAA,IACrB;AAAA,IACA;AAAA,MACE,MAAM;AAAA,MACN,YAAY,CAAC,MAAM;AAAA,MACnB,aAAa;AAAA,MACb,mBAAmB;AAAA,IACrB;AAAA,EACF;AACF;AAQO,SAAS,oBAAoB,WAA4B;AAC9D,QAAM,MAAM,UAAU,WAAW,GAAG,IAAI,UAAU,YAAY,IAAI,MAAM,UAAU,YAAY;AAC9F,SAAO,mBAAmB,SAAS,GAAG;AACxC;AAKO,SAAS,uBAAiC;AAC/C,SAAO,CAAC,GAAG,kBAAkB;AAC/B;AAKO,SAAS,wBAAkC;AAChD,SAAO,CAAC,GAAG,oBAAoB;AACjC;;;AD7dO,SAAS,WACd,KACA,SAIA;AACA,QAAM,CAAC,MAAM,OAAO,IAAIC,UAAyB,IAAI;AACrD,QAAM,CAAC,SAAS,UAAU,IAAIA,UAAS,IAAI;AAC3C,QAAM,CAAC,OAAO,QAAQ,IAAIA,UAAuB,IAAI;AAErD,QAAM,OAAOC,aAAY,YAAY;AACnC,QAAI,SAAS,YAAY,OAAO;AAC9B;AAAA,IACF;AAEA,eAAW,IAAI;AACf,aAAS,IAAI;AAEb,QAAI;AACF,YAAM,aAAa,MAAM,mBAAmB,KAAK,SAAS,gBAAgB;AAC1E,cAAQ,UAAU;AAAA,IACpB,SAAS,KAAK;AACZ,eAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,qBAAqB,CAAC;AAAA,IACxE,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,KAAK,OAAO,CAAC;AAEjB,YAAU,MAAM;AACd,SAAK;AAAA,EACP,GAAG,CAAC,IAAI,CAAC;AAET,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,EACV;AACF;AAyBO,SAAS,mBACd,MACA,SAIA;AACA,QAAM,CAAC,MAAM,OAAO,IAAID,UAAyB,IAAI;AACrD,QAAM,CAAC,SAAS,UAAU,IAAIA,UAAS,KAAK;AAC5C,QAAM,CAAC,OAAO,QAAQ,IAAIA,UAAuB,IAAI;AAErD,QAAM,OAAOC,aAAY,YAAY;AACnC,QAAI,CAAC,QAAQ,SAAS,YAAY,OAAO;AACvC,cAAQ,IAAI;AACZ;AAAA,IACF;AAEA,eAAW,IAAI;AACf,aAAS,IAAI;AAEb,QAAI;AACF,YAAM,aAAa,MAAM,YAAY,MAAM,SAAS,gBAAgB;AACpE,cAAQ,UAAU;AAAA,IACpB,SAAS,KAAK;AACZ,eAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,qBAAqB,CAAC;AAAA,IACxE,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,MAAM,OAAO,CAAC;AAElB,YAAU,MAAM;AACd,SAAK;AAAA,EACP,GAAG,CAAC,IAAI,CAAC;AAET,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,EACV;AACF;AAwBO,SAAS,sBACd,KACA,gBACA,aAIA;AACA,QAAM,CAAC,MAAM,OAAO,IAAID,UAAyB,IAAI;AACrD,QAAM,CAAC,SAAS,UAAU,IAAIA,UAAgC,IAAI;AAClE,QAAM,CAAC,SAAS,UAAU,IAAIA,UAAS,IAAI;AAC3C,QAAM,CAAC,OAAO,QAAQ,IAAIA,UAAuB,IAAI;AAErD,QAAM,OAAOC,aAAY,YAAY;AACnC,QAAI,aAAa,YAAY,OAAO;AAClC;AAAA,IACF;AAEA,eAAW,IAAI;AACf,aAAS,IAAI;AAEb,QAAI;AACF,YAAM,aAAa,MAAM,mBAAmB,KAAK,aAAa,gBAAgB;AAC9E,cAAQ,UAAU;AAGlB,YAAM,oBAAoB,MAAM,iBAAiB,YAAY,kBAAkB,CAAC,CAAC;AACjF,iBAAW,iBAAiB;AAAA,IAC9B,SAAS,KAAK;AACZ,eAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,qBAAqB,CAAC;AAAA,IACxE,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,KAAK,gBAAgB,WAAW,CAAC;AAErC,YAAU,MAAM;AACd,SAAK;AAAA,EACP,GAAG,CAAC,IAAI,CAAC;AAET,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,EACV;AACF;AA4BO,SAAS,8BACd,MACA,gBACA,aAIA;AACA,QAAM,CAAC,MAAM,OAAO,IAAID,UAAyB,IAAI;AACrD,QAAM,CAAC,SAAS,UAAU,IAAIA,UAAgC,IAAI;AAClE,QAAM,CAAC,SAAS,UAAU,IAAIA,UAAS,KAAK;AAC5C,QAAM,CAAC,OAAO,QAAQ,IAAIA,UAAuB,IAAI;AAErD,QAAM,OAAOC,aAAY,YAAY;AACnC,QAAI,CAAC,QAAQ,aAAa,YAAY,OAAO;AAC3C,cAAQ,IAAI;AACZ,iBAAW,IAAI;AACf;AAAA,IACF;AAEA,eAAW,IAAI;AACf,aAAS,IAAI;AAEb,QAAI;AACF,YAAM,aAAa,MAAM,YAAY,MAAM,aAAa,gBAAgB;AACxE,cAAQ,UAAU;AAGlB,YAAM,oBAAoB,MAAM,iBAAiB,YAAY,kBAAkB,CAAC,CAAC;AACjF,iBAAW,iBAAiB;AAAA,IAC9B,SAAS,KAAK;AACZ,eAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,qBAAqB,CAAC;AAAA,IACxE,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,MAAM,gBAAgB,WAAW,CAAC;AAEtC,YAAU,MAAM;AACd,SAAK;AAAA,EACP,GAAG,CAAC,IAAI,CAAC;AAET,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,EACV;AACF;AAqBO,SAAS,WACd,MACA,SACA;AACA,QAAM,CAAC,SAAS,UAAU,IAAID,UAAgC,IAAI;AAClE,QAAM,CAAC,SAAS,UAAU,IAAIA,UAAS,KAAK;AAC5C,QAAM,CAAC,OAAO,QAAQ,IAAIA,UAAuB,IAAI;AAErD,QAAM,YAAYC,aAAY,YAAY;AACxC,QAAI,CAAC,MAAM;AACT,iBAAW,IAAI;AACf;AAAA,IACF;AAEA,eAAW,IAAI;AACf,aAAS,IAAI;AAEb,QAAI;AACF,YAAM,oBAAoB,MAAM,iBAAiB,MAAM,WAAW,CAAC,CAAC;AACpE,iBAAW,iBAAiB;AAAA,IAC9B,SAAS,KAAK;AACZ,eAAS,eAAe,QAAQ,MAAM,IAAI,MAAM,6BAA6B,CAAC;AAAA,IAChF,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,MAAM,OAAO,CAAC;AAElB,YAAU,MAAM;AACd,cAAU;AAAA,EACZ,GAAG,CAAC,SAAS,CAAC;AAEd,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa;AAAA,EACf;AACF;AAsBO,SAAS,qBAAqB;AACnC,QAAM,CAAC,SAAS,UAAU,IAAID,UAAS,EAAE;AACzC,QAAM,CAAC,WAAW,YAAY,IAAIA,UAAS,CAAC;AAC5C,QAAM,CAAC,QAAQ,SAAS,IAAIA,UAAS,CAAC;AAEtC,QAAM,UAAUC,aAAY,CAAC,MAAc,aAAqB,MAAM;AACpE,eAAW,CAAC,SAAS;AACnB,YAAM,aAAa,QAAQ,OAAO,MAAM,MAAM;AAC9C,mBAAa,CAACC,UAASA,QAAO,CAAC;AAC/B,gBAAU,CAACA,UAASA,QAAO,UAAU;AACrC,aAAO;AAAA,IACT,CAAC;AAAA,EACH,GAAG,CAAC,CAAC;AAEL,QAAM,QAAQD,aAAY,MAAM;AAC9B,eAAW,EAAE;AACb,iBAAa,CAAC;AACd,cAAU,CAAC;AAAA,EACb,GAAG,CAAC,CAAC;AAEL,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;;;AE1XA,IAAI,OAAO,WAAW,eAAe,OAAQ,OAAe,WAAW,aAAa;AAClF,EAAC,OAAe,SAAS;AAAA,IACvB,MAAM,CAAC,SAAc;AACnB,UAAI,OAAO,SAAS,UAAU;AAC5B,cAAM,UAAU,IAAI,YAAY;AAChC,eAAO,QAAQ,OAAO,IAAI;AAAA,MAC5B;AACA,UAAI,gBAAgB,aAAa;AAC/B,eAAO,IAAI,WAAW,IAAI;AAAA,MAC5B;AACA,UAAI,gBAAgB,YAAY;AAC9B,eAAO;AAAA,MACT;AACA,aAAO,IAAI,WAAW,IAAI;AAAA,IAC5B;AAAA,IACA,OAAO,CAAC,SAAiB,IAAI,WAAW,IAAI;AAAA,IAC5C,aAAa,CAAC,SAAiB,IAAI,WAAW,IAAI;AAAA,IAClD,QAAQ,CAAC,MAAoB,gBAAyB;AACpD,YAAM,SAAS,IAAI,WAAW,eAAe,KAAK,OAAO,CAAC,KAAK,QAAQ,MAAM,IAAI,QAAQ,CAAC,CAAC;AAC3F,UAAI,SAAS;AACb,iBAAW,OAAO,MAAM;AACtB,eAAO,IAAI,KAAK,MAAM;AACtB,kBAAU,IAAI;AAAA,MAChB;AACA,aAAO;AAAA,IACT;AAAA,IACA,UAAU,CAAC,QAAa,eAAe;AAAA,EACzC;AACF;","names":["useState","useCallback","useState","useCallback","prev"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "aac-board-viewer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Universal AAC board viewer component for React. Supports GridSet, TouchChat, SNAP, OpenBoard, Asterics, Apple Panels, and more.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"aac",
|
|
@@ -50,25 +50,25 @@
|
|
|
50
50
|
"type-check": "tsc --noEmit"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"react": "^18.0.0",
|
|
54
|
-
"react-dom": "^18.0.0"
|
|
53
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
54
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"@willwade/aac-processors": "^0.1.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@types/react": "^
|
|
61
|
-
"@types/react-dom": "^
|
|
62
|
-
"@typescript-eslint/eslint-plugin": "^
|
|
63
|
-
"@typescript-eslint/parser": "^
|
|
64
|
-
"@vitejs/plugin-react": "^4.
|
|
60
|
+
"@types/react": "^19.0.0",
|
|
61
|
+
"@types/react-dom": "^19.0.0",
|
|
62
|
+
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
|
63
|
+
"@typescript-eslint/parser": "^8.0.0",
|
|
64
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
65
65
|
"autoprefixer": "^10.4.23",
|
|
66
|
-
"eslint": "^
|
|
67
|
-
"eslint-plugin-react-hooks": "^
|
|
66
|
+
"eslint": "^9.0.0",
|
|
67
|
+
"eslint-plugin-react-hooks": "^5.0.0",
|
|
68
68
|
"postcss": "^8.5.6",
|
|
69
69
|
"tailwindcss": "^3.4.19",
|
|
70
70
|
"tsup": "^8.0.0",
|
|
71
|
-
"typescript": "^5.
|
|
72
|
-
"vite": "^
|
|
71
|
+
"typescript": "^5.7.0",
|
|
72
|
+
"vite": "^6.0.0"
|
|
73
73
|
}
|
|
74
74
|
}
|