|  |  | @ -10,26 +10,25 @@ import type { AiPromptMode } from '@ghostfolio/common/types'; | 
			
		
	
		
			
				
					|  |  |  | import { Injectable } from '@nestjs/common'; | 
			
		
	
		
			
				
					|  |  |  | import { createOpenRouter } from '@openrouter/ai-sdk-provider'; | 
			
		
	
		
			
				
					|  |  |  | import { generateText } from 'ai'; | 
			
		
	
		
			
				
					|  |  |  | import { ColumnDescriptor } from 'tablemark'; | 
			
		
	
		
			
				
					|  |  |  | import type { ColumnDescriptor } from 'tablemark'; | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  | const tablemark = require('tablemark').default; | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  | // Column definitions for holdings table
 | 
			
		
	
		
			
				
					|  |  |  | const HOLDINGS_TABLE_COLUMNS: ({ key: string } & ColumnDescriptor)[] = [ | 
			
		
	
		
			
				
					|  |  |  |   { key: 'CURRENCY', name: 'Currency' }, | 
			
		
	
		
			
				
					|  |  |  | @Injectable() | 
			
		
	
		
			
				
					|  |  |  | export class AiService { | 
			
		
	
		
			
				
					|  |  |  |   private readonly HOLDINGS_TABLE_COLUMNS: ({ | 
			
		
	
		
			
				
					|  |  |  |     key: string; | 
			
		
	
		
			
				
					|  |  |  |   } & ColumnDescriptor)[] = [ | 
			
		
	
		
			
				
					|  |  |  |     { key: 'NAME', name: 'Name' }, | 
			
		
	
		
			
				
					|  |  |  |     { key: 'SYMBOL', name: 'Symbol' }, | 
			
		
	
		
			
				
					|  |  |  |     { key: 'CURRENCY', name: 'Currency' }, | 
			
		
	
		
			
				
					|  |  |  |     { key: 'ASSET_CLASS', name: 'Asset Class' }, | 
			
		
	
		
			
				
					|  |  |  |     { key: 'ASSET_SUB_CLASS', name: 'Asset Sub Class' }, | 
			
		
	
		
			
				
					|  |  |  |     { | 
			
		
	
		
			
				
					|  |  |  |       align: 'right', | 
			
		
	
		
			
				
					|  |  |  |       key: 'ALLOCATION_PERCENTAGE', | 
			
		
	
		
			
				
					|  |  |  |       name: 'Allocation in Percentage' | 
			
		
	
		
			
				
					|  |  |  |   }, | 
			
		
	
		
			
				
					|  |  |  |   { key: 'ASSET_SUB_CLASS', name: 'Asset Sub Class' } | 
			
		
	
		
			
				
					|  |  |  |     } | 
			
		
	
		
			
				
					|  |  |  |   ]; | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  | @Injectable() | 
			
		
	
		
			
				
					|  |  |  | export class AiService { | 
			
		
	
		
			
				
					|  |  |  |   public constructor( | 
			
		
	
		
			
				
					|  |  |  |     private readonly portfolioService: PortfolioService, | 
			
		
	
		
			
				
					|  |  |  |     private readonly propertyService: PropertyService | 
			
		
	
	
		
			
				
					|  |  | @ -75,50 +74,71 @@ export class AiService { | 
			
		
	
		
			
				
					|  |  |  |       userId | 
			
		
	
		
			
				
					|  |  |  |     }); | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     const holdingsTableColumns: ColumnDescriptor[] = HOLDINGS_TABLE_COLUMNS.map( | 
			
		
	
		
			
				
					|  |  |  |       ({ align, name }) => { | 
			
		
	
		
			
				
					|  |  |  |     const holdingsTableColumns: ColumnDescriptor[] = | 
			
		
	
		
			
				
					|  |  |  |       this.HOLDINGS_TABLE_COLUMNS.map(({ align, name }) => { | 
			
		
	
		
			
				
					|  |  |  |         return { name, align: align ?? 'left' }; | 
			
		
	
		
			
				
					|  |  |  |       } | 
			
		
	
		
			
				
					|  |  |  |     ); | 
			
		
	
		
			
				
					|  |  |  |       }); | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     const holdingsTableRows = Object.values(holdings) | 
			
		
	
		
			
				
					|  |  |  |       .sort((a, b) => { | 
			
		
	
		
			
				
					|  |  |  |         return b.allocationInPercentage - a.allocationInPercentage; | 
			
		
	
		
			
				
					|  |  |  |       }) | 
			
		
	
		
			
				
					|  |  |  |       .map((holding) => { | 
			
		
	
		
			
				
					|  |  |  |         return HOLDINGS_TABLE_COLUMNS.reduce( | 
			
		
	
		
			
				
					|  |  |  |       .map( | 
			
		
	
		
			
				
					|  |  |  |         ({ | 
			
		
	
		
			
				
					|  |  |  |           allocationInPercentage, | 
			
		
	
		
			
				
					|  |  |  |           assetClass, | 
			
		
	
		
			
				
					|  |  |  |           assetSubClass, | 
			
		
	
		
			
				
					|  |  |  |           currency, | 
			
		
	
		
			
				
					|  |  |  |           name: label, | 
			
		
	
		
			
				
					|  |  |  |           symbol | 
			
		
	
		
			
				
					|  |  |  |         }) => { | 
			
		
	
		
			
				
					|  |  |  |           return this.HOLDINGS_TABLE_COLUMNS.reduce( | 
			
		
	
		
			
				
					|  |  |  |             (row, { key, name }) => { | 
			
		
	
		
			
				
					|  |  |  |               switch (key) { | 
			
		
	
		
			
				
					|  |  |  |               case 'CURRENCY': | 
			
		
	
		
			
				
					|  |  |  |                 row[name] = holding.currency; | 
			
		
	
		
			
				
					|  |  |  |                 case 'ALLOCATION_PERCENTAGE': | 
			
		
	
		
			
				
					|  |  |  |                   row[name] = `${(allocationInPercentage * 100).toFixed(3)}%`; | 
			
		
	
		
			
				
					|  |  |  |                   break; | 
			
		
	
		
			
				
					|  |  |  |               case 'NAME': | 
			
		
	
		
			
				
					|  |  |  |                 row[name] = holding.name; | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |                 case 'ASSET_CLASS': | 
			
		
	
		
			
				
					|  |  |  |                   row[name] = assetClass ?? ''; | 
			
		
	
		
			
				
					|  |  |  |                   break; | 
			
		
	
		
			
				
					|  |  |  |               case 'SYMBOL': | 
			
		
	
		
			
				
					|  |  |  |                 row[name] = holding.symbol; | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |                 case 'ASSET_SUB_CLASS': | 
			
		
	
		
			
				
					|  |  |  |                   row[name] = assetSubClass ?? ''; | 
			
		
	
		
			
				
					|  |  |  |                   break; | 
			
		
	
		
			
				
					|  |  |  |               case 'ASSET_CLASS': | 
			
		
	
		
			
				
					|  |  |  |                 row[name] = holding.assetClass ?? ''; | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |                 case 'CURRENCY': | 
			
		
	
		
			
				
					|  |  |  |                   row[name] = currency; | 
			
		
	
		
			
				
					|  |  |  |                   break; | 
			
		
	
		
			
				
					|  |  |  |               case 'ALLOCATION_PERCENTAGE': | 
			
		
	
		
			
				
					|  |  |  |                 row[name] = | 
			
		
	
		
			
				
					|  |  |  |                   `${(holding.allocationInPercentage * 100).toFixed(3)}%`; | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |                 case 'NAME': | 
			
		
	
		
			
				
					|  |  |  |                   row[name] = label; | 
			
		
	
		
			
				
					|  |  |  |                   break; | 
			
		
	
		
			
				
					|  |  |  |               case 'ASSET_SUB_CLASS': | 
			
		
	
		
			
				
					|  |  |  |                 row[name] = holding.assetSubClass ?? ''; | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |                 case 'SYMBOL': | 
			
		
	
		
			
				
					|  |  |  |                   row[name] = symbol; | 
			
		
	
		
			
				
					|  |  |  |                   break; | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |                 default: | 
			
		
	
		
			
				
					|  |  |  |                   row[name] = ''; | 
			
		
	
		
			
				
					|  |  |  |                   break; | 
			
		
	
		
			
				
					|  |  |  |               } | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |               return row; | 
			
		
	
		
			
				
					|  |  |  |             }, | 
			
		
	
		
			
				
					|  |  |  |             {} as Record<string, string> | 
			
		
	
		
			
				
					|  |  |  |           ); | 
			
		
	
		
			
				
					|  |  |  |       }); | 
			
		
	
		
			
				
					|  |  |  |         } | 
			
		
	
		
			
				
					|  |  |  |       ); | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     // Dynamic import to load ESM module from CommonJS context
 | 
			
		
	
		
			
				
					|  |  |  |     // eslint-disable-next-line @typescript-eslint/no-implied-eval
 | 
			
		
	
		
			
				
					|  |  |  |     const dynamicImport = new Function('s', 'return import(s)') as ( | 
			
		
	
		
			
				
					|  |  |  |       s: string | 
			
		
	
		
			
				
					|  |  |  |     ) => Promise<typeof import('tablemark')>; | 
			
		
	
		
			
				
					|  |  |  |     const { tablemark } = await dynamicImport('tablemark'); | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
		
			
				
					|  |  |  |     const holdingsTableString = tablemark.default(holdingsTableRows, { | 
			
		
	
		
			
				
					|  |  |  |     const holdingsTableString = tablemark(holdingsTableRows, { | 
			
		
	
		
			
				
					|  |  |  |       columns: holdingsTableColumns | 
			
		
	
		
			
				
					|  |  |  |     }); | 
			
		
	
		
			
				
					|  |  |  | 
 | 
			
		
	
	
		
			
				
					|  |  | 
 |