@willwade/aac-processors 0.1.7 → 0.1.8
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/analytics.d.ts +7 -0
- package/dist/analytics.js +23 -0
- package/dist/browser/index.browser.js +5 -0
- package/dist/browser/metrics.js +17 -0
- package/dist/browser/processors/gridset/helpers.js +390 -0
- package/dist/browser/processors/snap/helpers.js +252 -0
- package/dist/browser/utilities/analytics/history.js +116 -0
- package/dist/browser/utilities/analytics/metrics/comparison.js +477 -0
- package/dist/browser/utilities/analytics/metrics/core.js +775 -0
- package/dist/browser/utilities/analytics/metrics/effort.js +221 -0
- package/dist/browser/utilities/analytics/metrics/obl-types.js +6 -0
- package/dist/browser/utilities/analytics/metrics/obl.js +282 -0
- package/dist/browser/utilities/analytics/metrics/sentence.js +121 -0
- package/dist/browser/utilities/analytics/metrics/types.js +6 -0
- package/dist/browser/utilities/analytics/metrics/vocabulary.js +138 -0
- package/dist/browser/utilities/analytics/reference/browser.js +67 -0
- package/dist/browser/utilities/analytics/reference/index.js +129 -0
- package/dist/browser/utils/dotnetTicks.js +17 -0
- package/dist/index.browser.d.ts +1 -0
- package/dist/index.browser.js +18 -1
- package/dist/index.node.d.ts +2 -2
- package/dist/index.node.js +5 -5
- package/dist/metrics.d.ts +17 -0
- package/dist/metrics.js +44 -0
- package/dist/utilities/analytics/metrics/comparison.d.ts +2 -1
- package/dist/utilities/analytics/metrics/comparison.js +3 -3
- package/dist/utilities/analytics/metrics/vocabulary.d.ts +2 -2
- package/dist/utilities/analytics/reference/browser.d.ts +31 -0
- package/dist/utilities/analytics/reference/browser.js +73 -0
- package/dist/utilities/analytics/reference/index.d.ts +21 -0
- package/dist/utilities/analytics/reference/index.js +22 -46
- package/package.json +9 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Usage Analytics Namespace
|
|
4
|
+
*
|
|
5
|
+
* End-user usage/history utilities (Grid 3, Snap, OBL).
|
|
6
|
+
* This is separate from pageset metrics.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
20
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
21
|
+
};
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
__exportStar(require("./utilities/analytics/history"), exports);
|
|
@@ -28,6 +28,11 @@ export { SnapProcessor } from './processors/snapProcessor';
|
|
|
28
28
|
export { TouchChatProcessor } from './processors/touchchatProcessor';
|
|
29
29
|
export { ApplePanelsProcessor } from './processors/applePanelsProcessor';
|
|
30
30
|
export { AstericsGridProcessor } from './processors/astericsGridProcessor';
|
|
31
|
+
// ===================================================================
|
|
32
|
+
// UTILITY FUNCTIONS
|
|
33
|
+
// ===================================================================
|
|
34
|
+
// Metrics namespace (pageset analytics)
|
|
35
|
+
export * as Metrics from './metrics';
|
|
31
36
|
import { DotProcessor } from './processors/dotProcessor';
|
|
32
37
|
import { OpmlProcessor } from './processors/opmlProcessor';
|
|
33
38
|
import { ObfProcessor } from './processors/obfProcessor';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metrics Namespace
|
|
3
|
+
*
|
|
4
|
+
* Pageset-focused metrics and analytics (board structure, effort, vocabulary).
|
|
5
|
+
* Use this for analyzing AAC trees, not end-user usage logs.
|
|
6
|
+
*/
|
|
7
|
+
export * from './utilities/analytics/metrics/types';
|
|
8
|
+
export * from './utilities/analytics/metrics/effort';
|
|
9
|
+
export * from './utilities/analytics/metrics/obl-types';
|
|
10
|
+
export { OblUtil, OblAnonymizer } from './utilities/analytics/metrics/obl';
|
|
11
|
+
export { MetricsCalculator } from './utilities/analytics/metrics/core';
|
|
12
|
+
export { VocabularyAnalyzer } from './utilities/analytics/metrics/vocabulary';
|
|
13
|
+
export { SentenceAnalyzer } from './utilities/analytics/metrics/sentence';
|
|
14
|
+
export { ComparisonAnalyzer } from './utilities/analytics/metrics/comparison';
|
|
15
|
+
export { ReferenceLoader } from './utilities/analytics/reference';
|
|
16
|
+
export { InMemoryReferenceLoader, createBrowserReferenceLoader, loadReferenceDataFromUrl, } from './utilities/analytics/reference/browser';
|
|
17
|
+
export * from './utilities/analytics/utils/idGenerator';
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { XMLBuilder } from 'fast-xml-parser';
|
|
2
|
+
import { AACSemanticCategory, AACSemanticIntent, } from '../../core/treeStructure';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as path from 'path';
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
|
+
import Database from 'better-sqlite3';
|
|
7
|
+
import { dotNetTicksToDate } from '../../utils/dotnetTicks';
|
|
8
|
+
import { getZipEntriesFromAdapter, resolveGridsetPasswordFromEnv } from './password';
|
|
9
|
+
import { openZipFromInput } from '../../utils/zip';
|
|
10
|
+
function normalizeZipPath(p) {
|
|
11
|
+
const unified = p.replace(/\\/g, '/');
|
|
12
|
+
try {
|
|
13
|
+
return unified.normalize('NFC');
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return unified;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build a map of button IDs to resolved image entry paths for a specific page.
|
|
21
|
+
* Helpful when rewriting zip entry names or validating images referenced in a grid.
|
|
22
|
+
*/
|
|
23
|
+
export function getPageTokenImageMap(tree, pageId) {
|
|
24
|
+
const map = new Map();
|
|
25
|
+
const page = tree.getPage(pageId);
|
|
26
|
+
if (!page)
|
|
27
|
+
return map;
|
|
28
|
+
for (const btn of page.buttons) {
|
|
29
|
+
if (btn.resolvedImageEntry) {
|
|
30
|
+
map.set(btn.id, normalizeZipPath(String(btn.resolvedImageEntry)));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return map;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Collect all image entries referenced across every page in a tree.
|
|
37
|
+
* Returns normalized zip entry paths that should be preserved when pruning images.
|
|
38
|
+
*/
|
|
39
|
+
export function getAllowedImageEntries(tree) {
|
|
40
|
+
const out = new Set();
|
|
41
|
+
Object.values(tree.pages).forEach((page) => {
|
|
42
|
+
page.buttons.forEach((btn) => {
|
|
43
|
+
if (btn.resolvedImageEntry)
|
|
44
|
+
out.add(normalizeZipPath(String(btn.resolvedImageEntry)));
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Read an image entry from a gridset zip by path.
|
|
51
|
+
* @param gridsetBuffer Gridset archive contents
|
|
52
|
+
* @param entryPath Entry name inside the zip
|
|
53
|
+
* @returns Image data buffer or null if not found
|
|
54
|
+
*/
|
|
55
|
+
export async function openImage(gridsetBuffer, entryPath, password = resolveGridsetPasswordFromEnv()) {
|
|
56
|
+
try {
|
|
57
|
+
const { zip } = await openZipFromInput(gridsetBuffer);
|
|
58
|
+
const entries = getZipEntriesFromAdapter(zip, password);
|
|
59
|
+
const want = normalizeZipPath(entryPath);
|
|
60
|
+
const entry = entries.find((e) => normalizeZipPath(e.entryName) === want);
|
|
61
|
+
if (!entry)
|
|
62
|
+
return null;
|
|
63
|
+
const data = await entry.getData();
|
|
64
|
+
if (typeof Buffer !== 'undefined' && typeof Buffer.from === 'function') {
|
|
65
|
+
return Buffer.from(data);
|
|
66
|
+
}
|
|
67
|
+
return data;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Generate a random GUID for Grid3 elements
|
|
75
|
+
* Grid3 uses GUIDs for grid identification
|
|
76
|
+
* @returns A UUID v4-like string in the format xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
|
77
|
+
*/
|
|
78
|
+
export function generateGrid3Guid() {
|
|
79
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
80
|
+
const r = (Math.random() * 16) | 0;
|
|
81
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
82
|
+
return v.toString(16);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Create Grid3 settings XML with start grid and common settings
|
|
87
|
+
* @param startGrid - Name of the grid to start on
|
|
88
|
+
* @param options - Optional settings (scan, hover, language, etc.)
|
|
89
|
+
* @returns XML string for Settings.xml
|
|
90
|
+
*/
|
|
91
|
+
export function createSettingsXml(startGrid, options) {
|
|
92
|
+
const builder = new XMLBuilder({
|
|
93
|
+
ignoreAttributes: false,
|
|
94
|
+
format: true,
|
|
95
|
+
indentBy: ' ',
|
|
96
|
+
});
|
|
97
|
+
const settingsData = {
|
|
98
|
+
GridSetSettings: {
|
|
99
|
+
'@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
100
|
+
StartGrid: startGrid,
|
|
101
|
+
ScanEnabled: options?.scanEnabled?.toString() ?? 'false',
|
|
102
|
+
ScanTimeoutMs: options?.scanTimeoutMs?.toString() ?? '2000',
|
|
103
|
+
HoverEnabled: options?.hoverEnabled?.toString() ?? 'false',
|
|
104
|
+
HoverTimeoutMs: options?.hoverTimeoutMs?.toString() ?? '1000',
|
|
105
|
+
MouseclickEnabled: options?.mouseclickEnabled?.toString() ?? 'true',
|
|
106
|
+
Language: options?.language ?? 'en-US',
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
return builder.build(settingsData);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Create Grid3 FileMap.xml content
|
|
113
|
+
* @param grids - Array of grid configurations with name and path
|
|
114
|
+
* @returns XML string for FileMap.xml
|
|
115
|
+
*/
|
|
116
|
+
export function createFileMapXml(grids) {
|
|
117
|
+
const builder = new XMLBuilder({
|
|
118
|
+
ignoreAttributes: false,
|
|
119
|
+
format: true,
|
|
120
|
+
indentBy: ' ',
|
|
121
|
+
});
|
|
122
|
+
const entries = grids.map((grid) => ({
|
|
123
|
+
'@_StaticFile': grid.path,
|
|
124
|
+
...(grid.dynamicFiles && grid.dynamicFiles.length > 0
|
|
125
|
+
? { DynamicFiles: { File: grid.dynamicFiles } }
|
|
126
|
+
: {}),
|
|
127
|
+
}));
|
|
128
|
+
const fileMapData = {
|
|
129
|
+
FileMap: {
|
|
130
|
+
'@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
|
|
131
|
+
Entries: {
|
|
132
|
+
Entry: entries,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
return builder.build(fileMapData);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get the Windows Common Documents folder path from registry
|
|
140
|
+
* Falls back to default path if registry access fails
|
|
141
|
+
* @returns Path to Common Documents folder
|
|
142
|
+
*/
|
|
143
|
+
export function getCommonDocumentsPath() {
|
|
144
|
+
// Only works on Windows
|
|
145
|
+
if (process.platform !== 'win32') {
|
|
146
|
+
return '';
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
// Query registry for Common Documents path
|
|
150
|
+
const command = 'REG.EXE QUERY "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders" /V "Common Documents"';
|
|
151
|
+
const output = execSync(command, { encoding: 'utf-8', windowsHide: true });
|
|
152
|
+
// Parse the output to extract the path
|
|
153
|
+
const match = output.match(/Common Documents\s+REG_SZ\s+(.+)/);
|
|
154
|
+
if (match && match[1]) {
|
|
155
|
+
return match[1].trim();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
// Registry access failed, fall back to default
|
|
160
|
+
}
|
|
161
|
+
// Default fallback path
|
|
162
|
+
return 'C:\\Users\\Public\\Documents';
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Find all Grid3 user data paths
|
|
166
|
+
* Searches for users and language codes in the Grid3 directory structure
|
|
167
|
+
* C:\Users\Public\Documents\Smartbox\Grid 3\Users\{UserName}\{langCode}\Phrases\history.sqlite
|
|
168
|
+
* Grid set/vocabulary archives live alongside users at:
|
|
169
|
+
* C:\Users\Public\Documents\Smartbox\Grid 3\Users\{UserName}\Grid Sets\
|
|
170
|
+
* @returns Array of Grid3 user path information
|
|
171
|
+
*/
|
|
172
|
+
export function findGrid3UserPaths() {
|
|
173
|
+
const results = [];
|
|
174
|
+
// Only works on Windows
|
|
175
|
+
if (process.platform !== 'win32') {
|
|
176
|
+
return results;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const commonDocs = getCommonDocumentsPath();
|
|
180
|
+
// Use Windows path joining so tests that mock a Windows platform stay consistent even on POSIX runners
|
|
181
|
+
const grid3BasePath = path.win32.join(commonDocs, 'Smartbox', 'Grid 3', 'Users');
|
|
182
|
+
// Check if Grid3 Users directory exists
|
|
183
|
+
if (!fs.existsSync(grid3BasePath)) {
|
|
184
|
+
return results;
|
|
185
|
+
}
|
|
186
|
+
// Enumerate users
|
|
187
|
+
const users = fs.readdirSync(grid3BasePath, { withFileTypes: true });
|
|
188
|
+
for (const userDir of users) {
|
|
189
|
+
if (!userDir.isDirectory())
|
|
190
|
+
continue;
|
|
191
|
+
const userName = userDir.name;
|
|
192
|
+
const userPath = path.win32.join(grid3BasePath, userName);
|
|
193
|
+
// Enumerate language codes
|
|
194
|
+
const langDirs = fs.readdirSync(userPath, { withFileTypes: true });
|
|
195
|
+
for (const langDir of langDirs) {
|
|
196
|
+
if (!langDir.isDirectory())
|
|
197
|
+
continue;
|
|
198
|
+
const langCode = langDir.name;
|
|
199
|
+
const basePath = path.win32.join(userPath, langCode);
|
|
200
|
+
const historyDbPath = path.win32.join(basePath, 'Phrases', 'history.sqlite');
|
|
201
|
+
// Only include if history database exists
|
|
202
|
+
if (fs.existsSync(historyDbPath)) {
|
|
203
|
+
results.push({
|
|
204
|
+
userName,
|
|
205
|
+
langCode,
|
|
206
|
+
basePath,
|
|
207
|
+
historyDbPath,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
// Silently fail if directory access fails
|
|
215
|
+
}
|
|
216
|
+
return results;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Find all Grid3 history database paths
|
|
220
|
+
* Convenience method that returns just the database file paths
|
|
221
|
+
* @returns Array of paths to history.sqlite files
|
|
222
|
+
*/
|
|
223
|
+
export function findGrid3HistoryDatabases() {
|
|
224
|
+
return findGrid3UserPaths().map((userPath) => userPath.historyDbPath);
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Get Grid 3 users (alias of findGrid3UserPaths for clarity)
|
|
228
|
+
*/
|
|
229
|
+
export function findGrid3Users() {
|
|
230
|
+
return findGrid3UserPaths();
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Find Grid 3 gridset/vocabulary files for each user
|
|
234
|
+
* @param userName Optional user filter; matches case-insensitively
|
|
235
|
+
* @returns Array of user/gridset path pairs
|
|
236
|
+
*/
|
|
237
|
+
export function findGrid3Vocabularies(userName) {
|
|
238
|
+
const results = [];
|
|
239
|
+
if (process.platform !== 'win32') {
|
|
240
|
+
return results;
|
|
241
|
+
}
|
|
242
|
+
const commonDocs = getCommonDocumentsPath();
|
|
243
|
+
const grid3BasePath = path.win32.join(commonDocs, 'Smartbox', 'Grid 3', 'Users');
|
|
244
|
+
if (!fs.existsSync(grid3BasePath)) {
|
|
245
|
+
return results;
|
|
246
|
+
}
|
|
247
|
+
const normalizedUser = userName?.toLowerCase();
|
|
248
|
+
const users = fs.readdirSync(grid3BasePath, { withFileTypes: true });
|
|
249
|
+
for (const userDir of users) {
|
|
250
|
+
if (!userDir.isDirectory())
|
|
251
|
+
continue;
|
|
252
|
+
if (normalizedUser && userDir.name.toLowerCase() !== normalizedUser)
|
|
253
|
+
continue;
|
|
254
|
+
const userRoot = path.win32.join(grid3BasePath, userDir.name);
|
|
255
|
+
const gridSetsDir = path.win32.join(userRoot, 'Grid Sets');
|
|
256
|
+
if (!fs.existsSync(gridSetsDir))
|
|
257
|
+
continue;
|
|
258
|
+
const entries = fs.readdirSync(gridSetsDir, { withFileTypes: true });
|
|
259
|
+
for (const entry of entries) {
|
|
260
|
+
if (!entry.isFile())
|
|
261
|
+
continue;
|
|
262
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
263
|
+
if (ext === '.gridset' || ext === '.gridsetx' || ext === '.grd' || ext === '.grdl') {
|
|
264
|
+
results.push({
|
|
265
|
+
userName: userDir.name,
|
|
266
|
+
gridsetPath: path.win32.join(gridSetsDir, entry.name),
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return results;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Find a specific user's Grid 3 history database
|
|
275
|
+
* @param userName User name to search for (case-insensitive)
|
|
276
|
+
* @param langCode Optional language code filter (case-insensitive)
|
|
277
|
+
* @returns Path to history.sqlite or null if not found
|
|
278
|
+
*/
|
|
279
|
+
export function findGrid3UserHistory(userName, langCode) {
|
|
280
|
+
if (!userName)
|
|
281
|
+
return null;
|
|
282
|
+
const normalizedUser = userName.toLowerCase();
|
|
283
|
+
const normalizedLang = langCode?.toLowerCase();
|
|
284
|
+
const match = findGrid3UserPaths().find((u) => u.userName.toLowerCase() === normalizedUser &&
|
|
285
|
+
(!normalizedLang || u.langCode.toLowerCase() === normalizedLang));
|
|
286
|
+
return match?.historyDbPath ?? null;
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Check whether Grid 3 appears to be installed (Windows only)
|
|
290
|
+
*/
|
|
291
|
+
export function isGrid3Installed() {
|
|
292
|
+
if (process.platform !== 'win32')
|
|
293
|
+
return false;
|
|
294
|
+
const commonDocs = getCommonDocumentsPath();
|
|
295
|
+
if (!commonDocs)
|
|
296
|
+
return false;
|
|
297
|
+
const grid3BasePath = path.win32.join(commonDocs, 'Smartbox', 'Grid 3', 'Users');
|
|
298
|
+
return fs.existsSync(grid3BasePath);
|
|
299
|
+
}
|
|
300
|
+
function parseGrid3ContentXml(xmlContent) {
|
|
301
|
+
const regex = /<r>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?<\/r>/gis;
|
|
302
|
+
const parts = [];
|
|
303
|
+
let match;
|
|
304
|
+
while ((match = regex.exec(xmlContent)) !== null) {
|
|
305
|
+
parts.push(match[1]);
|
|
306
|
+
}
|
|
307
|
+
if (parts.length > 0) {
|
|
308
|
+
return parts.join('');
|
|
309
|
+
}
|
|
310
|
+
return xmlContent.replace(/<[^>]+>/g, '').trim();
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Read history events from a Grid 3 history.sqlite database.
|
|
314
|
+
* @param historyDbPath Absolute path to the history database
|
|
315
|
+
* @returns Parsed history entries grouped by phrase
|
|
316
|
+
*/
|
|
317
|
+
export function readGrid3History(historyDbPath) {
|
|
318
|
+
if (!fs.existsSync(historyDbPath))
|
|
319
|
+
return [];
|
|
320
|
+
const db = new Database(historyDbPath, { readonly: true });
|
|
321
|
+
const rows = db
|
|
322
|
+
.prepare(`
|
|
323
|
+
SELECT p.Id as PhraseId,
|
|
324
|
+
p.Text as TextValue,
|
|
325
|
+
p.Content as ContentXml,
|
|
326
|
+
ph.Timestamp as TickValue,
|
|
327
|
+
ph.Latitude as Latitude,
|
|
328
|
+
ph.Longitude as Longitude
|
|
329
|
+
FROM PhraseHistory ph
|
|
330
|
+
INNER JOIN Phrases p ON p.Id = ph.PhraseId
|
|
331
|
+
WHERE ph.Timestamp <> 0
|
|
332
|
+
ORDER BY ph.Timestamp ASC
|
|
333
|
+
`)
|
|
334
|
+
.all();
|
|
335
|
+
const events = new Map();
|
|
336
|
+
for (const row of rows) {
|
|
337
|
+
const phraseId = row.PhraseId;
|
|
338
|
+
const rawContentSource = [row.ContentXml, row.TextValue].find((candidate) => {
|
|
339
|
+
if (candidate === null || candidate === undefined)
|
|
340
|
+
return false;
|
|
341
|
+
const asString = String(candidate);
|
|
342
|
+
return asString.trim().length > 0;
|
|
343
|
+
});
|
|
344
|
+
if (rawContentSource === undefined) {
|
|
345
|
+
continue; // Skip history rows with no usable text content
|
|
346
|
+
}
|
|
347
|
+
const rawContentText = String(rawContentSource);
|
|
348
|
+
const contentText = parseGrid3ContentXml(rawContentText);
|
|
349
|
+
const rawXml = typeof row.ContentXml === 'string' && row.ContentXml.trim().length > 0
|
|
350
|
+
? row.ContentXml
|
|
351
|
+
: undefined;
|
|
352
|
+
const entry = events.get(phraseId) ??
|
|
353
|
+
{
|
|
354
|
+
id: `grid:${phraseId}`,
|
|
355
|
+
content: contentText,
|
|
356
|
+
occurrences: [],
|
|
357
|
+
rawXml,
|
|
358
|
+
};
|
|
359
|
+
entry.occurrences.push({
|
|
360
|
+
timestamp: dotNetTicksToDate(BigInt(row.TickValue ?? 0)),
|
|
361
|
+
latitude: row.Latitude ?? null,
|
|
362
|
+
longitude: row.Longitude ?? null,
|
|
363
|
+
type: 'utterance',
|
|
364
|
+
intent: AACSemanticIntent.SPEAK_TEXT,
|
|
365
|
+
category: AACSemanticCategory.COMMUNICATION,
|
|
366
|
+
});
|
|
367
|
+
events.set(phraseId, entry);
|
|
368
|
+
}
|
|
369
|
+
return Array.from(events.values());
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Convenience wrapper to load history for a specific Grid 3 user/lang combination.
|
|
373
|
+
* @param userName Grid 3 user name (case-insensitive)
|
|
374
|
+
* @param langCode Optional language code to narrow selection (case-insensitive)
|
|
375
|
+
* @returns History entries for that user/language, or empty array if none
|
|
376
|
+
*/
|
|
377
|
+
export function readGrid3HistoryForUser(userName, langCode) {
|
|
378
|
+
const dbPath = findGrid3UserHistory(userName, langCode);
|
|
379
|
+
if (!dbPath)
|
|
380
|
+
return [];
|
|
381
|
+
return readGrid3History(dbPath);
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Load all available Grid 3 histories on the machine.
|
|
385
|
+
* @returns Combined history entries from every discovered history.sqlite
|
|
386
|
+
*/
|
|
387
|
+
export function readAllGrid3History() {
|
|
388
|
+
const paths = findGrid3HistoryDatabases();
|
|
389
|
+
return paths.flatMap((p) => readGrid3History(p));
|
|
390
|
+
}
|