Skip to content

Commit 9e2e969

Browse files
committed
added suggest words to metrics
1 parent 8f767f1 commit 9e2e969

4 files changed

Lines changed: 144 additions & 5 deletions

File tree

src/utilities/analytics/metrics/core.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,13 @@ export class MetricsCalculator {
989989
}
990990
if (!parentMetrics) return;
991991

992+
// Build set of original Suggest Words predictions (from Prediction.PredictThis).
993+
// These require an extra confirmation tap from the user. Smart grammar
994+
// morphology outcomes are generated automatically and need no extra tap.
995+
const suggestWordsSet = new Set<string>(
996+
((btn.parameters?.predictions || []) as string[]).map((w) => w.toLowerCase())
997+
);
998+
992999
// Calculate effort for each word form
9931000
btn.predictions.forEach((wordForm: string, index: number) => {
9941001
const wordFormLower = wordForm.toLowerCase();
@@ -1005,8 +1012,16 @@ export class MetricsCalculator {
10051012
predictionRowIndex * predictionsGridCols + predictionColIndex;
10061013
const predictionSelectionEffort = visualScanEffort(predictionPriorItems);
10071014

1008-
// Word form effort = parent button's cumulative effort + selection effort
1009-
const wordFormEffort = parentMetrics.effort + predictionSelectionEffort;
1015+
// Add confirmation cost for Suggest Words outcomes only.
1016+
// Suggest Words requires an explicit tap on the prediction bar,
1017+
// while smart grammar morphology forms are auto-generated (no extra tap).
1018+
const suggestWordsConfirmation = suggestWordsSet.has(wordFormLower)
1019+
? EFFORT_CONSTANTS.SUGGEST_WORDS_SELECTION_EFFORT
1020+
: 0;
1021+
1022+
// Word form effort = parent button's cumulative effort + selection effort + confirmation
1023+
const wordFormEffort =
1024+
parentMetrics.effort + predictionSelectionEffort + suggestWordsConfirmation;
10101025

10111026
// Check if this word already exists as a regular button
10121027
const existingBtn = existingLabels.get(wordFormLower);
@@ -1031,9 +1046,10 @@ export class MetricsCalculator {
10311046
semantic_id: parentMetrics.semantic_id,
10321047
clone_id: parentMetrics.clone_id,
10331048
temporary_home_id: parentMetrics.temporary_home_id,
1034-
is_word_form: true, // Mark this as a word form metric
1035-
parent_button_id: btn.id, // Track parent button
1036-
parent_button_label: parentMetrics.label, // Track parent label
1049+
is_word_form: true,
1050+
is_suggest_words: suggestWordsSet.has(wordFormLower) || undefined,
1051+
parent_button_id: btn.id,
1052+
parent_button_label: parentMetrics.label,
10371053
};
10381054

10391055
wordFormMetrics.push(wordFormBtn);

src/utilities/analytics/metrics/effort.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const EFFORT_CONSTANTS = {
3232
SCAN_SELECTION_COST: 0.1, // Cost of a switch selection
3333
DEFAULT_SCAN_ERROR_RATE: 0.1, // 10% chance of missing a selection
3434
SCAN_RETRY_PENALTY: 1.0, // Cost multiplier for a full loop retry
35+
SUGGEST_WORDS_SELECTION_EFFORT: 0.5, // Extra tap to confirm a Suggest Words prediction
3536
} as const;
3637

3738
/**

src/utilities/analytics/metrics/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface ButtonMetrics {
2424
comp_effort?: number; // Comparison: effort in comparison set
2525
// Word form metrics (for smart grammar predictions)
2626
is_word_form?: boolean; // True if this is a word form from predictions
27+
is_suggest_words?: boolean; // True if produced via Suggest Words (requires extra tap)
2728
parent_button_id?: string; // ID of parent button that has these predictions
2829
parent_button_label?: string; // Label of parent button
2930
pos?: string; // Part-of-speech tag from gridset (e.g., 'Verb', 'Noun')

test/suggestWordsEffort.test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
2+
import { describe, expect, it } from '@jest/globals';
3+
import { AACTree, AACPage, AACButton } from '../src/core/treeStructure';
4+
import { MetricsCalculator } from '../src/utilities/analytics/metrics/core';
5+
import { EFFORT_CONSTANTS, visualScanEffort } from '../src/utilities/analytics/metrics/effort';
6+
7+
function buildTreeWithPredictions(
8+
predictions: string[],
9+
parametersPredictions?: string[],
10+
pos?: string
11+
): { tree: AACTree; btnId: string } {
12+
const tree = new AACTree();
13+
const page = new AACPage({
14+
id: 'root',
15+
name: 'Home',
16+
grid: { columns: 2, rows: 2 },
17+
});
18+
19+
const btn = new AACButton({
20+
id: 'some_btn',
21+
label: 'some',
22+
type: 'SPEAK',
23+
x: 0,
24+
y: 0,
25+
predictions,
26+
pos,
27+
parameters: parametersPredictions ? { predictions: parametersPredictions } : undefined,
28+
});
29+
30+
page.grid[0][0] = btn;
31+
page.addButton(btn);
32+
tree.addPage(page);
33+
tree.rootId = 'root';
34+
35+
return { tree, btnId: btn.id };
36+
}
37+
38+
describe('Suggest Words effort cost', () => {
39+
it('adds confirmation cost to Suggest Words word forms', () => {
40+
const suggestWords = ['something', 'someone', 'somewhere'];
41+
const { tree } = buildTreeWithPredictions(suggestWords, suggestWords);
42+
43+
const calculator = new MetricsCalculator();
44+
const result = calculator.analyze(tree, { useSmartGrammar: true });
45+
46+
const parentBtn = result.buttons.find((b) => b.label === 'some');
47+
expect(parentBtn).toBeDefined();
48+
49+
const something = result.buttons.find((b) => b.label === 'something');
50+
expect(something).toBeDefined();
51+
expect(something!.is_word_form).toBe(true);
52+
expect(something!.is_suggest_words).toBe(true);
53+
54+
const expectedEffort =
55+
parentBtn!.effort + visualScanEffort(0) + EFFORT_CONSTANTS.SUGGEST_WORDS_SELECTION_EFFORT;
56+
expect(something!.effort).toBeCloseTo(expectedEffort, 4);
57+
});
58+
59+
it('does not add confirmation cost to morphology word forms', () => {
60+
const predictions = ['goes', 'going', 'went'];
61+
const { tree } = buildTreeWithPredictions(predictions, undefined, 'Verb');
62+
63+
const calculator = new MetricsCalculator();
64+
const result = calculator.analyze(tree, { useSmartGrammar: true });
65+
66+
const parentBtn = result.buttons.find((b) => b.label === 'some');
67+
expect(parentBtn).toBeDefined();
68+
69+
const goes = result.buttons.find((b) => b.label === 'goes');
70+
expect(goes).toBeDefined();
71+
expect(goes!.is_word_form).toBe(true);
72+
expect(goes!.is_suggest_words).toBeUndefined();
73+
74+
expect(goes!.effort).toBeCloseTo(parentBtn!.effort + visualScanEffort(0), 4);
75+
});
76+
77+
it('only adds confirmation to Suggest Words forms when predictions are mixed', () => {
78+
const suggestWordsOriginals = ['something', 'someone'];
79+
const allPredictions = ['something', 'someone', 'somes'];
80+
const { tree } = buildTreeWithPredictions(allPredictions, suggestWordsOriginals, 'Noun');
81+
82+
const calculator = new MetricsCalculator();
83+
const result = calculator.analyze(tree, { useSmartGrammar: true });
84+
85+
const parentBtn = result.buttons.find((b) => b.label === 'some');
86+
87+
const something = result.buttons.find((b) => b.label === 'something');
88+
expect(something).toBeDefined();
89+
expect(something!.is_suggest_words).toBe(true);
90+
expect(something!.effort).toBeCloseTo(
91+
parentBtn!.effort + visualScanEffort(0) + EFFORT_CONSTANTS.SUGGEST_WORDS_SELECTION_EFFORT,
92+
4
93+
);
94+
95+
// "somes" is at index 2 → predictionPriorItems = 2
96+
const somes = result.buttons.find((b) => b.label === 'somes');
97+
expect(somes).toBeDefined();
98+
expect(somes!.is_suggest_words).toBeUndefined();
99+
expect(somes!.effort).toBeCloseTo(parentBtn!.effort + visualScanEffort(2), 4);
100+
});
101+
102+
it('has no confirmation when parameters.predictions is absent', () => {
103+
const predictions = ['something', 'someone'];
104+
const { tree } = buildTreeWithPredictions(predictions, undefined, 'Noun');
105+
106+
const calculator = new MetricsCalculator();
107+
const result = calculator.analyze(tree, { useSmartGrammar: true });
108+
109+
const parentBtn = result.buttons.find((b) => b.label === 'some');
110+
111+
const something = result.buttons.find((b) => b.label === 'something');
112+
expect(something).toBeDefined();
113+
expect(something!.is_suggest_words).toBeUndefined();
114+
expect(something!.effort).toBeCloseTo(parentBtn!.effort + visualScanEffort(0), 4);
115+
});
116+
117+
it('SUGGEST_WORDS_SELECTION_EFFORT is between 0.5 and 1.0', () => {
118+
expect(EFFORT_CONSTANTS.SUGGEST_WORDS_SELECTION_EFFORT).toBeGreaterThanOrEqual(0.5);
119+
expect(EFFORT_CONSTANTS.SUGGEST_WORDS_SELECTION_EFFORT).toBeLessThanOrEqual(1.0);
120+
});
121+
});

0 commit comments

Comments
 (0)