CarouselForge Developer commited on
Commit
cd65da7
·
1 Parent(s): a0efd08

feat: add insights recommender engine for template/variant suggestions

Browse files
src/lib/insights/recommender.test.ts ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { scoreRecommendation, recommendTemplateVariant } from './recommender';
2
+ import type { GeneratedInsight } from '@/lib/analytics/insights';
3
+
4
+ describe('Recommender Engine', () => {
5
+ describe('scoreRecommendation', () => {
6
+ it('should give high score to high-confidence template insights', () => {
7
+ const insight: GeneratedInsight = {
8
+ id: 'template-2026-04-07',
9
+ observation: '"bold-statement" template performed best',
10
+ confidence_level: 'high',
11
+ applies_to: 'template',
12
+ recommendation: 'Use bold-statement',
13
+ created_at: new Date().toISOString(),
14
+ };
15
+ const score = scoreRecommendation(insight);
16
+ expect(score).toBeGreaterThan(0.8);
17
+ });
18
+
19
+ it('should weight recent insights higher than old ones', () => {
20
+ const recentInsight: GeneratedInsight = {
21
+ id: 'template-2026-04-14',
22
+ observation: '"split-screen" template worked well',
23
+ confidence_level: 'high',
24
+ applies_to: 'template',
25
+ recommendation: 'Use split-screen',
26
+ created_at: new Date().toISOString(),
27
+ };
28
+
29
+ const oldInsight: GeneratedInsight = {
30
+ id: 'template-2026-03-14',
31
+ observation: '"numbered-steps" template worked well',
32
+ confidence_level: 'high',
33
+ applies_to: 'template',
34
+ recommendation: 'Use numbered-steps',
35
+ created_at: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString(),
36
+ };
37
+
38
+ const recentScore = scoreRecommendation(recentInsight);
39
+ const oldScore = scoreRecommendation(oldInsight);
40
+ expect(recentScore).toBeGreaterThan(oldScore);
41
+ });
42
+
43
+ it('should give lower score to low-confidence insights', () => {
44
+ const lowConfInsight: GeneratedInsight = {
45
+ id: 'template-2026-04-14',
46
+ observation: 'Some template might work',
47
+ confidence_level: 'low',
48
+ applies_to: 'template',
49
+ recommendation: 'Maybe try this',
50
+ created_at: new Date().toISOString(),
51
+ };
52
+ const score = scoreRecommendation(lowConfInsight);
53
+ expect(score).toBeLessThan(0.5);
54
+ });
55
+ });
56
+
57
+ describe('recommendTemplateVariant', () => {
58
+ it('should return null when no insights provided', () => {
59
+ const rec = recommendTemplateVariant([]);
60
+ expect(rec).toBeNull();
61
+ });
62
+
63
+ it('should recommend template based on highest-scored insight', () => {
64
+ const insights: GeneratedInsight[] = [
65
+ {
66
+ id: 'template-1',
67
+ observation: '"bold-statement" performed best',
68
+ confidence_level: 'high',
69
+ applies_to: 'template',
70
+ recommendation: 'Use bold-statement',
71
+ created_at: new Date().toISOString(),
72
+ },
73
+ {
74
+ id: 'variant-1',
75
+ observation: '"alternate" variant got more engagement',
76
+ confidence_level: 'high',
77
+ applies_to: 'template',
78
+ recommendation: 'Try alternate variant',
79
+ created_at: new Date().toISOString(),
80
+ },
81
+ ];
82
+
83
+ const rec = recommendTemplateVariant(insights);
84
+ expect(rec).not.toBeNull();
85
+ expect(rec?.template).toBe('bold-statement');
86
+ expect(rec?.confidence).toBe('high');
87
+ });
88
+
89
+ it('should filter out non-template insights', () => {
90
+ const insights: GeneratedInsight[] = [
91
+ {
92
+ id: 'palette-1',
93
+ observation: 'Blue palette got engagement',
94
+ confidence_level: 'high',
95
+ applies_to: 'palette',
96
+ recommendation: 'Use blue',
97
+ created_at: new Date().toISOString(),
98
+ },
99
+ {
100
+ id: 'template-1',
101
+ observation: '"quote-card" template worked',
102
+ confidence_level: 'medium',
103
+ applies_to: 'template',
104
+ recommendation: 'Use quote-card',
105
+ created_at: new Date().toISOString(),
106
+ },
107
+ ];
108
+
109
+ const rec = recommendTemplateVariant(insights);
110
+ expect(rec?.template).toBe('quote-card');
111
+ });
112
+ });
113
+ });
src/lib/insights/recommender.ts ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { GeneratedInsight } from '@/lib/analytics/insights';
2
+ import type { Template } from '@/types/carousel';
3
+
4
+ export interface TemplateRecommendation {
5
+ template: Template;
6
+ confidence: 'high' | 'medium' | 'low';
7
+ reason: string;
8
+ score: number;
9
+ }
10
+
11
+ /**
12
+ * Score an insight based on confidence level, recency, and relevance.
13
+ * Returns 0-1 score where 1 is highest recommendation.
14
+ */
15
+ export function scoreRecommendation(insight: GeneratedInsight): number {
16
+ // Base score from confidence level
17
+ const confidenceScores: Record<string, number> = {
18
+ high: 1.0,
19
+ medium: 0.6,
20
+ low: 0.3,
21
+ };
22
+ let score = confidenceScores[insight.confidence_level] || 0.3;
23
+
24
+ // Adjust for recency (insights older than 2 weeks get lower score)
25
+ const createdDate = new Date(insight.created_at).getTime();
26
+ const now = Date.now();
27
+ const ageInDays = (now - createdDate) / (1000 * 60 * 60 * 24);
28
+
29
+ if (ageInDays > 14) {
30
+ score *= Math.max(0.5, 1 - ageInDays / 60); // Decay over 60 days
31
+ }
32
+
33
+ return Math.min(1, Math.max(0, score));
34
+ }
35
+
36
+ /**
37
+ * Extract template name from insight observation string.
38
+ * Looks for template names in quotes within observation.
39
+ */
40
+ function extractTemplateFromObservation(observation: string): Template | null {
41
+ const templates: Template[] = ['bold-statement', 'split-screen', 'numbered-steps', 'quote-card', 'data-point'];
42
+
43
+ for (const template of templates) {
44
+ // Look for template name in quotes or as part of sentence
45
+ const regex = new RegExp(`["']?${template}["']?`, 'i');
46
+ if (regex.test(observation)) {
47
+ return template;
48
+ }
49
+ }
50
+
51
+ return null;
52
+ }
53
+
54
+ /**
55
+ * Recommend a template/variant based on highest-scored insights.
56
+ * Returns null if no valid recommendations found.
57
+ */
58
+ export function recommendTemplateVariant(
59
+ insights: GeneratedInsight[]
60
+ ): TemplateRecommendation | null {
61
+ // Filter to template-related insights only
62
+ const templateInsights = insights.filter(
63
+ (i) => i.applies_to === 'template'
64
+ );
65
+
66
+ if (templateInsights.length === 0) {
67
+ return null;
68
+ }
69
+
70
+ // Score and sort
71
+ const scored = templateInsights
72
+ .map((insight) => ({
73
+ insight,
74
+ score: scoreRecommendation(insight),
75
+ template: extractTemplateFromObservation(insight.observation),
76
+ }))
77
+ .filter((item) => item.template !== null)
78
+ .sort((a, b) => b.score - a.score);
79
+
80
+ if (scored.length === 0) {
81
+ return null;
82
+ }
83
+
84
+ const best = scored[0];
85
+ return {
86
+ template: best.template!,
87
+ confidence: best.insight.confidence_level,
88
+ reason: best.insight.recommendation,
89
+ score: best.score,
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Filter insights to only high/medium confidence ones suitable for display.
95
+ */
96
+ export function filterHighConfidenceInsights(insights: GeneratedInsight[]): GeneratedInsight[] {
97
+ return insights.filter((i) => i.confidence_level === 'high' || i.confidence_level === 'medium');
98
+ }