@@ -118,6 +118,13 @@ export class MetricsCalculator {
118118 // Update buttons using dynamic spelling effort if applicable
119119 const buttons = Array . from ( knownButtons . values ( ) ) . sort ( ( a , b ) => a . effort - b . effort ) ;
120120
121+ // Calculate metrics for word forms (smart grammar predictions)
122+ const wordFormMetrics = this . calculateWordFormMetrics ( tree , buttons , options ) ;
123+ buttons . push ( ...wordFormMetrics ) ;
124+
125+ // Re-sort after adding word forms
126+ buttons . sort ( ( a , b ) => a . effort - b . effort ) ;
127+
121128 // Calculate grid dimensions
122129 const grid = this . calculateGridDimensions ( tree ) ;
123130
@@ -719,6 +726,91 @@ export class MetricsCalculator {
719726 return boardPcts ;
720727 }
721728
729+ /**
730+ * Calculate metrics for word forms (smart grammar predictions)
731+ *
732+ * Word forms are dynamically generated and not part of the tree structure.
733+ * Their effort is calculated as:
734+ * - Parent button's cumulative effort (to reach the button)
735+ * - + Effort to select the word form from its position in predictions grid
736+ *
737+ * @param tree - The AAC tree
738+ * @param buttons - Already calculated button metrics
739+ * @param options - Metrics options
740+ * @returns Array of word form button metrics
741+ */
742+ private calculateWordFormMetrics (
743+ tree : AACTree ,
744+ buttons : ButtonMetrics [ ] ,
745+ _options : MetricsOptions = { }
746+ ) : ButtonMetrics [ ] {
747+ const wordFormMetrics : ButtonMetrics [ ] = [ ] ;
748+
749+ // Track which buttons already exist to avoid duplicates
750+ const existingLabels = new Map < string , ButtonMetrics > ( ) ;
751+ buttons . forEach ( ( btn ) => existingLabels . set ( btn . label . toLowerCase ( ) , btn ) ) ;
752+
753+ // Iterate through all pages to find buttons with predictions
754+ Object . values ( tree . pages ) . forEach ( ( page : AACPage ) => {
755+ page . grid . forEach ( ( row : ( AACButton | null ) [ ] ) => {
756+ row . forEach ( ( btn : AACButton | null ) => {
757+ if ( ! btn || ! btn . predictions || btn . predictions . length === 0 ) return ;
758+
759+ // Find the parent button's metrics
760+ const parentMetrics = buttons . find ( ( b ) => b . id === btn . id ) ;
761+ if ( ! parentMetrics ) return ;
762+
763+ // Calculate effort for each word form
764+ btn . predictions . forEach ( ( wordForm : string , index : number ) => {
765+ const wordFormLower = wordForm . toLowerCase ( ) ;
766+
767+ // Skip if this word form already exists as a regular button
768+ if ( existingLabels . has ( wordFormLower ) ) {
769+ return ;
770+ }
771+
772+ // Calculate effort based on position in predictions array
773+ // Assume predictions are displayed in a grid layout (e.g., 2 columns)
774+ const predictionsGridCols = 2 ; // Typical predictions layout
775+ const predictionRowIndex = Math . floor ( index / predictionsGridCols ) ;
776+ const predictionColIndex = index % predictionsGridCols ;
777+
778+ // Calculate visual scan effort to reach this word form position
779+ // Using similar logic to button scanning effort
780+ const predictionPriorItems =
781+ predictionRowIndex * predictionsGridCols + predictionColIndex ;
782+ const predictionSelectionEffort = visualScanEffort ( predictionPriorItems ) ;
783+
784+ // Word form effort = parent button's cumulative effort + selection effort
785+ const wordFormEffort = parentMetrics . effort + predictionSelectionEffort ;
786+
787+ // Mark as word form with a special ID pattern
788+ const wordFormBtn : ButtonMetrics = {
789+ id : `${ btn . id } _wordform_${ index } ` ,
790+ label : wordForm ,
791+ level : parentMetrics . level ,
792+ effort : wordFormEffort ,
793+ count : 1 ,
794+ semantic_id : parentMetrics . semantic_id ,
795+ clone_id : parentMetrics . clone_id ,
796+ temporary_home_id : parentMetrics . temporary_home_id ,
797+ is_word_form : true , // Mark this as a word form metric
798+ parent_button_id : btn . id , // Track parent button
799+ parent_button_label : parentMetrics . label , // Track parent label
800+ } ;
801+
802+ wordFormMetrics . push ( wordFormBtn ) ;
803+ existingLabels . set ( wordFormLower , wordFormBtn ) ;
804+ } ) ;
805+ } ) ;
806+ } ) ;
807+ } ) ;
808+
809+ console . log ( `📝 Calculated ${ wordFormMetrics . length } word form metrics from predictions` ) ;
810+
811+ return wordFormMetrics ;
812+ }
813+
722814 /**
723815 * Calculate grid dimensions from the tree
724816 */
0 commit comments