aac-board-viewer 0.1.1
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/LICENSE +21 -0
- package/README.md +301 -0
- package/dist/index.d.mts +305 -0
- package/dist/index.d.ts +305 -0
- package/dist/index.js +675 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +628 -0
- package/dist/index.mjs.map +1 -0
- package/dist/styles.css +57 -0
- package/dist/styles.d.mts +2 -0
- package/dist/styles.d.ts +2 -0
- package/package.json +74 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
1
|
+
// src/components/BoardViewer.tsx
|
|
2
|
+
import React, { useState, useMemo, useCallback } from "react";
|
|
3
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
function BoardViewer({
|
|
5
|
+
tree,
|
|
6
|
+
buttonMetrics,
|
|
7
|
+
showMessageBar = true,
|
|
8
|
+
showEffortBadges = true,
|
|
9
|
+
showLinkIndicators = true,
|
|
10
|
+
initialPageId,
|
|
11
|
+
onButtonClick,
|
|
12
|
+
onPageChange,
|
|
13
|
+
className = ""
|
|
14
|
+
}) {
|
|
15
|
+
const resolveInitialPageId = useCallback(() => {
|
|
16
|
+
if (initialPageId && tree.pages[initialPageId]) {
|
|
17
|
+
return initialPageId;
|
|
18
|
+
}
|
|
19
|
+
if (tree.toolbarId && tree.rootId && tree.rootId !== tree.toolbarId) {
|
|
20
|
+
return tree.rootId;
|
|
21
|
+
}
|
|
22
|
+
if (tree.rootId && tree.pages[tree.rootId]) {
|
|
23
|
+
const rootPage = tree.pages[tree.rootId];
|
|
24
|
+
const isToolbar = rootPage.name.toLowerCase().includes("toolbar") || rootPage.name.toLowerCase().includes("tool bar");
|
|
25
|
+
if (!isToolbar) {
|
|
26
|
+
return tree.rootId;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
const nonToolbarPage = Object.values(tree.pages).find(
|
|
30
|
+
(p) => !p.name.toLowerCase().includes("toolbar") && !p.name.toLowerCase().includes("tool bar")
|
|
31
|
+
);
|
|
32
|
+
if (nonToolbarPage) {
|
|
33
|
+
return nonToolbarPage.id;
|
|
34
|
+
}
|
|
35
|
+
const pageIds = Object.keys(tree.pages);
|
|
36
|
+
return pageIds.length > 0 ? pageIds[0] : null;
|
|
37
|
+
}, [initialPageId, tree]);
|
|
38
|
+
const [currentPageId, setCurrentPageId] = useState(() => resolveInitialPageId());
|
|
39
|
+
const [pageHistory, setPageHistory] = useState([]);
|
|
40
|
+
const [message, setMessage] = useState("");
|
|
41
|
+
const [currentWordCount, setCurrentWordCount] = useState(0);
|
|
42
|
+
const [currentEffort, setCurrentEffort] = useState(0);
|
|
43
|
+
const buttonMetricsLookup = useMemo(() => {
|
|
44
|
+
if (!buttonMetrics) return {};
|
|
45
|
+
const lookup = {};
|
|
46
|
+
buttonMetrics.forEach((metric) => {
|
|
47
|
+
lookup[metric.id] = metric;
|
|
48
|
+
});
|
|
49
|
+
return lookup;
|
|
50
|
+
}, [buttonMetrics]);
|
|
51
|
+
const currentPage = currentPageId ? tree.pages[currentPageId] : null;
|
|
52
|
+
React.useEffect(() => {
|
|
53
|
+
setCurrentPageId(resolveInitialPageId());
|
|
54
|
+
setPageHistory([]);
|
|
55
|
+
}, [resolveInitialPageId]);
|
|
56
|
+
const updateStats = (word, effort) => {
|
|
57
|
+
setCurrentWordCount((prev) => prev + 1);
|
|
58
|
+
setCurrentEffort((prev) => prev + effort);
|
|
59
|
+
};
|
|
60
|
+
const handleButtonClick = (button) => {
|
|
61
|
+
if (onButtonClick) {
|
|
62
|
+
onButtonClick(button);
|
|
63
|
+
}
|
|
64
|
+
const targetPageId = button.targetPageId || button.semanticAction?.targetId;
|
|
65
|
+
if (targetPageId && tree.pages[targetPageId]) {
|
|
66
|
+
if (currentPage) {
|
|
67
|
+
setPageHistory((prev) => [...prev, currentPage]);
|
|
68
|
+
}
|
|
69
|
+
setCurrentPageId(targetPageId);
|
|
70
|
+
if (onPageChange) {
|
|
71
|
+
onPageChange(targetPageId);
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const word = button.message || button.label;
|
|
76
|
+
const effort = buttonMetricsLookup[button.id]?.effort || 1;
|
|
77
|
+
setMessage((prev) => {
|
|
78
|
+
const newMessage = prev + (prev ? " " : "") + word;
|
|
79
|
+
updateStats(word, effort);
|
|
80
|
+
return newMessage;
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
const handleBack = () => {
|
|
84
|
+
if (pageHistory.length > 0) {
|
|
85
|
+
const previousPage = pageHistory[pageHistory.length - 1];
|
|
86
|
+
setPageHistory((prev) => prev.slice(0, -1));
|
|
87
|
+
setCurrentPageId(previousPage.id);
|
|
88
|
+
if (onPageChange) {
|
|
89
|
+
onPageChange(previousPage.id);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
const clearMessage = () => {
|
|
94
|
+
setMessage("");
|
|
95
|
+
setCurrentWordCount(0);
|
|
96
|
+
setCurrentEffort(0);
|
|
97
|
+
};
|
|
98
|
+
const getTextColor = (backgroundColor) => {
|
|
99
|
+
if (!backgroundColor) return "text-gray-900 dark:text-gray-100";
|
|
100
|
+
const hex = backgroundColor.replace("#", "");
|
|
101
|
+
if (hex.length === 6) {
|
|
102
|
+
const r = parseInt(hex.substring(0, 2), 16);
|
|
103
|
+
const g = parseInt(hex.substring(2, 4), 16);
|
|
104
|
+
const b = parseInt(hex.substring(4, 6), 16);
|
|
105
|
+
const brightness = (r * 299 + g * 587 + b * 114) / 1e3;
|
|
106
|
+
return brightness >= 128 ? "text-gray-900" : "text-white";
|
|
107
|
+
}
|
|
108
|
+
return "text-gray-900 dark:text-gray-100";
|
|
109
|
+
};
|
|
110
|
+
if (!currentPage) {
|
|
111
|
+
return /* @__PURE__ */ jsx("div", { className: `flex items-center justify-center h-96 bg-gray-100 dark:bg-gray-800 rounded-lg ${className}`, children: /* @__PURE__ */ jsx("div", { className: "text-center", children: /* @__PURE__ */ jsx("p", { className: "text-gray-600 dark:text-gray-400", children: "No pages available" }) }) });
|
|
112
|
+
}
|
|
113
|
+
const toolbarPage = tree.toolbarId ? tree.pages[tree.toolbarId] : null;
|
|
114
|
+
const gridRows = currentPage.grid.length;
|
|
115
|
+
const gridCols = gridRows > 0 ? currentPage.grid[0].length : 0;
|
|
116
|
+
return /* @__PURE__ */ jsxs("div", { className: `bg-white dark:bg-gray-800 rounded-lg shadow-xl overflow-hidden ${className}`, children: [
|
|
117
|
+
showMessageBar && /* @__PURE__ */ jsx("div", { className: "p-4 border-b border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50", children: /* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-4", children: [
|
|
118
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 min-w-0", children: message ? /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
119
|
+
/* @__PURE__ */ jsx("p", { className: "text-lg text-gray-900 dark:text-white break-words", children: message }),
|
|
120
|
+
/* @__PURE__ */ jsxs("div", { className: "flex gap-4 text-sm", children: [
|
|
121
|
+
/* @__PURE__ */ jsxs("div", { className: "text-gray-600 dark:text-gray-400", children: [
|
|
122
|
+
currentWordCount,
|
|
123
|
+
" ",
|
|
124
|
+
currentWordCount === 1 ? "word" : "words"
|
|
125
|
+
] }),
|
|
126
|
+
buttonMetrics && /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
127
|
+
/* @__PURE__ */ jsxs("div", { className: "text-gray-600 dark:text-gray-400", children: [
|
|
128
|
+
"Effort: ",
|
|
129
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: currentEffort.toFixed(2) })
|
|
130
|
+
] }),
|
|
131
|
+
/* @__PURE__ */ jsxs("div", { className: "text-gray-600 dark:text-gray-400", children: [
|
|
132
|
+
"Avg:",
|
|
133
|
+
" ",
|
|
134
|
+
/* @__PURE__ */ jsx("span", { className: "font-medium", children: currentWordCount > 0 ? (currentEffort / currentWordCount).toFixed(2) : "0.00" })
|
|
135
|
+
] })
|
|
136
|
+
] })
|
|
137
|
+
] })
|
|
138
|
+
] }) : /* @__PURE__ */ jsx("p", { className: "text-gray-500 dark:text-gray-400 italic", children: "Tap buttons to build a sentence..." }) }),
|
|
139
|
+
message && /* @__PURE__ */ jsx(
|
|
140
|
+
"button",
|
|
141
|
+
{
|
|
142
|
+
onClick: clearMessage,
|
|
143
|
+
className: "p-2 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition",
|
|
144
|
+
"aria-label": "Clear message",
|
|
145
|
+
children: /* @__PURE__ */ jsx(
|
|
146
|
+
"svg",
|
|
147
|
+
{
|
|
148
|
+
className: "h-5 w-5 text-gray-600 dark:text-gray-400",
|
|
149
|
+
fill: "none",
|
|
150
|
+
stroke: "currentColor",
|
|
151
|
+
viewBox: "0 0 24 24",
|
|
152
|
+
children: /* @__PURE__ */ jsx(
|
|
153
|
+
"path",
|
|
154
|
+
{
|
|
155
|
+
strokeLinecap: "round",
|
|
156
|
+
strokeLinejoin: "round",
|
|
157
|
+
strokeWidth: 2,
|
|
158
|
+
d: "M6 18L18 6M6 6l12 12"
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
] }) }),
|
|
166
|
+
/* @__PURE__ */ jsxs("div", { className: "flex", children: [
|
|
167
|
+
toolbarPage && /* @__PURE__ */ jsx("div", { className: "w-16 sm:w-20 border-r border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-900/50", children: /* @__PURE__ */ jsxs("div", { className: "p-2", children: [
|
|
168
|
+
/* @__PURE__ */ jsx("p", { className: "text-[10px] text-gray-500 dark:text-gray-400 text-center mb-2 font-semibold", children: "TOOLBAR" }),
|
|
169
|
+
/* @__PURE__ */ jsx("div", { className: "grid gap-1", children: toolbarPage.grid.map(
|
|
170
|
+
(row, _rowIndex) => row.map((button, _colIndex) => {
|
|
171
|
+
if (!button) return null;
|
|
172
|
+
const buttonMetric = buttonMetricsLookup[button.id];
|
|
173
|
+
const effort = buttonMetric?.effort || 0;
|
|
174
|
+
return /* @__PURE__ */ jsxs(
|
|
175
|
+
"button",
|
|
176
|
+
{
|
|
177
|
+
onClick: () => handleButtonClick(button),
|
|
178
|
+
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",
|
|
179
|
+
style: {
|
|
180
|
+
backgroundColor: button.style?.backgroundColor || "#f3f4f6",
|
|
181
|
+
borderColor: button.style?.borderColor || "#e5e7eb"
|
|
182
|
+
},
|
|
183
|
+
title: `${button.label}
|
|
184
|
+
${button.message || ""}`,
|
|
185
|
+
children: [
|
|
186
|
+
buttonMetric && showEffortBadges && effort > 0 && /* @__PURE__ */ jsx("div", { className: "absolute top-0 right-0 px-0.5 py-0 text-[8px] font-semibold rounded bg-blue-600 text-white", children: effort.toFixed(1) }),
|
|
187
|
+
/* @__PURE__ */ jsx(
|
|
188
|
+
"span",
|
|
189
|
+
{
|
|
190
|
+
className: `text-[8px] sm:text-[9px] text-center font-medium leading-tight line-clamp-2 ${getTextColor(
|
|
191
|
+
button.style?.backgroundColor
|
|
192
|
+
)}`,
|
|
193
|
+
children: button.label
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
]
|
|
197
|
+
},
|
|
198
|
+
button.id
|
|
199
|
+
);
|
|
200
|
+
})
|
|
201
|
+
) })
|
|
202
|
+
] }) }),
|
|
203
|
+
/* @__PURE__ */ jsxs("div", { className: "flex-1", children: [
|
|
204
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700", children: [
|
|
205
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
206
|
+
pageHistory.length > 0 && /* @__PURE__ */ jsx(
|
|
207
|
+
"button",
|
|
208
|
+
{
|
|
209
|
+
onClick: handleBack,
|
|
210
|
+
className: "p-2 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition",
|
|
211
|
+
"aria-label": "Go back",
|
|
212
|
+
children: /* @__PURE__ */ jsx(
|
|
213
|
+
"svg",
|
|
214
|
+
{
|
|
215
|
+
className: "h-5 w-5 text-gray-600 dark:text-gray-400",
|
|
216
|
+
fill: "none",
|
|
217
|
+
stroke: "currentColor",
|
|
218
|
+
viewBox: "0 0 24 24",
|
|
219
|
+
children: /* @__PURE__ */ jsx(
|
|
220
|
+
"path",
|
|
221
|
+
{
|
|
222
|
+
strokeLinecap: "round",
|
|
223
|
+
strokeLinejoin: "round",
|
|
224
|
+
strokeWidth: 2,
|
|
225
|
+
d: "M15 19l-7-7 7-7"
|
|
226
|
+
}
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
),
|
|
232
|
+
/* @__PURE__ */ jsx("h3", { className: "text-lg font-semibold text-gray-900 dark:text-white", children: currentPage.name })
|
|
233
|
+
] }),
|
|
234
|
+
/* @__PURE__ */ jsxs("div", { className: "text-sm text-gray-500 dark:text-gray-400", children: [
|
|
235
|
+
gridRows,
|
|
236
|
+
"\xD7",
|
|
237
|
+
gridCols,
|
|
238
|
+
" grid"
|
|
239
|
+
] })
|
|
240
|
+
] }),
|
|
241
|
+
/* @__PURE__ */ jsx(
|
|
242
|
+
"div",
|
|
243
|
+
{
|
|
244
|
+
className: "p-4 gap-2 overflow-auto max-h-[600px]",
|
|
245
|
+
style: {
|
|
246
|
+
display: "grid",
|
|
247
|
+
gridTemplateColumns: `repeat(${gridCols}, minmax(0, 1fr))`
|
|
248
|
+
},
|
|
249
|
+
children: currentPage.grid.map(
|
|
250
|
+
(row, rowIndex) => row.map((button, colIndex) => {
|
|
251
|
+
if (!button) {
|
|
252
|
+
return /* @__PURE__ */ jsx("div", { className: "aspect-square" }, `empty-${rowIndex}-${colIndex}`);
|
|
253
|
+
}
|
|
254
|
+
const buttonMetric = buttonMetricsLookup[button.id];
|
|
255
|
+
const effort = buttonMetric?.effort || 0;
|
|
256
|
+
const targetPageId = button.targetPageId || button.semanticAction?.targetId;
|
|
257
|
+
const hasLink = targetPageId && tree.pages[targetPageId];
|
|
258
|
+
return /* @__PURE__ */ jsxs(
|
|
259
|
+
"button",
|
|
260
|
+
{
|
|
261
|
+
onClick: () => handleButtonClick(button),
|
|
262
|
+
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",
|
|
263
|
+
style: {
|
|
264
|
+
backgroundColor: button.style?.backgroundColor || "#f3f4f6",
|
|
265
|
+
borderColor: button.style?.borderColor || "#e5e7eb",
|
|
266
|
+
color: button.style?.fontColor || void 0
|
|
267
|
+
},
|
|
268
|
+
children: [
|
|
269
|
+
buttonMetric && showEffortBadges && /* @__PURE__ */ jsx("div", { className: "absolute top-1 right-1 px-1.5 py-0.5 text-xs font-semibold rounded bg-blue-600 text-white shadow-sm", children: effort.toFixed(1) }),
|
|
270
|
+
hasLink && showLinkIndicators && /* @__PURE__ */ jsx("div", { className: "absolute top-1 left-1 w-2 h-2 bg-green-500 rounded-full shadow-sm" }),
|
|
271
|
+
/* @__PURE__ */ jsx(
|
|
272
|
+
"span",
|
|
273
|
+
{
|
|
274
|
+
className: `text-xs sm:text-sm text-center font-medium leading-tight line-clamp-3 ${getTextColor(
|
|
275
|
+
button.style?.backgroundColor
|
|
276
|
+
)}`,
|
|
277
|
+
children: button.label
|
|
278
|
+
}
|
|
279
|
+
),
|
|
280
|
+
button.message && button.message !== button.label && /* @__PURE__ */ jsx(
|
|
281
|
+
"span",
|
|
282
|
+
{
|
|
283
|
+
className: `text-[10px] sm:text-xs text-center opacity-75 line-clamp-2 ${getTextColor(
|
|
284
|
+
button.style?.backgroundColor
|
|
285
|
+
)}`,
|
|
286
|
+
children: button.message
|
|
287
|
+
}
|
|
288
|
+
)
|
|
289
|
+
]
|
|
290
|
+
},
|
|
291
|
+
button.id
|
|
292
|
+
);
|
|
293
|
+
})
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
),
|
|
297
|
+
Object.keys(tree.pages).length > 1 && /* @__PURE__ */ jsx("div", { className: "p-4 border-t border-gray-200 dark:border-gray-700", children: /* @__PURE__ */ jsxs("p", { className: "text-sm text-gray-600 dark:text-gray-400 mb-2", children: [
|
|
298
|
+
Object.keys(tree.pages).length,
|
|
299
|
+
" pages in this vocabulary"
|
|
300
|
+
] }) })
|
|
301
|
+
] })
|
|
302
|
+
] })
|
|
303
|
+
] });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/hooks/useAACFile.ts
|
|
307
|
+
import { useState as useState2, useEffect, useCallback as useCallback2 } from "react";
|
|
308
|
+
|
|
309
|
+
// src/utils/loaders.ts
|
|
310
|
+
async function importProcessors() {
|
|
311
|
+
return import("@willwade/aac-processors");
|
|
312
|
+
}
|
|
313
|
+
async function getProcessorForFile(filepath, options) {
|
|
314
|
+
const {
|
|
315
|
+
getProcessor,
|
|
316
|
+
GridsetProcessor,
|
|
317
|
+
SnapProcessor,
|
|
318
|
+
TouchChatProcessor,
|
|
319
|
+
ObfProcessor,
|
|
320
|
+
ApplePanelsProcessor,
|
|
321
|
+
AstericsGridProcessor,
|
|
322
|
+
OpmlProcessor,
|
|
323
|
+
ExcelProcessor,
|
|
324
|
+
DotProcessor
|
|
325
|
+
} = await importProcessors();
|
|
326
|
+
const ext = filepath.toLowerCase();
|
|
327
|
+
if (ext.endsWith(".gridset")) {
|
|
328
|
+
return new GridsetProcessor();
|
|
329
|
+
}
|
|
330
|
+
if (ext.endsWith(".sps") || ext.endsWith(".spb")) {
|
|
331
|
+
return options ? new SnapProcessor(null, options) : new SnapProcessor();
|
|
332
|
+
}
|
|
333
|
+
if (ext.endsWith(".ce")) {
|
|
334
|
+
return new TouchChatProcessor();
|
|
335
|
+
}
|
|
336
|
+
if (ext.endsWith(".obf")) {
|
|
337
|
+
return new ObfProcessor();
|
|
338
|
+
}
|
|
339
|
+
if (ext.endsWith(".obz")) {
|
|
340
|
+
return new ObfProcessor();
|
|
341
|
+
}
|
|
342
|
+
if (ext.endsWith(".grd")) {
|
|
343
|
+
return new AstericsGridProcessor();
|
|
344
|
+
}
|
|
345
|
+
if (ext.endsWith(".plist")) {
|
|
346
|
+
return new ApplePanelsProcessor();
|
|
347
|
+
}
|
|
348
|
+
if (ext.endsWith(".opml")) {
|
|
349
|
+
return new OpmlProcessor();
|
|
350
|
+
}
|
|
351
|
+
if (ext.endsWith(".xlsx") || ext.endsWith(".xls")) {
|
|
352
|
+
return new ExcelProcessor();
|
|
353
|
+
}
|
|
354
|
+
if (ext.endsWith(".dot")) {
|
|
355
|
+
return new DotProcessor();
|
|
356
|
+
}
|
|
357
|
+
return getProcessor(filepath);
|
|
358
|
+
}
|
|
359
|
+
async function loadAACFile(filepath, options) {
|
|
360
|
+
const processor = await getProcessorForFile(filepath, options);
|
|
361
|
+
return processor.loadIntoTree(filepath);
|
|
362
|
+
}
|
|
363
|
+
async function loadAACFileWithMetadata(filepath, options) {
|
|
364
|
+
const tree = await loadAACFile(filepath, options);
|
|
365
|
+
const ext = filepath.toLowerCase();
|
|
366
|
+
let format = "unknown";
|
|
367
|
+
if (ext.endsWith(".gridset")) format = "gridset";
|
|
368
|
+
else if (ext.endsWith(".sps") || ext.endsWith(".spb")) format = "snap";
|
|
369
|
+
else if (ext.endsWith(".ce")) format = "touchchat";
|
|
370
|
+
else if (ext.endsWith(".obf")) format = "openboard";
|
|
371
|
+
else if (ext.endsWith(".obz")) format = "openboard";
|
|
372
|
+
else if (ext.endsWith(".grd")) format = "asterics-grid";
|
|
373
|
+
else if (ext.endsWith(".plist")) format = "apple-panels";
|
|
374
|
+
else if (ext.endsWith(".opml")) format = "opml";
|
|
375
|
+
else if (ext.endsWith(".xlsx") || ext.endsWith(".xls")) format = "excel";
|
|
376
|
+
else if (ext.endsWith(".dot")) format = "dot";
|
|
377
|
+
return {
|
|
378
|
+
tree,
|
|
379
|
+
format,
|
|
380
|
+
metadata: tree.metadata
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
async function loadAACFileFromURL(url, options) {
|
|
384
|
+
const response = await fetch(url);
|
|
385
|
+
if (!response.ok) {
|
|
386
|
+
throw new Error(`Failed to load file: ${response.statusText}`);
|
|
387
|
+
}
|
|
388
|
+
const blob = await response.blob();
|
|
389
|
+
const filename = getFilenameFromURL(url);
|
|
390
|
+
return loadAACFileFromFile(blob, filename, options);
|
|
391
|
+
}
|
|
392
|
+
async function loadAACFileFromFile(_file, _filename, _options) {
|
|
393
|
+
throw new Error("Client-side file loading not yet fully implemented. Please use server-side loading or loadAACFileFromURL with proper CORS headers.");
|
|
394
|
+
}
|
|
395
|
+
function getFilenameFromURL(url) {
|
|
396
|
+
try {
|
|
397
|
+
const urlObj = new URL(url);
|
|
398
|
+
const pathname = urlObj.pathname;
|
|
399
|
+
const parts = pathname.split("/");
|
|
400
|
+
return parts[parts.length - 1] || "unknown";
|
|
401
|
+
} catch {
|
|
402
|
+
return "unknown";
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
async function calculateMetrics(tree, options = {}) {
|
|
406
|
+
const { MetricsCalculator } = await import("@willwade/aac-processors");
|
|
407
|
+
const calculator = new MetricsCalculator();
|
|
408
|
+
let metricsOptions = {};
|
|
409
|
+
if (options.accessMethod === "scanning" && options.scanningConfig) {
|
|
410
|
+
const { CellScanningOrder, ScanningSelectionMethod } = await import("@willwade/aac-processors");
|
|
411
|
+
let cellScanningOrder = CellScanningOrder.SimpleScan;
|
|
412
|
+
let blockScanEnabled = false;
|
|
413
|
+
switch (options.scanningConfig.pattern) {
|
|
414
|
+
case "linear":
|
|
415
|
+
cellScanningOrder = CellScanningOrder.SimpleScan;
|
|
416
|
+
break;
|
|
417
|
+
case "row-column":
|
|
418
|
+
cellScanningOrder = CellScanningOrder.RowColumnScan;
|
|
419
|
+
break;
|
|
420
|
+
case "block":
|
|
421
|
+
cellScanningOrder = CellScanningOrder.RowColumnScan;
|
|
422
|
+
blockScanEnabled = true;
|
|
423
|
+
break;
|
|
424
|
+
}
|
|
425
|
+
metricsOptions = {
|
|
426
|
+
scanningConfig: {
|
|
427
|
+
cellScanningOrder,
|
|
428
|
+
blockScanEnabled,
|
|
429
|
+
selectionMethod: ScanningSelectionMethod.AutoScan,
|
|
430
|
+
errorCorrectionEnabled: options.scanningConfig.errorCorrection || false,
|
|
431
|
+
errorRate: 0.1
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
const metricsResult = calculator.analyze(tree, metricsOptions);
|
|
436
|
+
return metricsResult.buttons.map((btn) => ({
|
|
437
|
+
id: btn.id,
|
|
438
|
+
label: btn.label,
|
|
439
|
+
effort: btn.effort,
|
|
440
|
+
count: btn.count ?? 0,
|
|
441
|
+
is_word: true,
|
|
442
|
+
level: btn.level,
|
|
443
|
+
semantic_id: btn.semantic_id,
|
|
444
|
+
clone_id: btn.clone_id
|
|
445
|
+
}));
|
|
446
|
+
}
|
|
447
|
+
function getSupportedFormats() {
|
|
448
|
+
return [
|
|
449
|
+
{
|
|
450
|
+
name: "Grid 3",
|
|
451
|
+
extensions: [".gridset"],
|
|
452
|
+
description: "Smartbox Grid 3 communication boards"
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
name: "TD Snap",
|
|
456
|
+
extensions: [".sps", ".spb"],
|
|
457
|
+
description: "Tobii Dynavox Snap files"
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
name: "TouchChat",
|
|
461
|
+
extensions: [".ce"],
|
|
462
|
+
description: "Saltillo TouchChat files"
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
name: "OpenBoard",
|
|
466
|
+
extensions: [".obf", ".obz"],
|
|
467
|
+
description: "OpenBoard Format (OBZ/OBF)"
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
name: "Asterics Grid",
|
|
471
|
+
extensions: [".grd"],
|
|
472
|
+
description: "Asterics Grid files (.grd)"
|
|
473
|
+
},
|
|
474
|
+
{
|
|
475
|
+
name: "Apple Panels",
|
|
476
|
+
extensions: [".plist"],
|
|
477
|
+
description: "Apple iOS Panels files"
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
name: "OPML",
|
|
481
|
+
extensions: [".opml"],
|
|
482
|
+
description: "OPML outline files"
|
|
483
|
+
},
|
|
484
|
+
{
|
|
485
|
+
name: "Excel",
|
|
486
|
+
extensions: [".xlsx", ".xls"],
|
|
487
|
+
description: "Excel spreadsheet boards"
|
|
488
|
+
},
|
|
489
|
+
{
|
|
490
|
+
name: "DOT",
|
|
491
|
+
extensions: [".dot"],
|
|
492
|
+
description: "DOT graph visualization files"
|
|
493
|
+
}
|
|
494
|
+
];
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// src/hooks/useAACFile.ts
|
|
498
|
+
function useAACFile(url, options) {
|
|
499
|
+
const [tree, setTree] = useState2(null);
|
|
500
|
+
const [loading, setLoading] = useState2(true);
|
|
501
|
+
const [error, setError] = useState2(null);
|
|
502
|
+
const load = useCallback2(async () => {
|
|
503
|
+
if (options?.enabled === false) {
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
setLoading(true);
|
|
507
|
+
setError(null);
|
|
508
|
+
try {
|
|
509
|
+
const loadedTree = await loadAACFileFromURL(url, options?.processorOptions);
|
|
510
|
+
setTree(loadedTree);
|
|
511
|
+
} catch (err) {
|
|
512
|
+
setError(err instanceof Error ? err : new Error("Failed to load file"));
|
|
513
|
+
} finally {
|
|
514
|
+
setLoading(false);
|
|
515
|
+
}
|
|
516
|
+
}, [url, options]);
|
|
517
|
+
useEffect(() => {
|
|
518
|
+
load();
|
|
519
|
+
}, [load]);
|
|
520
|
+
return {
|
|
521
|
+
tree,
|
|
522
|
+
loading,
|
|
523
|
+
error,
|
|
524
|
+
reload: load
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function useAACFileWithMetrics(url, metricsOptions, fileOptions) {
|
|
528
|
+
const [tree, setTree] = useState2(null);
|
|
529
|
+
const [metrics, setMetrics] = useState2(null);
|
|
530
|
+
const [loading, setLoading] = useState2(true);
|
|
531
|
+
const [error, setError] = useState2(null);
|
|
532
|
+
const load = useCallback2(async () => {
|
|
533
|
+
if (fileOptions?.enabled === false) {
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
setLoading(true);
|
|
537
|
+
setError(null);
|
|
538
|
+
try {
|
|
539
|
+
const loadedTree = await loadAACFileFromURL(url, fileOptions?.processorOptions);
|
|
540
|
+
setTree(loadedTree);
|
|
541
|
+
const calculatedMetrics = await calculateMetrics(loadedTree, metricsOptions || {});
|
|
542
|
+
setMetrics(calculatedMetrics);
|
|
543
|
+
} catch (err) {
|
|
544
|
+
setError(err instanceof Error ? err : new Error("Failed to load file"));
|
|
545
|
+
} finally {
|
|
546
|
+
setLoading(false);
|
|
547
|
+
}
|
|
548
|
+
}, [url, metricsOptions, fileOptions]);
|
|
549
|
+
useEffect(() => {
|
|
550
|
+
load();
|
|
551
|
+
}, [load]);
|
|
552
|
+
return {
|
|
553
|
+
tree,
|
|
554
|
+
metrics,
|
|
555
|
+
loading,
|
|
556
|
+
error,
|
|
557
|
+
reload: load
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
function useMetrics(tree, options) {
|
|
561
|
+
const [metrics, setMetrics] = useState2(null);
|
|
562
|
+
const [loading, setLoading] = useState2(false);
|
|
563
|
+
const [error, setError] = useState2(null);
|
|
564
|
+
const calculate = useCallback2(async () => {
|
|
565
|
+
if (!tree) {
|
|
566
|
+
setMetrics(null);
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
setLoading(true);
|
|
570
|
+
setError(null);
|
|
571
|
+
try {
|
|
572
|
+
const calculatedMetrics = await calculateMetrics(tree, options || {});
|
|
573
|
+
setMetrics(calculatedMetrics);
|
|
574
|
+
} catch (err) {
|
|
575
|
+
setError(err instanceof Error ? err : new Error("Failed to calculate metrics"));
|
|
576
|
+
} finally {
|
|
577
|
+
setLoading(false);
|
|
578
|
+
}
|
|
579
|
+
}, [tree, options]);
|
|
580
|
+
useEffect(() => {
|
|
581
|
+
calculate();
|
|
582
|
+
}, [calculate]);
|
|
583
|
+
return {
|
|
584
|
+
metrics,
|
|
585
|
+
loading,
|
|
586
|
+
error,
|
|
587
|
+
recalculate: calculate
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function useSentenceBuilder() {
|
|
591
|
+
const [message, setMessage] = useState2("");
|
|
592
|
+
const [wordCount, setWordCount] = useState2(0);
|
|
593
|
+
const [effort, setEffort] = useState2(0);
|
|
594
|
+
const addWord = useCallback2((word, wordEffort = 1) => {
|
|
595
|
+
setMessage((prev) => {
|
|
596
|
+
const newMessage = prev + (prev ? " " : "") + word;
|
|
597
|
+
setWordCount((prev2) => prev2 + 1);
|
|
598
|
+
setEffort((prev2) => prev2 + wordEffort);
|
|
599
|
+
return newMessage;
|
|
600
|
+
});
|
|
601
|
+
}, []);
|
|
602
|
+
const clear = useCallback2(() => {
|
|
603
|
+
setMessage("");
|
|
604
|
+
setWordCount(0);
|
|
605
|
+
setEffort(0);
|
|
606
|
+
}, []);
|
|
607
|
+
return {
|
|
608
|
+
message,
|
|
609
|
+
wordCount,
|
|
610
|
+
effort,
|
|
611
|
+
addWord,
|
|
612
|
+
clear
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
export {
|
|
616
|
+
BoardViewer,
|
|
617
|
+
calculateMetrics,
|
|
618
|
+
getSupportedFormats,
|
|
619
|
+
loadAACFile,
|
|
620
|
+
loadAACFileFromFile,
|
|
621
|
+
loadAACFileFromURL,
|
|
622
|
+
loadAACFileWithMetadata,
|
|
623
|
+
useAACFile,
|
|
624
|
+
useAACFileWithMetrics,
|
|
625
|
+
useMetrics,
|
|
626
|
+
useSentenceBuilder
|
|
627
|
+
};
|
|
628
|
+
//# sourceMappingURL=index.mjs.map
|