@willwade/aac-processors 0.0.15 → 0.0.16
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/README.md +17 -8
- package/dist/core/treeStructure.d.ts +1 -0
- package/dist/processors/gridset/commands.js +15 -0
- package/dist/processors/gridset/pluginTypes.js +4 -4
- package/dist/processors/gridsetProcessor.d.ts +4 -0
- package/dist/processors/gridsetProcessor.js +315 -47
- package/dist/processors/snapProcessor.js +105 -9
- package/dist/processors/touchchatProcessor.js +33 -13
- package/dist/types/aac.d.ts +13 -3
- package/dist/types/aac.js +6 -2
- package/dist/utilities/analytics/metrics/comparison.d.ts +1 -0
- package/dist/utilities/analytics/metrics/comparison.js +52 -24
- package/dist/utilities/analytics/metrics/core.d.ts +7 -2
- package/dist/utilities/analytics/metrics/core.js +327 -197
- package/dist/utilities/analytics/metrics/effort.d.ts +8 -3
- package/dist/utilities/analytics/metrics/effort.js +10 -5
- package/dist/utilities/analytics/metrics/sentence.js +17 -4
- package/dist/utilities/analytics/metrics/types.d.ts +39 -0
- package/dist/utilities/analytics/metrics/vocabulary.js +1 -1
- package/dist/utilities/analytics/reference/index.js +12 -1
- package/dist/utilities/translation/translationProcessor.d.ts +2 -1
- package/dist/utilities/translation/translationProcessor.js +5 -2
- package/package.json +1 -1
|
@@ -12,6 +12,17 @@ const path_1 = __importDefault(require("path"));
|
|
|
12
12
|
const fs_1 = __importDefault(require("fs"));
|
|
13
13
|
const crypto_1 = __importDefault(require("crypto"));
|
|
14
14
|
const snapValidator_1 = require("../validation/snapValidator");
|
|
15
|
+
/**
|
|
16
|
+
* Map Snap Visible value to AAC standard visibility
|
|
17
|
+
* Snap: 0 = hidden, 1 (or non-zero) = visible
|
|
18
|
+
* Maps to: 'Hidden' | 'Visible' | undefined
|
|
19
|
+
*/
|
|
20
|
+
function mapSnapVisibility(visible) {
|
|
21
|
+
if (visible === null || visible === undefined) {
|
|
22
|
+
return undefined; // Default to visible
|
|
23
|
+
}
|
|
24
|
+
return visible === 0 ? 'Hidden' : 'Visible';
|
|
25
|
+
}
|
|
15
26
|
class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
16
27
|
constructor(symbolResolver = null, options = {}) {
|
|
17
28
|
super(options);
|
|
@@ -63,6 +74,40 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
63
74
|
};
|
|
64
75
|
// Load pages first, using UniqueId as canonical id
|
|
65
76
|
const pages = db.prepare('SELECT * FROM Page').all();
|
|
77
|
+
// Load PageSetProperties to find default Keyboard and Home pages
|
|
78
|
+
let defaultKeyboardPageId;
|
|
79
|
+
let defaultHomePageId;
|
|
80
|
+
let dashboardPageId;
|
|
81
|
+
try {
|
|
82
|
+
const properties = db.prepare('SELECT * FROM PageSetProperties').get();
|
|
83
|
+
if (properties) {
|
|
84
|
+
defaultKeyboardPageId = properties.DefaultKeyboardPageUniqueId;
|
|
85
|
+
defaultHomePageId = properties.DefaultHomePageUniqueId;
|
|
86
|
+
dashboardPageId = properties.DashboardUniqueId;
|
|
87
|
+
const toolbarId = properties.ToolBarUniqueId;
|
|
88
|
+
const hasGlobalToolbar = toolbarId && toolbarId !== '00000000-0000-0000-0000-000000000000';
|
|
89
|
+
if (hasGlobalToolbar) {
|
|
90
|
+
tree.rootId = toolbarId;
|
|
91
|
+
}
|
|
92
|
+
else if (defaultHomePageId) {
|
|
93
|
+
tree.rootId = defaultHomePageId;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
console.warn('[SnapProcessor] Failed to load PageSetProperties:', e);
|
|
99
|
+
}
|
|
100
|
+
// If still no root, look for a page titled "Tool Bar" or similar
|
|
101
|
+
if (!tree.rootId || tree.rootId === defaultHomePageId) {
|
|
102
|
+
const toolbarPage = pages.find((p) => p.Title === 'Tool Bar' || p.Name === 'Tool Bar');
|
|
103
|
+
if (toolbarPage) {
|
|
104
|
+
tree.rootId = String(toolbarPage.UniqueId || toolbarPage.Id);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// If still no root, fallback to first page
|
|
108
|
+
if (!tree.rootId && pages.length > 0) {
|
|
109
|
+
tree.rootId = String(pages[0].UniqueId || pages[0].Id);
|
|
110
|
+
}
|
|
66
111
|
// Map from numeric Id -> UniqueId for later lookup
|
|
67
112
|
const idToUniqueId = {};
|
|
68
113
|
pages.forEach((pageRow) => {
|
|
@@ -219,6 +264,7 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
219
264
|
buttonColumns.has('LabelColor') ? 'b.LabelColor' : 'NULL AS LabelColor',
|
|
220
265
|
buttonColumns.has('BackgroundColor') ? 'b.BackgroundColor' : 'NULL AS BackgroundColor',
|
|
221
266
|
buttonColumns.has('NavigatePageId') ? 'b.NavigatePageId' : 'NULL AS NavigatePageId',
|
|
267
|
+
buttonColumns.has('ContentType') ? 'b.ContentType' : 'NULL AS ContentType',
|
|
222
268
|
];
|
|
223
269
|
if (this.loadAudio) {
|
|
224
270
|
selectFields.push(buttonColumns.has('MessageRecordingId')
|
|
@@ -232,14 +278,30 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
232
278
|
: 'NULL AS SerializedMessageSoundMetadata');
|
|
233
279
|
}
|
|
234
280
|
const placementColumns = getTableColumns('ElementPlacement');
|
|
235
|
-
|
|
281
|
+
const hasButtonPageLink = getTableColumns('ButtonPageLink').size > 0;
|
|
282
|
+
selectFields.push(placementColumns.has('GridPosition') ? 'ep.GridPosition' : 'NULL AS GridPosition', placementColumns.has('PageLayoutId') ? 'ep.PageLayoutId' : 'NULL AS PageLayoutId', placementColumns.has('Visible') ? 'ep.Visible' : 'NULL AS Visible', 'er.PageId as ButtonPageId');
|
|
283
|
+
if (hasButtonPageLink) {
|
|
284
|
+
selectFields.push('bpl.PageUniqueId AS LinkedPageUniqueId');
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
selectFields.push('NULL AS LinkedPageUniqueId');
|
|
288
|
+
}
|
|
289
|
+
const hasCommandSequence = getTableColumns('CommandSequence').size > 0;
|
|
290
|
+
if (hasCommandSequence) {
|
|
291
|
+
selectFields.push('cs.SerializedCommands');
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
selectFields.push('NULL AS SerializedCommands');
|
|
295
|
+
}
|
|
236
296
|
const buttonQuery = `
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
297
|
+
SELECT ${selectFields.join(', ')}
|
|
298
|
+
FROM Button b
|
|
299
|
+
INNER JOIN ElementReference er ON b.ElementReferenceId = er.Id
|
|
300
|
+
LEFT JOIN ElementPlacement ep ON ep.ElementReferenceId = er.Id
|
|
301
|
+
${hasButtonPageLink ? 'LEFT JOIN ButtonPageLink bpl ON b.Id = bpl.ButtonId' : ''}
|
|
302
|
+
${hasCommandSequence ? 'LEFT JOIN CommandSequence cs ON b.Id = cs.ButtonId' : ''}
|
|
303
|
+
WHERE er.PageId = ? ${selectedPageLayoutId ? 'AND ep.PageLayoutId = ?' : ''}
|
|
304
|
+
`;
|
|
243
305
|
const queryParams = selectedPageLayoutId
|
|
244
306
|
? [pageRow.Id, selectedPageLayoutId]
|
|
245
307
|
: [pageRow.Id];
|
|
@@ -279,9 +341,40 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
279
341
|
if (btnRow.NavigatePageId && idToUniqueId[String(btnRow.NavigatePageId)]) {
|
|
280
342
|
targetPageUniqueId = idToUniqueId[String(btnRow.NavigatePageId)];
|
|
281
343
|
}
|
|
344
|
+
else if (btnRow.LinkedPageUniqueId) {
|
|
345
|
+
targetPageUniqueId = String(btnRow.LinkedPageUniqueId);
|
|
346
|
+
}
|
|
282
347
|
else if (btnRow.PageUniqueId) {
|
|
283
348
|
targetPageUniqueId = String(btnRow.PageUniqueId);
|
|
284
349
|
}
|
|
350
|
+
// Parse CommandSequence for navigation targets if not found yet
|
|
351
|
+
if (btnRow.SerializedCommands) {
|
|
352
|
+
try {
|
|
353
|
+
const commands = JSON.parse(btnRow.SerializedCommands);
|
|
354
|
+
const values = commands.$values || [];
|
|
355
|
+
for (const cmd of values) {
|
|
356
|
+
if (cmd.$type === '2' && cmd.LinkedPageId) {
|
|
357
|
+
// Normal Navigation
|
|
358
|
+
targetPageUniqueId = String(cmd.LinkedPageId);
|
|
359
|
+
}
|
|
360
|
+
else if (cmd.$type === '16') {
|
|
361
|
+
// Go to Home
|
|
362
|
+
targetPageUniqueId = defaultHomePageId;
|
|
363
|
+
}
|
|
364
|
+
else if (cmd.$type === '17') {
|
|
365
|
+
// Go to Keyboard
|
|
366
|
+
targetPageUniqueId = defaultKeyboardPageId;
|
|
367
|
+
}
|
|
368
|
+
else if (cmd.$type === '18') {
|
|
369
|
+
// Go to Dashboard
|
|
370
|
+
targetPageUniqueId = dashboardPageId;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
catch (e) {
|
|
375
|
+
// Ignore JSON parse errors in commands
|
|
376
|
+
}
|
|
377
|
+
}
|
|
285
378
|
// Determine parent page association for this button
|
|
286
379
|
const parentPageId = btnRow.ButtonPageId ? String(btnRow.ButtonPageId) : undefined;
|
|
287
380
|
const parentUniqueId = parentPageId && idToUniqueId[parentPageId] ? idToUniqueId[parentPageId] : uniqueId;
|
|
@@ -344,11 +437,14 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
344
437
|
}
|
|
345
438
|
const button = new treeStructure_1.AACButton({
|
|
346
439
|
id: String(btnRow.Id),
|
|
347
|
-
label: btnRow.Label || '',
|
|
348
|
-
message: btnRow.Message || btnRow.Label || '',
|
|
440
|
+
label: btnRow.Label || (btnRow.ContentType === 1 ? '[Prediction]' : ''),
|
|
441
|
+
message: btnRow.Message || (btnRow.ContentType === 1 ? '[Prediction]' : btnRow.Label || ''),
|
|
349
442
|
targetPageId: targetPageUniqueId,
|
|
350
443
|
semanticAction: semanticAction,
|
|
444
|
+
contentType: btnRow.ContentType === 1 ? 'AutoContent' : undefined,
|
|
445
|
+
contentSubType: btnRow.ContentType === 1 ? 'Prediction' : undefined,
|
|
351
446
|
audioRecording: audioRecording,
|
|
447
|
+
visibility: mapSnapVisibility(btnRow.Visible),
|
|
352
448
|
semantic_id: btnRow.LibrarySymbolId
|
|
353
449
|
? `snap_symbol_${btnRow.LibrarySymbolId}`
|
|
354
450
|
: undefined, // Extract semantic_id from LibrarySymbolId
|
|
@@ -85,8 +85,20 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
85
85
|
const tree = new treeStructure_1.AACTree();
|
|
86
86
|
// Set root ID to the first page ID (will be updated if we find a better root)
|
|
87
87
|
let rootPageId = null;
|
|
88
|
+
const getTableColumns = (tableName) => {
|
|
89
|
+
if (!db)
|
|
90
|
+
return new Set();
|
|
91
|
+
try {
|
|
92
|
+
const rows = db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
93
|
+
return new Set(rows.map((row) => row.name));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return new Set();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
88
99
|
// Load ID mappings first
|
|
89
100
|
const idMappings = new Map();
|
|
101
|
+
const numericToRid = new Map();
|
|
90
102
|
try {
|
|
91
103
|
const mappingQuery = 'SELECT numeric_id, string_id FROM page_id_mapping';
|
|
92
104
|
const mappings = db.prepare(mappingQuery).all();
|
|
@@ -116,15 +128,18 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
116
128
|
// console.log('No styles found:', e);
|
|
117
129
|
}
|
|
118
130
|
// First, load all pages and get their names from resources
|
|
131
|
+
const resourceColumns = getTableColumns('resources');
|
|
132
|
+
const hasRid = resourceColumns.has('rid');
|
|
119
133
|
const pageQuery = `
|
|
120
|
-
SELECT p.*, r.name
|
|
134
|
+
SELECT p.*, r.name${hasRid ? ', r.rid' : ''}
|
|
121
135
|
FROM pages p
|
|
122
136
|
JOIN resources r ON r.id = p.resource_id
|
|
123
137
|
`;
|
|
124
138
|
const pages = db.prepare(pageQuery).all();
|
|
125
139
|
pages.forEach((pageRow) => {
|
|
126
|
-
// Use
|
|
127
|
-
const pageId = idMappings.get(pageRow.id) || String(pageRow.id);
|
|
140
|
+
// Use resource RID (UUID) if available, otherwise mapped string ID, then numeric ID
|
|
141
|
+
const pageId = (hasRid ? pageRow.rid : null) || idMappings.get(pageRow.id) || String(pageRow.id);
|
|
142
|
+
numericToRid.set(pageRow.id, pageId);
|
|
128
143
|
const style = pageStyles.get(pageRow.page_style_id);
|
|
129
144
|
const page = new treeStructure_1.AACPage({
|
|
130
145
|
id: pageId,
|
|
@@ -166,6 +181,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
166
181
|
touchChat: {
|
|
167
182
|
actionCode: 0, // Default speak action
|
|
168
183
|
actionData: cell.message || cell.label || '',
|
|
184
|
+
resourceId: cell.resource_id,
|
|
169
185
|
},
|
|
170
186
|
},
|
|
171
187
|
fallback: {
|
|
@@ -210,7 +226,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
210
226
|
const pageGrids = new Map();
|
|
211
227
|
boxInstances.forEach((instance) => {
|
|
212
228
|
// Use mapped string ID if available, otherwise use numeric ID as string
|
|
213
|
-
const pageId =
|
|
229
|
+
const pageId = numericToRid.get(instance.page_id) || String(instance.page_id);
|
|
214
230
|
const page = tree.getPage(pageId);
|
|
215
231
|
const buttons = buttonBoxes.get(instance.button_box_id);
|
|
216
232
|
if (page && buttons) {
|
|
@@ -341,7 +357,7 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
341
357
|
},
|
|
342
358
|
});
|
|
343
359
|
// Find the page that references this resource
|
|
344
|
-
const page = Object.values(tree.pages).find((p) => p.id === String(btnRow.id));
|
|
360
|
+
const page = Object.values(tree.pages).find((p) => p.id === (numericToRid.get(btnRow.id) || String(btnRow.id)));
|
|
345
361
|
if (page)
|
|
346
362
|
page.addButton(button);
|
|
347
363
|
});
|
|
@@ -351,23 +367,27 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
|
|
|
351
367
|
}
|
|
352
368
|
// Load navigation actions
|
|
353
369
|
const navActionsQuery = `
|
|
354
|
-
SELECT
|
|
355
|
-
FROM
|
|
356
|
-
JOIN actions a ON a.resource_id = b.resource_id
|
|
370
|
+
SELECT a.resource_id, COALESCE(${hasRid ? 'r_rid.rid, r_id.rid, ' : ''}r_id.id, ad.value) as target_page_id
|
|
371
|
+
FROM actions a
|
|
357
372
|
JOIN action_data ad ON ad.action_id = a.id
|
|
358
|
-
|
|
373
|
+
${hasRid ? 'LEFT JOIN resources r_rid ON r_rid.rid = ad.value AND r_rid.type = 7' : ''}
|
|
374
|
+
LEFT JOIN resources r_id ON (CASE WHEN ad.value GLOB '[0-9]*' THEN CAST(ad.value AS INTEGER) ELSE -1 END) = r_id.id AND r_id.type = 7
|
|
375
|
+
WHERE a.code IN (1, 8, 9)
|
|
359
376
|
`;
|
|
360
377
|
try {
|
|
361
378
|
const navActions = db.prepare(navActionsQuery).all();
|
|
362
379
|
navActions.forEach((nav) => {
|
|
363
|
-
// Find button in any page
|
|
380
|
+
// Find button in any page by its resourceId
|
|
364
381
|
for (const pageId in tree.pages) {
|
|
365
382
|
const page = tree.pages[pageId];
|
|
366
|
-
const button = page.buttons.find((b) => b.
|
|
383
|
+
const button = page.buttons.find((b) => b.semanticAction?.platformData?.touchChat?.resourceId === nav.resource_id);
|
|
367
384
|
if (button) {
|
|
368
385
|
// Use mapped string ID for target page if available
|
|
369
|
-
const
|
|
370
|
-
|
|
386
|
+
const numericTargetId = parseInt(String(nav.target_page_id));
|
|
387
|
+
const targetPageId = !isNaN(numericTargetId)
|
|
388
|
+
? numericToRid.get(numericTargetId) || String(numericTargetId)
|
|
389
|
+
: String(nav.target_page_id);
|
|
390
|
+
button.targetPageId = targetPageId;
|
|
371
391
|
// Create semantic action for navigation
|
|
372
392
|
button.semanticAction = {
|
|
373
393
|
category: treeStructure_1.AACSemanticCategory.NAVIGATION,
|
package/dist/types/aac.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { AACSemanticAction } from '../core/treeStructure';
|
|
|
4
4
|
* Determines how the scanning advances through items
|
|
5
5
|
*/
|
|
6
6
|
export declare enum ScanningSelectionMethod {
|
|
7
|
-
/** Automatically advance through items at timed intervals */
|
|
7
|
+
/** Automatically advance through items at timed intervals (1 Switch) */
|
|
8
8
|
AutoScan = "AutoScan",
|
|
9
9
|
/** Automatic scanning with overscan (two-stage scanning) */
|
|
10
10
|
AutoScanWithOverscan = "AutoScanWithOverscan",
|
|
@@ -12,8 +12,12 @@ export declare enum ScanningSelectionMethod {
|
|
|
12
12
|
HoldToAdvance = "HoldToAdvance",
|
|
13
13
|
/** Hold to advance with overscan */
|
|
14
14
|
HoldToAdvanceWithOverscan = "HoldToAdvanceWithOverscan",
|
|
15
|
-
/** Tap switch to advance, tap again to select */
|
|
16
|
-
TapToAdvance = "TapToAdvance"
|
|
15
|
+
/** Tap switch to advance, tap again to select (Automatic) */
|
|
16
|
+
TapToAdvance = "TapToAdvance",
|
|
17
|
+
/** Tap switch to advance, another switch to select (2 Switch Step Scan) */
|
|
18
|
+
StepScan2Switch = "StepScan2Switch",
|
|
19
|
+
/** Tap switch 1 to advance, tap switch 1 again to select (1 Switch Step Scan) */
|
|
20
|
+
StepScan1Switch = "StepScan1Switch"
|
|
17
21
|
}
|
|
18
22
|
/**
|
|
19
23
|
* Cell scanning order patterns
|
|
@@ -48,6 +52,12 @@ export interface ScanningConfig {
|
|
|
48
52
|
dwellTime?: number;
|
|
49
53
|
/** How the selection is accepted */
|
|
50
54
|
acceptScanMethod?: 'Switch' | 'Timeout' | 'Hold';
|
|
55
|
+
/** Whether to factor in error correction effort (e.g., missed hits) */
|
|
56
|
+
errorCorrectionEnabled?: boolean;
|
|
57
|
+
/** Maximum number of loops before the scan times out */
|
|
58
|
+
maxLoops?: number;
|
|
59
|
+
/** Estimated probability of missing a target (0.0 to 1.0) */
|
|
60
|
+
errorRate?: number;
|
|
51
61
|
}
|
|
52
62
|
export interface AACStyle {
|
|
53
63
|
backgroundColor?: string;
|
package/dist/types/aac.js
CHANGED
|
@@ -7,7 +7,7 @@ exports.CellScanningOrder = exports.ScanningSelectionMethod = void 0;
|
|
|
7
7
|
*/
|
|
8
8
|
var ScanningSelectionMethod;
|
|
9
9
|
(function (ScanningSelectionMethod) {
|
|
10
|
-
/** Automatically advance through items at timed intervals */
|
|
10
|
+
/** Automatically advance through items at timed intervals (1 Switch) */
|
|
11
11
|
ScanningSelectionMethod["AutoScan"] = "AutoScan";
|
|
12
12
|
/** Automatic scanning with overscan (two-stage scanning) */
|
|
13
13
|
ScanningSelectionMethod["AutoScanWithOverscan"] = "AutoScanWithOverscan";
|
|
@@ -15,8 +15,12 @@ var ScanningSelectionMethod;
|
|
|
15
15
|
ScanningSelectionMethod["HoldToAdvance"] = "HoldToAdvance";
|
|
16
16
|
/** Hold to advance with overscan */
|
|
17
17
|
ScanningSelectionMethod["HoldToAdvanceWithOverscan"] = "HoldToAdvanceWithOverscan";
|
|
18
|
-
/** Tap switch to advance, tap again to select */
|
|
18
|
+
/** Tap switch to advance, tap again to select (Automatic) */
|
|
19
19
|
ScanningSelectionMethod["TapToAdvance"] = "TapToAdvance";
|
|
20
|
+
/** Tap switch to advance, another switch to select (2 Switch Step Scan) */
|
|
21
|
+
ScanningSelectionMethod["StepScan2Switch"] = "StepScan2Switch";
|
|
22
|
+
/** Tap switch 1 to advance, tap switch 1 again to select (1 Switch Step Scan) */
|
|
23
|
+
ScanningSelectionMethod["StepScan1Switch"] = "StepScan1Switch";
|
|
20
24
|
})(ScanningSelectionMethod || (exports.ScanningSelectionMethod = ScanningSelectionMethod = {}));
|
|
21
25
|
/**
|
|
22
26
|
* Cell scanning order patterns
|
|
@@ -16,25 +16,33 @@ class ComparisonAnalyzer {
|
|
|
16
16
|
this.sentenceAnalyzer = new sentence_1.SentenceAnalyzer();
|
|
17
17
|
this.referenceLoader = new index_1.ReferenceLoader();
|
|
18
18
|
}
|
|
19
|
+
normalize(word) {
|
|
20
|
+
return word
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.trim()
|
|
23
|
+
.replace(/[.?!,]/g, '');
|
|
24
|
+
}
|
|
19
25
|
/**
|
|
20
26
|
* Compare two board sets
|
|
21
27
|
*/
|
|
22
28
|
compare(targetResult, compareResult, options) {
|
|
23
29
|
// Create base result from target
|
|
24
30
|
const baseResult = { ...targetResult };
|
|
25
|
-
// Create word maps
|
|
31
|
+
// Create word maps with normalized keys
|
|
26
32
|
const targetWords = new Map();
|
|
27
33
|
targetResult.buttons.forEach((btn) => {
|
|
28
|
-
const
|
|
34
|
+
const key = this.normalize(btn.label);
|
|
35
|
+
const existing = targetWords.get(key);
|
|
29
36
|
if (!existing || btn.effort < existing.effort) {
|
|
30
|
-
targetWords.set(
|
|
37
|
+
targetWords.set(key, btn);
|
|
31
38
|
}
|
|
32
39
|
});
|
|
33
40
|
const compareWords = new Map();
|
|
34
41
|
compareResult.buttons.forEach((btn) => {
|
|
35
|
-
const
|
|
42
|
+
const key = this.normalize(btn.label);
|
|
43
|
+
const existing = compareWords.get(key);
|
|
36
44
|
if (!existing || btn.effort < existing.effort) {
|
|
37
|
-
compareWords.set(
|
|
45
|
+
compareWords.set(key, btn);
|
|
38
46
|
}
|
|
39
47
|
});
|
|
40
48
|
// Find missing/extra/overlapping words
|
|
@@ -62,7 +70,8 @@ class ComparisonAnalyzer {
|
|
|
62
70
|
overlappingWords.sort((a, b) => a.localeCompare(b));
|
|
63
71
|
// Add comparison metrics to buttons
|
|
64
72
|
const enrichedButtons = targetResult.buttons.map((btn) => {
|
|
65
|
-
const
|
|
73
|
+
const key = this.normalize(btn.label);
|
|
74
|
+
const compBtn = compareWords.get(key);
|
|
66
75
|
return {
|
|
67
76
|
...btn,
|
|
68
77
|
comp_level: compBtn?.level,
|
|
@@ -124,8 +133,9 @@ class ComparisonAnalyzer {
|
|
|
124
133
|
let targetCovered = 0;
|
|
125
134
|
let compareCovered = 0;
|
|
126
135
|
list.words.forEach((word) => {
|
|
127
|
-
const
|
|
128
|
-
const
|
|
136
|
+
const key = this.normalize(word);
|
|
137
|
+
const targetBtn = targetWords.get(key);
|
|
138
|
+
const compareBtn = compareWords.get(key);
|
|
129
139
|
if (targetBtn) {
|
|
130
140
|
targetCovered++;
|
|
131
141
|
targetTotal += targetBtn.effort;
|
|
@@ -140,6 +150,9 @@ class ComparisonAnalyzer {
|
|
|
140
150
|
list: list.words,
|
|
141
151
|
average_effort: targetCovered > 0 ? targetTotal / targetCovered : 0,
|
|
142
152
|
comp_effort: compareCovered > 0 ? compareTotal / compareCovered : 0,
|
|
153
|
+
target_covered: targetCovered,
|
|
154
|
+
compare_covered: compareCovered,
|
|
155
|
+
total_words: list.words.length,
|
|
143
156
|
};
|
|
144
157
|
});
|
|
145
158
|
// Analyze missing from specific lists
|
|
@@ -147,7 +160,8 @@ class ComparisonAnalyzer {
|
|
|
147
160
|
coreLists.forEach((list) => {
|
|
148
161
|
const listMissing = [];
|
|
149
162
|
list.words.forEach((word) => {
|
|
150
|
-
|
|
163
|
+
const key = this.normalize(word);
|
|
164
|
+
if (!targetWords.has(key)) {
|
|
151
165
|
listMissing.push(word);
|
|
152
166
|
}
|
|
153
167
|
});
|
|
@@ -172,6 +186,13 @@ class ComparisonAnalyzer {
|
|
|
172
186
|
comp_words: compareResult.total_words,
|
|
173
187
|
comp_grid: compareResult.grid,
|
|
174
188
|
comp_effort_score: this.calculateEffortScore(compareResult),
|
|
189
|
+
comp_spelling_effort_base: compareResult.spelling_effort_base,
|
|
190
|
+
comp_spelling_effort_per_letter: compareResult.spelling_effort_per_letter,
|
|
191
|
+
comp_spelling_page_id: compareResult.spelling_page_id,
|
|
192
|
+
has_dynamic_prediction: targetResult.has_dynamic_prediction,
|
|
193
|
+
prediction_page_id: targetResult.prediction_page_id,
|
|
194
|
+
comp_has_dynamic_prediction: compareResult.has_dynamic_prediction,
|
|
195
|
+
comp_prediction_page_id: compareResult.prediction_page_id,
|
|
175
196
|
// Vocabulary comparison
|
|
176
197
|
missing_words: missingWords,
|
|
177
198
|
extra_words: extraWords,
|
|
@@ -196,19 +217,21 @@ class ComparisonAnalyzer {
|
|
|
196
217
|
* Calculate CARE component scores
|
|
197
218
|
*/
|
|
198
219
|
calculateCareComponents(targetResult, compareResult, _overlappingWords) {
|
|
199
|
-
// Create word maps
|
|
220
|
+
// Create word maps with normalized keys
|
|
200
221
|
const targetWords = new Map();
|
|
201
222
|
targetResult.buttons.forEach((btn) => {
|
|
202
|
-
const
|
|
223
|
+
const key = this.normalize(btn.label);
|
|
224
|
+
const existing = targetWords.get(key);
|
|
203
225
|
if (!existing || btn.effort < existing.effort) {
|
|
204
|
-
targetWords.set(
|
|
226
|
+
targetWords.set(key, btn);
|
|
205
227
|
}
|
|
206
228
|
});
|
|
207
229
|
const compareWords = new Map();
|
|
208
230
|
compareResult.buttons.forEach((btn) => {
|
|
209
|
-
const
|
|
231
|
+
const key = this.normalize(btn.label);
|
|
232
|
+
const existing = compareWords.get(key);
|
|
210
233
|
if (!existing || btn.effort < existing.effort) {
|
|
211
|
-
compareWords.set(
|
|
234
|
+
compareWords.set(key, btn);
|
|
212
235
|
}
|
|
213
236
|
});
|
|
214
237
|
// Load reference data
|
|
@@ -223,9 +246,10 @@ class ComparisonAnalyzer {
|
|
|
223
246
|
list.words.forEach((word) => allCoreWords.add(word.toLowerCase()));
|
|
224
247
|
});
|
|
225
248
|
allCoreWords.forEach((word) => {
|
|
226
|
-
|
|
249
|
+
const key = this.normalize(word);
|
|
250
|
+
if (targetWords.has(key))
|
|
227
251
|
coreCount++;
|
|
228
|
-
if (compareWords.has(
|
|
252
|
+
if (compareWords.has(key))
|
|
229
253
|
compCoreCount++;
|
|
230
254
|
});
|
|
231
255
|
// Calculate sentence construction effort
|
|
@@ -234,8 +258,9 @@ class ComparisonAnalyzer {
|
|
|
234
258
|
let sentenceWordCount = 0;
|
|
235
259
|
sentences.forEach((words) => {
|
|
236
260
|
words.forEach((word) => {
|
|
237
|
-
const
|
|
238
|
-
const
|
|
261
|
+
const key = this.normalize(word);
|
|
262
|
+
const targetBtn = targetWords.get(key);
|
|
263
|
+
const compareBtn = compareWords.get(key);
|
|
239
264
|
if (targetBtn) {
|
|
240
265
|
sentenceEffort += targetBtn.effort;
|
|
241
266
|
}
|
|
@@ -258,8 +283,9 @@ class ComparisonAnalyzer {
|
|
|
258
283
|
let compFringeCount = 0;
|
|
259
284
|
let commonFringeCount = 0;
|
|
260
285
|
fringe.forEach((word) => {
|
|
261
|
-
const
|
|
262
|
-
const
|
|
286
|
+
const key = this.normalize(word);
|
|
287
|
+
const inTarget = targetWords.has(key);
|
|
288
|
+
const inCompare = compareWords.has(key);
|
|
263
289
|
if (inTarget)
|
|
264
290
|
fringeCount++;
|
|
265
291
|
if (inCompare)
|
|
@@ -285,8 +311,9 @@ class ComparisonAnalyzer {
|
|
|
285
311
|
const fringe = this.referenceLoader.loadFringe();
|
|
286
312
|
const result = [];
|
|
287
313
|
fringe.forEach((word) => {
|
|
288
|
-
const
|
|
289
|
-
const
|
|
314
|
+
const key = this.normalize(word);
|
|
315
|
+
const targetBtn = targetWords.get(key);
|
|
316
|
+
const compareBtn = compareWords.get(key);
|
|
290
317
|
if (targetBtn) {
|
|
291
318
|
result.push({
|
|
292
319
|
word,
|
|
@@ -305,8 +332,9 @@ class ComparisonAnalyzer {
|
|
|
305
332
|
const fringe = this.referenceLoader.loadFringe();
|
|
306
333
|
const result = [];
|
|
307
334
|
fringe.forEach((word) => {
|
|
308
|
-
const
|
|
309
|
-
const
|
|
335
|
+
const key = this.normalize(word);
|
|
336
|
+
const targetBtn = targetWords.get(key);
|
|
337
|
+
const compareBtn = compareWords.get(key);
|
|
310
338
|
if (targetBtn && compareBtn) {
|
|
311
339
|
result.push({
|
|
312
340
|
word,
|
|
@@ -7,16 +7,21 @@
|
|
|
7
7
|
* Based on: aac-metrics/lib/aac-metrics/metrics.rb
|
|
8
8
|
*/
|
|
9
9
|
import { AACTree } from '../../../core/treeStructure';
|
|
10
|
-
import { MetricsResult } from './types';
|
|
10
|
+
import { MetricsOptions, MetricsResult } from './types';
|
|
11
11
|
export declare class MetricsCalculator {
|
|
12
12
|
private locale;
|
|
13
13
|
/**
|
|
14
14
|
* Main analysis function - calculates metrics for an AAC tree
|
|
15
15
|
*
|
|
16
16
|
* @param tree - The AAC tree to analyze
|
|
17
|
+
* @param options - Optional configuration for metrics calculation
|
|
17
18
|
* @returns Complete metrics result
|
|
18
19
|
*/
|
|
19
|
-
analyze(tree: AACTree): MetricsResult;
|
|
20
|
+
analyze(tree: AACTree, options?: MetricsOptions): MetricsResult;
|
|
21
|
+
/**
|
|
22
|
+
* Identify keyboard/spelling page and calculate base/avg effort
|
|
23
|
+
*/
|
|
24
|
+
private identifySpellingMetrics;
|
|
20
25
|
/**
|
|
21
26
|
* Build reference maps for semantic_id and clone_id frequencies
|
|
22
27
|
*/
|