@iservu-inc/adf-cli 0.9.1 → 0.11.0

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.
@@ -0,0 +1,508 @@
1
+ const inquirer = require('inquirer');
2
+ const chalk = require('chalk');
3
+ const analytics = require('./analytics');
4
+ const exporter = require('./analytics-exporter');
5
+
6
+ /**
7
+ * Learning Analytics Dashboard - CLI View
8
+ *
9
+ * Interactive dashboard for visualizing learning system analytics with:
10
+ * - Overview statistics
11
+ * - Skip trends visualization
12
+ * - Category preferences heatmap
13
+ * - Pattern health indicators
14
+ * - ASCII charts and visualizations
15
+ * - Export functionality
16
+ */
17
+
18
+ /**
19
+ * Display analytics dashboard
20
+ * @param {string} projectPath - Project root path
21
+ */
22
+ async function showAnalyticsDashboard(projectPath) {
23
+ try {
24
+ console.log(chalk.cyan('\n' + '═'.repeat(70)));
25
+ console.log(chalk.cyan.bold(' LEARNING ANALYTICS DASHBOARD'));
26
+ console.log(chalk.cyan('═'.repeat(70)) + '\n');
27
+
28
+ // Generate analytics data
29
+ const analyticsData = await analytics.generateAnalytics(projectPath);
30
+
31
+ // Show main dashboard
32
+ await showMainDashboard(analyticsData, projectPath);
33
+ } catch (error) {
34
+ console.error(chalk.red('\n❌ Error displaying analytics:'), error.message);
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Show main dashboard view
40
+ * @param {Object} analyticsData - Complete analytics data
41
+ * @param {string} projectPath - Project root path
42
+ */
43
+ async function showMainDashboard(analyticsData, projectPath) {
44
+ // Display overview
45
+ displayOverview(analyticsData.overview);
46
+ console.log(chalk.gray('─'.repeat(70)) + '\n');
47
+
48
+ // Display skip trends
49
+ displaySkipTrends(analyticsData.skipTrends);
50
+ console.log(chalk.gray('─'.repeat(70)) + '\n');
51
+
52
+ // Display effectiveness
53
+ displayEffectiveness(analyticsData.effectiveness);
54
+ console.log(chalk.gray('─'.repeat(70)) + '\n');
55
+
56
+ // Interactive menu
57
+ const { action } = await inquirer.prompt([
58
+ {
59
+ type: 'list',
60
+ name: 'action',
61
+ message: 'What would you like to do?',
62
+ choices: [
63
+ { name: 'View Category Breakdown', value: 'categories' },
64
+ { name: 'View Pattern Health', value: 'patterns' },
65
+ { name: 'View Session Timeline', value: 'timeline' },
66
+ { name: 'View Question Statistics', value: 'questions' },
67
+ { name: 'View Impactful Patterns', value: 'impact' },
68
+ new inquirer.Separator(),
69
+ { name: chalk.green('📦 Export Analytics (JSON/CSV)'), value: 'export' },
70
+ new inquirer.Separator(),
71
+ { name: chalk.gray('← Back to Learning System'), value: 'back' }
72
+ ]
73
+ }
74
+ ]);
75
+
76
+ // Handle action
77
+ switch (action) {
78
+ case 'categories':
79
+ await showCategoryView(analyticsData);
80
+ await showMainDashboard(analyticsData, projectPath);
81
+ break;
82
+ case 'patterns':
83
+ await showPatternHealthView(analyticsData);
84
+ await showMainDashboard(analyticsData, projectPath);
85
+ break;
86
+ case 'timeline':
87
+ await showTimelineView(analyticsData);
88
+ await showMainDashboard(analyticsData, projectPath);
89
+ break;
90
+ case 'questions':
91
+ await showQuestionStatsView(analyticsData);
92
+ await showMainDashboard(analyticsData, projectPath);
93
+ break;
94
+ case 'impact':
95
+ await showImpactfulPatternsView(analyticsData);
96
+ await showMainDashboard(analyticsData, projectPath);
97
+ break;
98
+ case 'export':
99
+ await handleExport(analyticsData, projectPath);
100
+ await showMainDashboard(analyticsData, projectPath);
101
+ break;
102
+ case 'back':
103
+ // Return to learning system menu
104
+ break;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Display overview statistics
110
+ * @param {Object} overview - Overview data
111
+ */
112
+ function displayOverview(overview) {
113
+ console.log(chalk.bold('📊 OVERVIEW\n'));
114
+
115
+ console.log(`Total Sessions: ${chalk.cyan(overview.totalSessions)} sessions`);
116
+ console.log(`Total Skips: ${chalk.yellow(overview.totalSkips)} questions (${overview.manualSkips} manual, ${overview.filteredSkips} filtered)`);
117
+ console.log(`Total Answers: ${chalk.green(overview.totalAnswers)} questions`);
118
+ console.log(`Active Patterns: ${chalk.magenta(overview.activePatterns)} patterns`);
119
+ console.log(`Active Rules: ${chalk.blue(overview.activeRules)} rules`);
120
+ console.log(`Learning System Age: ${chalk.white(overview.learningAge)} days`);
121
+ if (overview.dataSizeMB > 0) {
122
+ console.log(`Data Size: ${chalk.white(overview.dataSizeMB.toFixed(2))} MB`);
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Display skip trends
128
+ * @param {Array} skipTrends - Skip trends data
129
+ */
130
+ function displaySkipTrends(skipTrends) {
131
+ console.log(chalk.bold('📈 SKIP TRENDS (Last 12 Weeks)\n'));
132
+
133
+ // Find max for scaling
134
+ const maxSkips = Math.max(...skipTrends.map(t => t.totalSkips), 1);
135
+ const chartWidth = 20;
136
+
137
+ // Show last 4 weeks with bars
138
+ const recentWeeks = skipTrends.slice(-4);
139
+
140
+ for (const week of recentWeeks) {
141
+ const barWidth = Math.round((week.totalSkips / maxSkips) * chartWidth);
142
+ const bar = chalk.cyan('█'.repeat(barWidth)) + chalk.gray('░'.repeat(chartWidth - barWidth));
143
+ const label = `Week ${week.weekNumber}:`.padEnd(10);
144
+ const details = `${week.totalSkips} skips (${week.manualSkips} manual, ${week.filteredSkips} filtered)`;
145
+
146
+ console.log(`${label} ${bar} ${details}`);
147
+ }
148
+
149
+ // Calculate trend
150
+ const trend = calculateTrend(skipTrends);
151
+ console.log(`\nTrend: ${formatTrend(trend)}`);
152
+ }
153
+
154
+ /**
155
+ * Display effectiveness metrics
156
+ * @param {Object} effectiveness - Effectiveness data
157
+ */
158
+ function displayEffectiveness(effectiveness) {
159
+ console.log(chalk.bold('✨ EFFECTIVENESS\n'));
160
+
161
+ console.log(`Time Saved: ${chalk.green(effectiveness.timeSavedMinutes)} minutes (${effectiveness.timeSavedHours} hours)`);
162
+ console.log(`Questions Filtered: ${chalk.yellow(effectiveness.questionsFiltered)} questions`);
163
+ console.log(`Pattern Accuracy: ${chalk.cyan(effectiveness.patternAccuracy + '%')} ${getAccuracyIndicator(effectiveness.patternAccuracy)}`);
164
+ console.log(`Rule Applications: ${chalk.blue(effectiveness.totalRuleApplications)} total (${effectiveness.avgApplicationsPerRule} avg per rule)`);
165
+
166
+ const status = getEffectivenessStatus(effectiveness.overallEffectiveness);
167
+ console.log(`\nOverall Effectiveness: ${chalk.bold(effectiveness.overallEffectiveness + '%')} ${status}`);
168
+ }
169
+
170
+ /**
171
+ * Show category view
172
+ * @param {Object} analyticsData - Analytics data
173
+ */
174
+ async function showCategoryView(analyticsData) {
175
+ console.clear();
176
+ console.log(chalk.cyan('\n' + '═'.repeat(70)));
177
+ console.log(chalk.cyan.bold(' CATEGORY PREFERENCES'));
178
+ console.log(chalk.cyan('═'.repeat(70)) + '\n');
179
+
180
+ const categories = analyticsData.categoryPreferences;
181
+
182
+ if (categories.length === 0) {
183
+ console.log(chalk.gray('No category data available yet.'));
184
+ } else {
185
+ console.log(chalk.bold('🎯 SKIP RATE BY CATEGORY\n'));
186
+
187
+ // Find max for scaling
188
+ const maxRate = 100;
189
+ const chartWidth = 20;
190
+
191
+ for (const cat of categories) {
192
+ const barWidth = Math.round((cat.skipRate / maxRate) * chartWidth);
193
+ const bar = chalk.red('█'.repeat(barWidth)) + chalk.gray('░'.repeat(chartWidth - barWidth));
194
+ const label = cat.category.padEnd(20);
195
+ const details = `${cat.skipRate}% (${cat.skips}/${cat.total})`.padEnd(15);
196
+ const level = getLevelBadge(cat.level);
197
+
198
+ console.log(`${label} ${bar} ${details} ${level}`);
199
+ }
200
+ }
201
+
202
+ await pressAnyKey();
203
+ }
204
+
205
+ /**
206
+ * Show pattern health view
207
+ * @param {Object} analyticsData - Analytics data
208
+ */
209
+ async function showPatternHealthView(analyticsData) {
210
+ console.clear();
211
+ console.log(chalk.cyan('\n' + '═'.repeat(70)));
212
+ console.log(chalk.cyan.bold(' PATTERN HEALTH'));
213
+ console.log(chalk.cyan('═'.repeat(70)) + '\n');
214
+
215
+ const { patternDistribution, decayStatus } = analyticsData;
216
+
217
+ console.log(chalk.bold('🧬 PATTERN CONFIDENCE DISTRIBUTION\n'));
218
+
219
+ const chartWidth = 10;
220
+ const total = patternDistribution.totalPatterns;
221
+
222
+ if (total === 0) {
223
+ console.log(chalk.gray('No patterns detected yet.'));
224
+ } else {
225
+ displayDistributionBar('High (90%+):', patternDistribution.distribution.high, total, chartWidth, chalk.green);
226
+ displayDistributionBar('Medium (75-89%):', patternDistribution.distribution.medium, total, chartWidth, chalk.yellow);
227
+ displayDistributionBar('Low (50-74%):', patternDistribution.distribution.low, total, chartWidth, chalk.orange);
228
+ displayDistributionBar('Very Low (<50%):', patternDistribution.distribution.veryLow, total, chartWidth, chalk.red);
229
+
230
+ console.log(`\nAverage Confidence: ${chalk.cyan(patternDistribution.avgConfidence + '%')}`);
231
+ console.log(`At Risk (decay): ${chalk.red(patternDistribution.atRiskCount)} patterns\n`);
232
+
233
+ console.log(chalk.bold('⏰ PATTERN DECAY STATUS\n'));
234
+
235
+ displayDistributionBar('Healthy (75%+, <3mo):', decayStatus.healthy, total, chartWidth, chalk.green);
236
+ displayDistributionBar('Warning (50-74%, 3-5mo):', decayStatus.warning, total, chartWidth, chalk.yellow);
237
+ displayDistributionBar('Critical (<50%, >5mo):', decayStatus.critical, total, chartWidth, chalk.red);
238
+
239
+ console.log(`\nRecently Renewed: ${chalk.green(decayStatus.recentlyRenewed)} patterns (last 30 days)`);
240
+ console.log(`Average Age: ${chalk.white(decayStatus.avgAge)} days`);
241
+
242
+ if (decayStatus.critical > 0) {
243
+ console.log(`\n${chalk.yellow('⚠')} Warning: ${decayStatus.critical} patterns approaching removal threshold`);
244
+ }
245
+ }
246
+
247
+ await pressAnyKey();
248
+ }
249
+
250
+ /**
251
+ * Show timeline view
252
+ * @param {Object} analyticsData - Analytics data
253
+ */
254
+ async function showTimelineView(analyticsData) {
255
+ console.clear();
256
+ console.log(chalk.cyan('\n' + '═'.repeat(70)));
257
+ console.log(chalk.cyan.bold(' SESSION TIMELINE'));
258
+ console.log(chalk.cyan('═'.repeat(70)) + '\n');
259
+
260
+ const { sessionTimeline } = analyticsData;
261
+
262
+ console.log(chalk.bold('📅 SESSION HISTORY\n'));
263
+ console.log(`Total Sessions: ${chalk.cyan(sessionTimeline.totalSessions)}`);
264
+ console.log(`Sessions per Week: ${chalk.yellow(sessionTimeline.sessionsPerWeek)}`);
265
+ console.log(`First Session: ${chalk.white(sessionTimeline.firstSession || 'N/A')}`);
266
+ console.log(`Last Session: ${chalk.white(sessionTimeline.lastSession || 'N/A')}\n`);
267
+
268
+ if (sessionTimeline.timeline.length > 0) {
269
+ console.log(chalk.bold('Recent Sessions (Last 10):\n'));
270
+
271
+ const recentSessions = sessionTimeline.timeline.slice(0, 10);
272
+
273
+ for (const session of recentSessions) {
274
+ const date = new Date(session.timestamp).toLocaleDateString();
275
+ const time = new Date(session.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
276
+ const projectType = session.projectType || 'unknown';
277
+ const stats = `${session.answerCount}A / ${session.skipCount}S`;
278
+
279
+ console.log(`${chalk.gray(date + ' ' + time)} - ${chalk.cyan(projectType.padEnd(15))} ${chalk.white(stats)}`);
280
+ }
281
+ } else {
282
+ console.log(chalk.gray('No sessions recorded yet.'));
283
+ }
284
+
285
+ await pressAnyKey();
286
+ }
287
+
288
+ /**
289
+ * Show question stats view
290
+ * @param {Object} analyticsData - Analytics data
291
+ */
292
+ async function showQuestionStatsView(analyticsData) {
293
+ console.clear();
294
+ console.log(chalk.cyan('\n' + '═'.repeat(70)));
295
+ console.log(chalk.cyan.bold(' QUESTION STATISTICS'));
296
+ console.log(chalk.cyan('═'.repeat(70)) + '\n');
297
+
298
+ const { questionStats } = analyticsData;
299
+
300
+ // Most answered
301
+ console.log(chalk.bold('✅ MOST ANSWERED QUESTIONS (Top 5)\n'));
302
+ if (questionStats.mostAnswered.length > 0) {
303
+ questionStats.mostAnswered.slice(0, 5).forEach((q, i) => {
304
+ console.log(`${chalk.cyan((i + 1) + '.')} ${q.text}`);
305
+ console.log(` ${chalk.gray(`Answered ${q.answers} times | Category: ${q.category || 'N/A'}`)}\n`);
306
+ });
307
+ } else {
308
+ console.log(chalk.gray('No data available.\n'));
309
+ }
310
+
311
+ // Most skipped
312
+ console.log(chalk.bold('⏭️ MOST SKIPPED QUESTIONS (Top 5)\n'));
313
+ if (questionStats.mostSkipped.length > 0) {
314
+ questionStats.mostSkipped.slice(0, 5).forEach((q, i) => {
315
+ console.log(`${chalk.yellow((i + 1) + '.')} ${q.text}`);
316
+ console.log(` ${chalk.gray(`Skipped ${q.skips} times | Skip rate: ${q.skipRate}%`)}\n`);
317
+ });
318
+ } else {
319
+ console.log(chalk.gray('No data available.\n'));
320
+ }
321
+
322
+ await pressAnyKey();
323
+ }
324
+
325
+ /**
326
+ * Show impactful patterns view
327
+ * @param {Object} analyticsData - Analytics data
328
+ */
329
+ async function showImpactfulPatternsView(analyticsData) {
330
+ console.clear();
331
+ console.log(chalk.cyan('\n' + '═'.repeat(70)));
332
+ console.log(chalk.cyan.bold(' IMPACTFUL PATTERNS'));
333
+ console.log(chalk.cyan('═'.repeat(70)) + '\n');
334
+
335
+ const { impactfulPatterns } = analyticsData;
336
+
337
+ // Top by time saved
338
+ console.log(chalk.bold('⏱️ TOP PATTERNS BY TIME SAVED\n'));
339
+ if (impactfulPatterns.topByTimeSaved.length > 0) {
340
+ impactfulPatterns.topByTimeSaved.forEach((item, i) => {
341
+ console.log(`${chalk.green((i + 1) + '.')} ${item.pattern.questionText || item.pattern.questionId}`);
342
+ console.log(` ${chalk.gray(`${item.timeSaved} minutes saved | ${item.appliedCount} applications | ${item.pattern.confidence}% confidence`)}\n`);
343
+ });
344
+ } else {
345
+ console.log(chalk.gray('No patterns with impact yet.\n'));
346
+ }
347
+
348
+ // Perfect confidence
349
+ if (impactfulPatterns.perfectConfidence.length > 0) {
350
+ console.log(chalk.bold(`💯 PERFECT CONFIDENCE PATTERNS (${impactfulPatterns.perfectConfidence.length})\n`));
351
+ impactfulPatterns.perfectConfidence.slice(0, 5).forEach((pattern, i) => {
352
+ console.log(`${chalk.magenta((i + 1) + '.')} ${pattern.questionText || pattern.questionId}`);
353
+ console.log(` ${chalk.gray(`100% confidence | Type: ${pattern.type}`)}\n`);
354
+ });
355
+ }
356
+
357
+ await pressAnyKey();
358
+ }
359
+
360
+ /**
361
+ * Handle export
362
+ * @param {Object} analyticsData - Analytics data
363
+ * @param {string} projectPath - Project root path
364
+ */
365
+ async function handleExport(analyticsData, projectPath) {
366
+ console.clear();
367
+ console.log(chalk.cyan('\n' + '═'.repeat(70)));
368
+ console.log(chalk.cyan.bold(' EXPORT ANALYTICS'));
369
+ console.log(chalk.cyan('═'.repeat(70)) + '\n');
370
+
371
+ const { format } = await inquirer.prompt([
372
+ {
373
+ type: 'list',
374
+ name: 'format',
375
+ message: 'Select export format:',
376
+ choices: [
377
+ { name: 'JSON (single comprehensive file)', value: 'json' },
378
+ { name: 'CSV (multiple files)', value: 'csv' },
379
+ new inquirer.Separator(),
380
+ { name: chalk.gray('← Cancel'), value: 'cancel' }
381
+ ]
382
+ }
383
+ ]);
384
+
385
+ if (format === 'cancel') return;
386
+
387
+ try {
388
+ const result = await exporter.exportAnalytics(analyticsData, projectPath, format);
389
+
390
+ console.log(chalk.green('\n✅ Analytics exported successfully!\n'));
391
+
392
+ if (format === 'json') {
393
+ console.log(`File: ${chalk.cyan(result.file)}`);
394
+ console.log(`Size: ${chalk.white(result.size)}`);
395
+ } else {
396
+ console.log(`Files exported: ${chalk.cyan(result.files.length)}`);
397
+ result.files.forEach(f => {
398
+ console.log(` - ${chalk.white(f)}`);
399
+ });
400
+ }
401
+
402
+ console.log();
403
+ } catch (error) {
404
+ console.error(chalk.red('\n❌ Export failed:'), error.message);
405
+ }
406
+
407
+ await pressAnyKey();
408
+ }
409
+
410
+ // =============================================================================
411
+ // Helper Functions
412
+ // =============================================================================
413
+
414
+ /**
415
+ * Display distribution bar
416
+ * @param {string} label - Label
417
+ * @param {number} count - Count
418
+ * @param {number} total - Total
419
+ * @param {number} chartWidth - Chart width
420
+ * @param {Function} colorFn - Chalk color function
421
+ */
422
+ function displayDistributionBar(label, count, total, chartWidth, colorFn) {
423
+ const percentage = total > 0 ? Math.round((count / total) * 100) : 0;
424
+ const barWidth = Math.round((count / total) * chartWidth);
425
+ const bar = colorFn('█'.repeat(barWidth)) + chalk.gray('░'.repeat(chartWidth - barWidth));
426
+
427
+ console.log(`${label.padEnd(25)} ${bar} ${count} (${percentage}%)`);
428
+ }
429
+
430
+ /**
431
+ * Calculate trend from skip trends
432
+ * @param {Array} skipTrends - Skip trends data
433
+ * @returns {string} Trend direction
434
+ */
435
+ function calculateTrend(skipTrends) {
436
+ if (skipTrends.length < 4) return 'stable';
437
+
438
+ const recent = skipTrends.slice(-4);
439
+ const first = recent[0].totalSkips;
440
+ const last = recent[recent.length - 1].totalSkips;
441
+
442
+ if (last > first * 1.2) return 'increasing';
443
+ if (last < first * 0.8) return 'decreasing';
444
+ return 'stable';
445
+ }
446
+
447
+ /**
448
+ * Format trend with icon and color
449
+ * @param {string} trend - Trend direction
450
+ * @returns {string} Formatted trend
451
+ */
452
+ function formatTrend(trend) {
453
+ if (trend === 'increasing') return chalk.red('Increasing ↑');
454
+ if (trend === 'decreasing') return chalk.green('Decreasing ↓');
455
+ return chalk.yellow('Stable →');
456
+ }
457
+
458
+ /**
459
+ * Get accuracy indicator
460
+ * @param {number} accuracy - Accuracy percentage
461
+ * @returns {string} Indicator
462
+ */
463
+ function getAccuracyIndicator(accuracy) {
464
+ if (accuracy >= 90) return chalk.green('(Excellent)');
465
+ if (accuracy >= 75) return chalk.yellow('(Good)');
466
+ if (accuracy >= 50) return chalk.orange('(Fair)');
467
+ return chalk.red('(Needs Improvement)');
468
+ }
469
+
470
+ /**
471
+ * Get effectiveness status
472
+ * @param {number} effectiveness - Effectiveness percentage
473
+ * @returns {string} Status
474
+ */
475
+ function getEffectivenessStatus(effectiveness) {
476
+ if (effectiveness >= 90) return chalk.green('✓ EXCELLENT');
477
+ if (effectiveness >= 75) return chalk.green('✓ GOOD');
478
+ if (effectiveness >= 50) return chalk.yellow('○ FAIR');
479
+ return chalk.red('✗ NEEDS IMPROVEMENT');
480
+ }
481
+
482
+ /**
483
+ * Get level badge
484
+ * @param {string} level - Level ('high', 'medium', 'low')
485
+ * @returns {string} Badge
486
+ */
487
+ function getLevelBadge(level) {
488
+ if (level === 'high') return chalk.red.bold('HIGH');
489
+ if (level === 'medium') return chalk.yellow.bold('MEDIUM');
490
+ return chalk.green.bold('LOW');
491
+ }
492
+
493
+ /**
494
+ * Press any key to continue
495
+ */
496
+ async function pressAnyKey() {
497
+ await inquirer.prompt([
498
+ {
499
+ type: 'input',
500
+ name: 'continue',
501
+ message: chalk.gray('Press Enter to continue...')
502
+ }
503
+ ]);
504
+ }
505
+
506
+ module.exports = {
507
+ showAnalyticsDashboard
508
+ };