heyhank 0.1.0 → 0.2.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.
Files changed (156) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +83 -10
  3. package/bin/cli.ts +7 -7
  4. package/bin/ctl.ts +42 -42
  5. package/dist/assets/{AgentsPage-BPhirnCe.js → AgentsPage-B-AAmsMK.js} +3 -3
  6. package/dist/assets/AssistantPage-BV1Mfwdt.js +2 -0
  7. package/dist/assets/BusinessPage-tLpNEz19.js +1 -0
  8. package/dist/assets/{CronManager-DDbz-yiT.js → CronManager-B-K_n3Jg.js} +1 -1
  9. package/dist/assets/HelpPage-Bhf_j6Xr.js +1 -0
  10. package/dist/assets/{IntegrationsPage-CrOitCmJ.js → IntegrationsPage-DAMjs9tM.js} +1 -1
  11. package/dist/assets/JarvisHUD-C_TGXCCn.js +120 -0
  12. package/dist/assets/MediaPage-C48HTTrt.js +1 -0
  13. package/dist/assets/MemoryPage-JkC-qtgp.js +1 -0
  14. package/dist/assets/{PlatformDashboard-Do6F0O2p.js → PlatformDashboard-AUo7tNnE.js} +1 -1
  15. package/dist/assets/{Playground-Fc5cdc5p.js → Playground-AzNMsRBL.js} +1 -1
  16. package/dist/assets/{ProcessPanel-CslEiZkI.js → ProcessPanel-DpE_2sX3.js} +1 -1
  17. package/dist/assets/{PromptsPage-D2EhsdNO.js → PromptsPage-C2RQOs6p.js} +2 -2
  18. package/dist/assets/RunsPage-B9UOyO79.js +1 -0
  19. package/dist/assets/{SandboxManager-a1AVI5q2.js → SandboxManager-jHvYjwfh.js} +1 -1
  20. package/dist/assets/SettingsPage-BBJax6gt.js +51 -0
  21. package/dist/assets/SkillsMarketplace-IjmjfdjD.js +1 -0
  22. package/dist/assets/SocialMediaPage-DoPZHhr2.js +10 -0
  23. package/dist/assets/{TailscalePage-CHiFhZXF.js → TailscalePage-DDEY7ckO.js} +1 -1
  24. package/dist/assets/TelephonyPage-OPNBZYKt.js +9 -0
  25. package/dist/assets/{TerminalPage-Drwyrnfd.js → TerminalPage-BjMbHHW3.js} +1 -1
  26. package/dist/assets/{gemini-live-client-C7rqAW7G.js → gemini-live-client-C70FEtX2.js} +11 -8
  27. package/dist/assets/{index-CEqZnThB.js → index-BgYM4wXw.js} +94 -93
  28. package/dist/assets/index-BkjSoVgn.css +32 -0
  29. package/dist/assets/sw-register-C7NOHtIu.js +1 -0
  30. package/dist/assets/text-chat-client-BSbLJerZ.js +2 -0
  31. package/dist/index.html +2 -2
  32. package/dist/sw.js +1 -1
  33. package/package.json +6 -1
  34. package/server/agent-executor.ts +37 -2
  35. package/server/agent-store.ts +3 -3
  36. package/server/agent-types.ts +11 -0
  37. package/server/assistant-store.ts +232 -6
  38. package/server/auth-manager.ts +9 -0
  39. package/server/cache-headers.ts +1 -1
  40. package/server/calendar-service.ts +10 -0
  41. package/server/ceo/document-store.ts +129 -0
  42. package/server/ceo/finance-store.ts +343 -0
  43. package/server/ceo/kpi-store.ts +208 -0
  44. package/server/ceo/memory-import.ts +277 -0
  45. package/server/ceo/news-store.ts +208 -0
  46. package/server/ceo/template-store.ts +134 -0
  47. package/server/ceo/time-tracking-store.ts +227 -0
  48. package/server/claude-auth-monitor.ts +128 -0
  49. package/server/claude-code-worker.ts +86 -0
  50. package/server/claude-session-discovery.ts +74 -1
  51. package/server/cli-launcher.ts +32 -10
  52. package/server/codex-adapter.ts +2 -2
  53. package/server/codex-ws-proxy.cjs +1 -1
  54. package/server/container-manager.ts +4 -4
  55. package/server/content-intelligence/content-engine.ts +1112 -0
  56. package/server/content-intelligence/platform-knowledge.ts +870 -0
  57. package/server/cron-store.ts +3 -3
  58. package/server/embedding-service.ts +49 -0
  59. package/server/event-bus-types.ts +13 -0
  60. package/server/federation/node-store.ts +5 -4
  61. package/server/fs-utils.ts +28 -1
  62. package/server/hank-notifications-store.ts +91 -0
  63. package/server/hank-tool-executor.ts +1835 -0
  64. package/server/hank-tools.ts +2107 -0
  65. package/server/image-pull-manager.ts +2 -2
  66. package/server/index.ts +25 -2
  67. package/server/llm-providers-streaming.ts +541 -0
  68. package/server/llm-providers.ts +12 -0
  69. package/server/marketplace.ts +249 -0
  70. package/server/mcp-registry.ts +158 -0
  71. package/server/memory-service.ts +296 -0
  72. package/server/obsidian-sync.ts +184 -0
  73. package/server/provider-manager.ts +5 -2
  74. package/server/provider-registry.ts +12 -0
  75. package/server/reminder-scheduler.ts +37 -1
  76. package/server/routes/agent-routes.ts +2 -1
  77. package/server/routes/assistant-routes.ts +198 -5
  78. package/server/routes/ceo-finance-kpi-routes.ts +167 -0
  79. package/server/routes/ceo-news-time-routes.ts +137 -0
  80. package/server/routes/ceo-routes.ts +99 -0
  81. package/server/routes/content-routes.ts +116 -0
  82. package/server/routes/email-routes.ts +147 -0
  83. package/server/routes/env-routes.ts +3 -3
  84. package/server/routes/fs-routes.ts +12 -9
  85. package/server/routes/hank-chat-routes.ts +592 -0
  86. package/server/routes/llm-routes.ts +12 -0
  87. package/server/routes/marketplace-routes.ts +63 -0
  88. package/server/routes/media-routes.ts +1 -1
  89. package/server/routes/memory-routes.ts +127 -0
  90. package/server/routes/platform-routes.ts +14 -675
  91. package/server/routes/sandbox-routes.ts +1 -1
  92. package/server/routes/settings-routes.ts +51 -1
  93. package/server/routes/socialmedia-routes.ts +152 -2
  94. package/server/routes/system-routes.ts +2 -2
  95. package/server/routes/team-routes.ts +71 -0
  96. package/server/routes/telephony-routes.ts +98 -18
  97. package/server/routes.ts +36 -9
  98. package/server/session-creation-service.ts +2 -2
  99. package/server/session-orchestrator.ts +54 -2
  100. package/server/session-types.ts +2 -0
  101. package/server/settings-manager.ts +50 -2
  102. package/server/skill-discovery.ts +68 -0
  103. package/server/socialmedia/adapters/browser-adapter.ts +179 -0
  104. package/server/socialmedia/adapters/postiz-adapter.ts +291 -14
  105. package/server/socialmedia/manager.ts +234 -15
  106. package/server/socialmedia/store.ts +51 -1
  107. package/server/socialmedia/types.ts +35 -2
  108. package/server/socialview/browser-manager.ts +150 -0
  109. package/server/socialview/extractors.ts +1298 -0
  110. package/server/socialview/image-describe.ts +188 -0
  111. package/server/socialview/library.ts +119 -0
  112. package/server/socialview/poster.ts +276 -0
  113. package/server/socialview/routes.ts +371 -0
  114. package/server/socialview/style-analyzer.ts +187 -0
  115. package/server/socialview/style-profiles.ts +67 -0
  116. package/server/socialview/types.ts +166 -0
  117. package/server/socialview/vision.ts +127 -0
  118. package/server/socialview/vnc-manager.ts +110 -0
  119. package/server/style-injector.ts +135 -0
  120. package/server/team-service.ts +239 -0
  121. package/server/team-store.ts +75 -0
  122. package/server/team-types.ts +52 -0
  123. package/server/telephony/audio-bridge.ts +281 -35
  124. package/server/telephony/audio-recorder.ts +132 -0
  125. package/server/telephony/call-manager.ts +803 -104
  126. package/server/telephony/call-types.ts +67 -1
  127. package/server/telephony/esl-client.ts +319 -0
  128. package/server/telephony/freeswitch-sync.ts +155 -0
  129. package/server/telephony/phone-utils.ts +63 -0
  130. package/server/telephony/telephony-store.ts +9 -8
  131. package/server/url-validator.ts +82 -0
  132. package/server/vault-markdown.ts +317 -0
  133. package/server/vault-migration.ts +121 -0
  134. package/server/vault-store.ts +466 -0
  135. package/server/vault-watcher.ts +59 -0
  136. package/server/vector-store.ts +210 -0
  137. package/server/voice-pipeline/gemini-live-adapter.ts +97 -0
  138. package/server/voice-pipeline/greeting-cache.ts +200 -0
  139. package/server/voice-pipeline/manager.ts +249 -0
  140. package/server/voice-pipeline/pipeline.ts +335 -0
  141. package/server/voice-pipeline/providers/index.ts +47 -0
  142. package/server/voice-pipeline/providers/llm-internal.ts +527 -0
  143. package/server/voice-pipeline/providers/stt-google.ts +157 -0
  144. package/server/voice-pipeline/providers/tts-google.ts +126 -0
  145. package/server/voice-pipeline/types.ts +247 -0
  146. package/server/ws-bridge-types.ts +6 -1
  147. package/dist/assets/AssistantPage-DJ-cMQfb.js +0 -1
  148. package/dist/assets/HelpPage-DMfkzERp.js +0 -1
  149. package/dist/assets/MediaPage-CE5rdvkC.js +0 -1
  150. package/dist/assets/RunsPage-C5BZF5Rx.js +0 -1
  151. package/dist/assets/SettingsPage-DirhjQrJ.js +0 -51
  152. package/dist/assets/SocialMediaPage-DBuM28vD.js +0 -1
  153. package/dist/assets/TelephonyPage-x0VV0fOo.js +0 -1
  154. package/dist/assets/index-C8M_PUmX.css +0 -32
  155. package/dist/assets/sw-register-LSSpj6RU.js +0 -1
  156. package/server/socialmedia/adapters/ayrshare-adapter.ts +0 -169
@@ -0,0 +1,1112 @@
1
+ // ─── Content Engine / Ad Creator ────────────────────────────────────────────
2
+ // Comprehensive social media content generation system.
3
+ // Analyzes websites, creates content strategies, and generates
4
+ // platform-optimized content and ad creatives.
5
+
6
+ import { randomUUID } from "node:crypto";
7
+ import { callInternalAI } from "../internal-ai.js";
8
+ import { selectForFewShot } from "../socialview/library.js";
9
+ import { getProfile as getStyleProfile } from "../socialview/style-profiles.js";
10
+ import type { SocialPlatform, StyleProfile } from "../socialview/types.js";
11
+ import { SOCIAL_PLATFORMS } from "../socialview/types.js";
12
+ import {
13
+ getPlatform,
14
+ buildPlatformSummary,
15
+ ALL_PLATFORMS,
16
+ type PlatformSpec,
17
+ } from "./platform-knowledge.js";
18
+ import { listHashtagPools } from "../socialmedia/store.js";
19
+
20
+ // ─── Types ──────────────────────────────────────────────────────────────────
21
+
22
+ export interface WebsiteIntelligence {
23
+ url: string;
24
+ businessType: "ecommerce" | "service" | "saas" | "blog" | "portfolio" | "agency" | "other";
25
+ industry: string;
26
+ companyName: string;
27
+ language: string;
28
+ products: Array<{ name: string; description: string; price?: string; imageUrl?: string }>;
29
+ services: Array<{ name: string; description: string }>;
30
+ usp: string[];
31
+ targetAudience: string;
32
+ tone: string;
33
+ colors: string[];
34
+ fonts: string[];
35
+ logo?: { url: string; alt?: string };
36
+ heroImages: Array<{ url: string; alt?: string }>;
37
+ productImages: Array<{ url: string; alt?: string }>;
38
+ headlines: string[];
39
+ ctas: string[];
40
+ testimonials: string[];
41
+ title: string;
42
+ description: string;
43
+ ogImage?: string;
44
+ crawledPages: string[];
45
+ analyzedAt: string;
46
+ }
47
+
48
+ export interface ContentPillar {
49
+ name: string;
50
+ description: string;
51
+ painPoints: string[];
52
+ contentIdeas: string[];
53
+ }
54
+
55
+ export interface ContentSchedule {
56
+ platform: string;
57
+ postsPerWeek: number;
58
+ bestDays: string[];
59
+ bestHours: string;
60
+ formats: string[];
61
+ }
62
+
63
+ export interface ContentStrategy {
64
+ businessType: string;
65
+ pillars: ContentPillar[];
66
+ schedules: ContentSchedule[];
67
+ tone: string;
68
+ ctas: string[];
69
+ journeyMapping: {
70
+ attract: string[];
71
+ convert: string[];
72
+ close: string[];
73
+ };
74
+ }
75
+
76
+ export type JourneyStage = "attract" | "convert" | "close";
77
+ export type CopyFramework = "PAS" | "AIDA" | "BAB" | "StoryBrand";
78
+
79
+ export interface ContentPiece {
80
+ id: string;
81
+ platform: string;
82
+ type: "social-post" | "blog" | "newsletter" | "ad";
83
+ journeyStage: JourneyStage;
84
+ framework: CopyFramework;
85
+ pillar: string;
86
+ targetPain: string;
87
+ hook: string;
88
+ headline: string;
89
+ body: string;
90
+ cta: string;
91
+ hashtags: string[];
92
+ imagePrompt?: string;
93
+ imageUrl?: string;
94
+ scheduledFor?: string;
95
+ status: "draft" | "review" | "approved" | "published";
96
+ }
97
+
98
+ export interface AdCreative {
99
+ id: string;
100
+ platform: string;
101
+ format: string;
102
+ aspectRatio: string;
103
+ resolution: string;
104
+ headline: string;
105
+ body: string;
106
+ cta: string;
107
+ imagePrompt: string;
108
+ brandColors: string[];
109
+ tone: string;
110
+ }
111
+
112
+ // ─── Cache ──────────────────────────────────────────────────────────────────
113
+
114
+ const analysisCache = new Map<string, { data: WebsiteIntelligence; expiresAt: number }>();
115
+ const CACHE_TTL_MS = 30 * 60 * 1000; // 30 minutes
116
+
117
+ function getCached(url: string): WebsiteIntelligence | null {
118
+ const entry = analysisCache.get(url);
119
+ if (!entry) return null;
120
+ if (Date.now() > entry.expiresAt) {
121
+ analysisCache.delete(url);
122
+ return null;
123
+ }
124
+ return entry.data;
125
+ }
126
+
127
+ function setCache(url: string, data: WebsiteIntelligence): void {
128
+ analysisCache.set(url, { data, expiresAt: Date.now() + CACHE_TTL_MS });
129
+ }
130
+
131
+ // ─── HTML Parsing Helpers ───────────────────────────────────────────────────
132
+
133
+ function extractMetaTags(html: string): Record<string, string> {
134
+ const tags: Record<string, string> = {};
135
+ const metaRegex = /<meta\s+[^>]*?(?:name|property|http-equiv)\s*=\s*["']([^"']+)["'][^>]*?content\s*=\s*["']([^"']*?)["'][^>]*?\/?>/gi;
136
+ const metaRegex2 = /<meta\s+[^>]*?content\s*=\s*["']([^"']*?)["'][^>]*?(?:name|property)\s*=\s*["']([^"']+)["'][^>]*?\/?>/gi;
137
+ let match: RegExpExecArray | null;
138
+ while ((match = metaRegex.exec(html)) !== null) {
139
+ tags[match[1].toLowerCase()] = match[2];
140
+ }
141
+ while ((match = metaRegex2.exec(html)) !== null) {
142
+ tags[match[2].toLowerCase()] = match[1];
143
+ }
144
+ // Title tag
145
+ const titleMatch = html.match(/<title[^>]*>([^<]*)<\/title>/i);
146
+ if (titleMatch) tags["title"] = titleMatch[1].trim();
147
+ return tags;
148
+ }
149
+
150
+ function extractImages(html: string, baseUrl: string): Array<{ src: string; alt?: string; width?: number; height?: number }> {
151
+ const images: Array<{ src: string; alt?: string; width?: number; height?: number }> = [];
152
+ const imgRegex = /<img\s+[^>]*?src\s*=\s*["']([^"']+)["'][^>]*?\/?>/gi;
153
+ let match: RegExpExecArray | null;
154
+ while ((match = imgRegex.exec(html)) !== null) {
155
+ const tag = match[0];
156
+ const src = resolveUrl(match[1], baseUrl);
157
+ if (!src) continue;
158
+ const altMatch = tag.match(/alt\s*=\s*["']([^"']*?)["']/i);
159
+ const widthMatch = tag.match(/width\s*=\s*["']?(\d+)/i);
160
+ const heightMatch = tag.match(/height\s*=\s*["']?(\d+)/i);
161
+ images.push({
162
+ src,
163
+ alt: altMatch?.[1] || undefined,
164
+ width: widthMatch ? parseInt(widthMatch[1], 10) : undefined,
165
+ height: heightMatch ? parseInt(heightMatch[1], 10) : undefined,
166
+ });
167
+ }
168
+ return images;
169
+ }
170
+
171
+ function extractLinks(html: string, baseUrl: string): string[] {
172
+ const links: string[] = [];
173
+ const linkRegex = /<a\s+[^>]*?href\s*=\s*["']([^"'#]+)["'][^>]*?>/gi;
174
+ let match: RegExpExecArray | null;
175
+ const baseHostname = getHostname(baseUrl);
176
+ while ((match = linkRegex.exec(html)) !== null) {
177
+ const href = resolveUrl(match[1], baseUrl);
178
+ if (!href) continue;
179
+ const hrefHostname = getHostname(href);
180
+ if (hrefHostname === baseHostname) {
181
+ links.push(href);
182
+ }
183
+ }
184
+ return [...new Set(links)];
185
+ }
186
+
187
+ function extractHeadings(html: string): string[] {
188
+ const headings: string[] = [];
189
+ const headingRegex = /<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi;
190
+ let match: RegExpExecArray | null;
191
+ while ((match = headingRegex.exec(html)) !== null) {
192
+ const text = stripHtml(match[1]).trim();
193
+ if (text) headings.push(text);
194
+ }
195
+ return headings;
196
+ }
197
+
198
+ function extractParagraphs(html: string): string[] {
199
+ const paragraphs: string[] = [];
200
+ const pRegex = /<p[^>]*>([\s\S]*?)<\/p>/gi;
201
+ let match: RegExpExecArray | null;
202
+ while ((match = pRegex.exec(html)) !== null) {
203
+ const text = stripHtml(match[1]).trim();
204
+ if (text && text.length > 20) paragraphs.push(text);
205
+ }
206
+ return paragraphs;
207
+ }
208
+
209
+ function extractCssColors(html: string): string[] {
210
+ const colors: string[] = [];
211
+ const colorRegex = /#(?:[0-9a-fA-F]{3}){1,2}\b/g;
212
+ const rgbRegex = /rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)/g;
213
+ let match: RegExpExecArray | null;
214
+ while ((match = colorRegex.exec(html)) !== null) {
215
+ colors.push(match[0]);
216
+ }
217
+ while ((match = rgbRegex.exec(html)) !== null) {
218
+ colors.push(match[0]);
219
+ }
220
+ // Deduplicate and take top 10
221
+ return [...new Set(colors)].slice(0, 10);
222
+ }
223
+
224
+ function extractLogoUrl(html: string, images: Array<{ src: string; alt?: string }>): { url: string; alt?: string } | undefined {
225
+ // Look for images with "logo" in src, alt, or class
226
+ const logoRegex = /<img\s+[^>]*?(?:src|alt|class)\s*=\s*["'][^"']*logo[^"']*["'][^>]*?>/gi;
227
+ let match: RegExpExecArray | null;
228
+ while ((match = logoRegex.exec(html)) !== null) {
229
+ const srcMatch = match[0].match(/src\s*=\s*["']([^"']+)["']/i);
230
+ const altMatch = match[0].match(/alt\s*=\s*["']([^"']*?)["']/i);
231
+ if (srcMatch) {
232
+ return { url: srcMatch[1], alt: altMatch?.[1] };
233
+ }
234
+ }
235
+ // Fallback: look in images array
236
+ for (const img of images) {
237
+ if (img.src.toLowerCase().includes("logo") || img.alt?.toLowerCase().includes("logo")) {
238
+ return { url: img.src, alt: img.alt };
239
+ }
240
+ }
241
+ return undefined;
242
+ }
243
+
244
+ function stripHtml(html: string): string {
245
+ return html.replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/\s+/g, " ");
246
+ }
247
+
248
+ function resolveUrl(href: string, base: string): string | null {
249
+ try {
250
+ return new URL(href, base).href;
251
+ } catch {
252
+ return null;
253
+ }
254
+ }
255
+
256
+ function getHostname(url: string): string {
257
+ try {
258
+ return new URL(url).hostname;
259
+ } catch {
260
+ return "";
261
+ }
262
+ }
263
+
264
+ // ─── Website Fetching ───────────────────────────────────────────────────────
265
+
266
+ async function fetchPage(url: string): Promise<string | null> {
267
+ try {
268
+ const controller = new AbortController();
269
+ const timeout = setTimeout(() => controller.abort(), 10_000);
270
+ const response = await fetch(url, {
271
+ headers: {
272
+ "User-Agent": "HeyHank-ContentEngine/1.0 (https://heyhank.com)",
273
+ "Accept": "text/html,application/xhtml+xml",
274
+ },
275
+ signal: controller.signal,
276
+ redirect: "follow",
277
+ });
278
+ clearTimeout(timeout);
279
+ if (!response.ok) return null;
280
+ const contentType = response.headers.get("content-type") || "";
281
+ if (!contentType.includes("text/html") && !contentType.includes("text/plain") && !contentType.includes("application/xhtml")) {
282
+ return null;
283
+ }
284
+ return await response.text();
285
+ } catch {
286
+ return null;
287
+ }
288
+ }
289
+
290
+ // ─── Priority pages to crawl ────────────────────────────────────────────────
291
+
292
+ const PRIORITY_PATHS = ["/about", "/services", "/products", "/pricing", "/contact", "/about-us", "/our-services", "/ueber-uns", "/leistungen", "/produkte", "/kontakt"];
293
+
294
+ function prioritizeLinks(links: string[], baseUrl: string): string[] {
295
+ const base = baseUrl.replace(/\/$/, "");
296
+ const prioritized: string[] = [];
297
+ const rest: string[] = [];
298
+
299
+ for (const link of links) {
300
+ if (link === baseUrl || link === base || link === base + "/") continue;
301
+ const path = new URL(link).pathname.toLowerCase();
302
+ if (PRIORITY_PATHS.some((p) => path === p || path === p + "/")) {
303
+ prioritized.push(link);
304
+ } else {
305
+ rest.push(link);
306
+ }
307
+ }
308
+
309
+ return [...prioritized, ...rest].slice(0, 5);
310
+ }
311
+
312
+ // ─── LLM Analysis ──────────────────────────────────────────────────────────
313
+
314
+ async function analyzeWithLLM(collectedData: {
315
+ url: string;
316
+ meta: Record<string, string>;
317
+ headings: string[];
318
+ paragraphs: string[];
319
+ images: Array<{ src: string; alt?: string }>;
320
+ colors: string[];
321
+ }): Promise<Partial<WebsiteIntelligence>> {
322
+ const prompt = `Analyze this website data and extract business intelligence. Return ONLY valid JSON.
323
+
324
+ URL: ${collectedData.url}
325
+
326
+ META TAGS:
327
+ ${Object.entries(collectedData.meta).map(([k, v]) => `${k}: ${v}`).join("\n")}
328
+
329
+ HEADINGS:
330
+ ${collectedData.headings.slice(0, 30).join("\n")}
331
+
332
+ CONTENT (first paragraphs):
333
+ ${collectedData.paragraphs.slice(0, 20).join("\n\n")}
334
+
335
+ IMAGES (${collectedData.images.length} total):
336
+ ${collectedData.images.slice(0, 15).map((i) => `${i.src} (alt: ${i.alt || "none"})`).join("\n")}
337
+
338
+ COLORS FOUND: ${collectedData.colors.join(", ")}
339
+
340
+ Respond with this exact JSON structure (no markdown, no explanation):
341
+ {
342
+ "businessType": "ecommerce|service|saas|blog|portfolio|agency|other",
343
+ "industry": "specific industry name",
344
+ "companyName": "company name",
345
+ "language": "detected language code (e.g. en, de, fr)",
346
+ "products": [{"name": "...", "description": "...", "price": "..."}],
347
+ "services": [{"name": "...", "description": "..."}],
348
+ "usp": ["unique selling point 1", "..."],
349
+ "targetAudience": "description of target audience",
350
+ "tone": "brand tone of voice (e.g. professional, friendly, casual, authoritative)",
351
+ "testimonials": ["testimonial quote 1", "..."],
352
+ "ctas": ["call to action text found on site"]
353
+ }`;
354
+
355
+ const result = await callInternalAI({
356
+ systemPrompt: "You are a business intelligence analyst. Extract structured data from websites. Return ONLY valid JSON, no markdown fences, no explanation.",
357
+ userPrompt: prompt,
358
+ maxTokens: 2048,
359
+ temperature: 0.3,
360
+ timeoutMs: 30_000,
361
+ });
362
+
363
+ if (!result.ok || !result.text) {
364
+ console.error("[content-engine] LLM analysis failed:", result.error);
365
+ return {};
366
+ }
367
+
368
+ try {
369
+ // Try to extract JSON from the response (handle markdown fences)
370
+ let jsonText = result.text.trim();
371
+ const fenceMatch = jsonText.match(/```(?:json)?\s*([\s\S]*?)```/);
372
+ if (fenceMatch) jsonText = fenceMatch[1].trim();
373
+ return JSON.parse(jsonText) as Partial<WebsiteIntelligence>;
374
+ } catch (e) {
375
+ console.error("[content-engine] Failed to parse LLM response:", e);
376
+ return {};
377
+ }
378
+ }
379
+
380
+ // ─── Main Functions ─────────────────────────────────────────────────────────
381
+
382
+ /**
383
+ * Analyze a website to extract brand identity, business type, products/services,
384
+ * colors, images, and tone of voice.
385
+ */
386
+ export async function analyzeWebsite(url: string): Promise<WebsiteIntelligence> {
387
+ // Check cache first
388
+ const cached = getCached(url);
389
+ if (cached) return cached;
390
+
391
+ console.log(`[content-engine] Analyzing website: ${url}`);
392
+
393
+ // Normalize URL
394
+ if (!url.startsWith("http")) url = "https://" + url;
395
+
396
+ // 1. Fetch main page
397
+ const mainHtml = await fetchPage(url);
398
+ if (!mainHtml) {
399
+ throw new Error(`Could not fetch website: ${url}`);
400
+ }
401
+
402
+ // 2. Extract data from main page
403
+ const meta = extractMetaTags(mainHtml);
404
+ const allImages = extractImages(mainHtml, url);
405
+ const links = extractLinks(mainHtml, url);
406
+ const headings = extractHeadings(mainHtml);
407
+ const paragraphs = extractParagraphs(mainHtml);
408
+ const colors = extractCssColors(mainHtml);
409
+ const logo = extractLogoUrl(mainHtml, allImages);
410
+
411
+ // 3. Crawl additional pages
412
+ const additionalPages = prioritizeLinks(links, url);
413
+ const crawledPages = [url];
414
+ const additionalHeadings: string[] = [];
415
+ const additionalParagraphs: string[] = [];
416
+ const additionalImages: typeof allImages = [];
417
+
418
+ for (const pageUrl of additionalPages) {
419
+ const pageHtml = await fetchPage(pageUrl);
420
+ if (!pageHtml) continue;
421
+ crawledPages.push(pageUrl);
422
+ additionalHeadings.push(...extractHeadings(pageHtml));
423
+ additionalParagraphs.push(...extractParagraphs(pageHtml));
424
+ additionalImages.push(...extractImages(pageHtml, pageUrl));
425
+ }
426
+
427
+ const combinedHeadings = [...headings, ...additionalHeadings];
428
+ const combinedParagraphs = [...paragraphs, ...additionalParagraphs];
429
+ const combinedImages = [...allImages, ...additionalImages];
430
+
431
+ // 4. Filter images (skip tiny icons <100px)
432
+ const significantImages = combinedImages.filter((img) => {
433
+ if (img.width && img.width < 100) return false;
434
+ if (img.height && img.height < 100) return false;
435
+ const src = img.src.toLowerCase();
436
+ if (src.includes("favicon") || src.includes("icon") || src.endsWith(".ico")) return false;
437
+ if (src.includes("pixel") || src.includes("tracking") || src.includes("analytics")) return false;
438
+ return true;
439
+ });
440
+
441
+ // 5. Send to LLM for analysis
442
+ const llmAnalysis = await analyzeWithLLM({
443
+ url,
444
+ meta,
445
+ headings: combinedHeadings,
446
+ paragraphs: combinedParagraphs,
447
+ images: significantImages.slice(0, 20),
448
+ colors,
449
+ });
450
+
451
+ // 6. Build the intelligence object
452
+ const intelligence: WebsiteIntelligence = {
453
+ url,
454
+ businessType: llmAnalysis.businessType || "other",
455
+ industry: llmAnalysis.industry || "Unknown",
456
+ companyName: llmAnalysis.companyName || meta["og:site_name"] || meta["title"] || "Unknown",
457
+ language: llmAnalysis.language || "en",
458
+ products: llmAnalysis.products || [],
459
+ services: llmAnalysis.services || [],
460
+ usp: llmAnalysis.usp || [],
461
+ targetAudience: llmAnalysis.targetAudience || "General audience",
462
+ tone: llmAnalysis.tone || "professional",
463
+ colors,
464
+ fonts: [], // Would need CSS parsing for fonts
465
+ logo: logo ? { url: resolveUrl(logo.url, url) || logo.url, alt: logo.alt } : undefined,
466
+ heroImages: significantImages.slice(0, 5).map((i) => ({ url: i.src, alt: i.alt })),
467
+ productImages: significantImages
468
+ .filter((i) => i.alt?.toLowerCase().includes("product") || i.src.toLowerCase().includes("product"))
469
+ .slice(0, 10)
470
+ .map((i) => ({ url: i.src, alt: i.alt })),
471
+ headlines: combinedHeadings.slice(0, 20),
472
+ ctas: llmAnalysis.ctas || [],
473
+ testimonials: llmAnalysis.testimonials || [],
474
+ title: meta["title"] || meta["og:title"] || "",
475
+ description: meta["description"] || meta["og:description"] || "",
476
+ ogImage: meta["og:image"] || undefined,
477
+ crawledPages,
478
+ analyzedAt: new Date().toISOString(),
479
+ };
480
+
481
+ // Cache it
482
+ setCache(url, intelligence);
483
+
484
+ return intelligence;
485
+ }
486
+
487
+ // ─── Content Strategy ───────────────────────────────────────────────────────
488
+
489
+ const PILLAR_TEMPLATES: Record<string, ContentPillar[]> = {
490
+ ecommerce: [
491
+ {
492
+ name: "Product Highlights",
493
+ description: "Showcase products, features, and benefits",
494
+ painPoints: ["Can't find quality products", "Unsure about product quality", "Too many choices"],
495
+ contentIdeas: ["Product spotlights", "Feature breakdowns", "Use cases", "Comparison posts"],
496
+ },
497
+ {
498
+ name: "Customer Stories",
499
+ description: "Social proof through customer experiences",
500
+ painPoints: ["Need validation before buying", "Want real reviews", "Risk of bad purchase"],
501
+ contentIdeas: ["Customer testimonials", "Before/after transformations", "User-generated content", "Review roundups"],
502
+ },
503
+ {
504
+ name: "Behind the Scenes",
505
+ description: "Build trust through transparency",
506
+ painPoints: ["Don't trust online brands", "Want to know who makes products"],
507
+ contentIdeas: ["Team introductions", "Production process", "Packaging and shipping", "Company values"],
508
+ },
509
+ {
510
+ name: "Industry Tips",
511
+ description: "Position as helpful expert",
512
+ painPoints: ["Need guidance on product use", "Want to maximize value"],
513
+ contentIdeas: ["How-to guides", "Tips and tricks", "Seasonal guides", "Expert advice"],
514
+ },
515
+ ],
516
+ service: [
517
+ {
518
+ name: "Expertise & Tips",
519
+ description: "Demonstrate authority and provide value",
520
+ painPoints: ["Don't know where to start", "Need expert guidance", "Information overload"],
521
+ contentIdeas: ["Quick tips", "Common mistakes", "Step-by-step guides", "FAQ answers"],
522
+ },
523
+ {
524
+ name: "Case Studies",
525
+ description: "Prove results with real examples",
526
+ painPoints: ["Skeptical about results", "Need proof it works"],
527
+ contentIdeas: ["Client success stories", "Before/after results", "Process breakdowns", "ROI showcases"],
528
+ },
529
+ {
530
+ name: "Behind the Scenes",
531
+ description: "Humanize the brand",
532
+ painPoints: ["Want to know who they're working with", "Need personal connection"],
533
+ contentIdeas: ["Day in the life", "Team spotlights", "Office/workspace tours", "Company culture"],
534
+ },
535
+ {
536
+ name: "Industry Insights",
537
+ description: "Position as thought leader",
538
+ painPoints: ["Need to stay current", "Want informed decisions"],
539
+ contentIdeas: ["Trend analysis", "Industry news commentary", "Data-driven insights", "Predictions"],
540
+ },
541
+ ],
542
+ saas: [
543
+ {
544
+ name: "Feature Highlights",
545
+ description: "Showcase product capabilities",
546
+ painPoints: ["Current tools are inefficient", "Need better solutions", "Too complex"],
547
+ contentIdeas: ["Feature demos", "Tips and shortcuts", "New feature announcements", "Integration showcases"],
548
+ },
549
+ {
550
+ name: "Tutorials",
551
+ description: "Help users get maximum value",
552
+ painPoints: ["Hard to learn new tools", "Underusing the product"],
553
+ contentIdeas: ["Step-by-step tutorials", "Use case walkthroughs", "Power user tips", "Template showcases"],
554
+ },
555
+ {
556
+ name: "Industry Trends",
557
+ description: "Position as forward-thinking leader",
558
+ painPoints: ["Falling behind competitors", "Need to stay current"],
559
+ contentIdeas: ["Market analysis", "Technology trends", "Future predictions", "Data reports"],
560
+ },
561
+ {
562
+ name: "Customer Success",
563
+ description: "Social proof and inspiration",
564
+ painPoints: ["Unsure if the tool will work for them", "Need validation"],
565
+ contentIdeas: ["Success stories", "User interviews", "ROI case studies", "Community highlights"],
566
+ },
567
+ ],
568
+ };
569
+
570
+ /**
571
+ * Create a content strategy based on business analysis and target platforms.
572
+ */
573
+ export function createContentStrategy(
574
+ intelligence: WebsiteIntelligence,
575
+ platforms: string[],
576
+ ): ContentStrategy {
577
+ // 1. Get pillar templates based on business type
578
+ const pillars = PILLAR_TEMPLATES[intelligence.businessType] || PILLAR_TEMPLATES["service"]!;
579
+
580
+ // 2. Create posting schedules per platform
581
+ const schedules: ContentSchedule[] = [];
582
+ for (const platformKey of platforms) {
583
+ const spec = getPlatform(platformKey);
584
+ if (!spec) continue;
585
+ schedules.push({
586
+ platform: spec.key,
587
+ postsPerWeek: parsePostsPerWeek(spec.frequency.recommended),
588
+ bestDays: spec.bestTimes.bestDays,
589
+ bestHours: spec.bestTimes.bestHours,
590
+ formats: spec.formats.slice(0, 3).map((f) => f.name),
591
+ });
592
+ }
593
+
594
+ // 3. Define journey mapping
595
+ const journeyMapping = {
596
+ attract: [
597
+ "Educational content addressing pain points",
598
+ "Industry insights and trends",
599
+ "Entertaining/engaging content",
600
+ "Shareable infographics and tips",
601
+ ],
602
+ convert: [
603
+ "Case studies and success stories",
604
+ "Product/service comparisons",
605
+ "Free resources (guides, templates)",
606
+ "Webinars and live demos",
607
+ ],
608
+ close: [
609
+ "Testimonials and social proof",
610
+ "Limited-time offers",
611
+ "Free trials and demos",
612
+ "Direct CTAs with clear value proposition",
613
+ ],
614
+ };
615
+
616
+ // 4. Generate CTAs based on business type
617
+ const ctas = intelligence.ctas.length > 0
618
+ ? intelligence.ctas
619
+ : generateDefaultCTAs(intelligence.businessType);
620
+
621
+ return {
622
+ businessType: intelligence.businessType,
623
+ pillars,
624
+ schedules,
625
+ tone: intelligence.tone,
626
+ ctas,
627
+ journeyMapping,
628
+ };
629
+ }
630
+
631
+ function parsePostsPerWeek(recommended: string): number {
632
+ const match = recommended.match(/(\d+)(?:\s*-\s*(\d+))?/);
633
+ if (!match) return 3;
634
+ const low = parseInt(match[1], 10);
635
+ const high = match[2] ? parseInt(match[2], 10) : low;
636
+ return Math.round((low + high) / 2);
637
+ }
638
+
639
+ function generateDefaultCTAs(businessType: string): string[] {
640
+ switch (businessType) {
641
+ case "ecommerce":
642
+ return ["Shop Now", "Get Yours Today", "Limited Stock", "Free Shipping"];
643
+ case "service":
644
+ return ["Book a Consultation", "Get Started", "Contact Us", "Learn More"];
645
+ case "saas":
646
+ return ["Start Free Trial", "See Demo", "Sign Up Free", "Try It Now"];
647
+ default:
648
+ return ["Learn More", "Get Started", "Contact Us"];
649
+ }
650
+ }
651
+
652
+ // ─── Hashtag Pool Integration ───────────────────────────────────────────────
653
+
654
+ /**
655
+ * Build hashtag context from saved pools for the LLM prompt.
656
+ * Matches by industry/language if possible, otherwise uses all pools.
657
+ */
658
+ /**
659
+ * Build a few-shot reference block from the SocialView library for the given
660
+ * platform. Only gold-marked posts (highest-engagement, manually approved) are
661
+ * used so the agent learns from curated examples, not noise.
662
+ * Returns "" if platform is not one of the supported social platforms or if
663
+ * the library has no gold posts for it.
664
+ */
665
+ function buildFewShotBlock(platform: string): string {
666
+ // Normalize content-engine platform strings to SocialPlatform union.
667
+ const plat = normalizePlatform(platform);
668
+ if (!plat) return "";
669
+
670
+ const examples = selectForFewShot(plat, 5);
671
+ if (examples.length === 0) return "";
672
+
673
+ const lines: string[] = [
674
+ "",
675
+ `REFERENCE POSTS (top-performing examples from the ${plat} library — study the hook pattern, tone, length, CTA style, and visual direction; do NOT copy verbatim):`,
676
+ ];
677
+ for (const p of examples) {
678
+ lines.push("---");
679
+ if (p.engagementRate !== null) {
680
+ lines.push(`engagement_rate=${p.engagementRate.toFixed(3)} source=${p.source}`);
681
+ } else {
682
+ lines.push(`source=${p.source}`);
683
+ }
684
+ if (p.hook) lines.push(`hook: ${p.hook}`);
685
+ if (p.cta) lines.push(`cta: ${p.cta}`);
686
+ if (p.text) lines.push(`body: ${p.text.slice(0, 500)}`);
687
+ if (p.hashtags.length) lines.push(`hashtags: ${p.hashtags.map((h) => "#" + h).join(" ")}`);
688
+ const visual = p.media.find((m) => m.description)?.description;
689
+ if (visual) lines.push(`visual: ${visual.slice(0, 240)}`);
690
+ if (p.tags.length) lines.push(`tags: ${p.tags.join(", ")}`);
691
+ }
692
+ lines.push("---");
693
+ return lines.join("\n");
694
+ }
695
+
696
+ function normalizePlatform(platform: string): SocialPlatform | null {
697
+ const p = platform.toLowerCase();
698
+ if (p === "x") return "twitter";
699
+ if (SOCIAL_PLATFORMS.includes(p as SocialPlatform)) return p as SocialPlatform;
700
+ return null;
701
+ }
702
+
703
+ /**
704
+ * Render a `StyleProfile` as an instruction block for the generation prompt.
705
+ * Token-efficient: structured rules, not raw post examples (those still come
706
+ * from `buildFewShotBlock`).
707
+ */
708
+ function buildStyleProfileBlock(profile: StyleProfile): string {
709
+ const lines: string[] = [];
710
+ lines.push("");
711
+ lines.push(
712
+ `STYLE PROFILE — schreibe im Stil von ${profile.displayName} (@${profile.handle}, ${profile.platform}). ` +
713
+ `Imitiere Stil und Struktur, NICHT Inhalte. Diese Person ist die Vorlage:`,
714
+ );
715
+ lines.push(`- Tonfall: ${profile.toneOfVoice || "nicht spezifiziert"}`);
716
+ lines.push(
717
+ `- Länge: ~${profile.averageWordCount} Wörter (${profile.lengthCategory}). Nicht signifikant abweichen.`,
718
+ );
719
+
720
+ if (profile.hookPatterns.length > 0) {
721
+ const top = profile.hookPatterns
722
+ .slice()
723
+ .sort((a, b) => b.frequency - a.frequency)
724
+ .slice(0, 3)
725
+ .map((h) => `${h.type} (${Math.round(h.frequency * 100)}%)`)
726
+ .join(", ");
727
+ lines.push(`- Bevorzugte Hook-Pattern: ${top}`);
728
+ const exampleHook = profile.hookPatterns[0]?.examples?.[0];
729
+ if (exampleHook) lines.push(` Beispiel-Hook: "${exampleHook}"`);
730
+ }
731
+
732
+ if (profile.ctaPatterns.length > 0) {
733
+ const top = profile.ctaPatterns
734
+ .slice()
735
+ .sort((a, b) => b.frequency - a.frequency)
736
+ .slice(0, 2)
737
+ .map((c) => `${c.type} (${Math.round(c.frequency * 100)}%)`)
738
+ .join(", ");
739
+ lines.push(`- CTA-Pattern: ${top}`);
740
+ }
741
+
742
+ lines.push(
743
+ `- Emoji-Stil: ${profile.emojiStyle}` +
744
+ (profile.emojiList.length > 0 ? ` — typisch: ${profile.emojiList.slice(0, 6).join(" ")}` : ""),
745
+ );
746
+ lines.push(`- Hashtag-Stil: ${profile.hashtagStyle}`);
747
+
748
+ if (profile.contentPillars.length > 0) {
749
+ lines.push(`- Themen-Säulen: ${profile.contentPillars.join(", ")}`);
750
+ }
751
+
752
+ if (profile.commentEngagementPattern) {
753
+ lines.push(`- Engagement-Trick (Eigenkommentare): ${profile.commentEngagementPattern}`);
754
+ }
755
+
756
+ if (profile.rawAnalysis) {
757
+ lines.push(`- Stil-Zusammenfassung: ${profile.rawAnalysis}`);
758
+ }
759
+
760
+ return lines.join("\n");
761
+ }
762
+
763
+ async function getHashtagPoolContext(industry: string, language: string): Promise<string> {
764
+ try {
765
+ const pools = listHashtagPools();
766
+ if (pools.length === 0) return "";
767
+
768
+ // Try to find matching pool by industry, fall back to all
769
+ const matching = pools.filter(
770
+ (p) =>
771
+ p.industry.toLowerCase() === industry.toLowerCase() ||
772
+ p.language === language
773
+ );
774
+ const selected = matching.length > 0 ? matching : pools;
775
+
776
+ const lines: string[] = [
777
+ "HASHTAG POOL (use these curated hashtags, mix popular + medium + niche):",
778
+ ];
779
+ for (const pool of selected) {
780
+ lines.push(` Business: ${pool.name} (${pool.industry})`);
781
+ if (pool.popular.length > 0) lines.push(` Popular (high reach): ${pool.popular.join(", ")}`);
782
+ if (pool.medium.length > 0) lines.push(` Medium (balanced): ${pool.medium.join(", ")}`);
783
+ if (pool.niche.length > 0) lines.push(` Niche (targeted): ${pool.niche.join(", ")}`);
784
+ if (pool.branded.length > 0) lines.push(` Branded: ${pool.branded.join(", ")}`);
785
+ if (pool.blocked.length > 0) lines.push(` NEVER USE: ${pool.blocked.join(", ")}`);
786
+ }
787
+ lines.push("- Pick 1-2 popular + 1-2 medium + 1-2 niche per post. Always include 1 branded if available.");
788
+ lines.push("- You may add 1 situational hashtag that fits the specific post topic.");
789
+ return lines.join("\n");
790
+ } catch {
791
+ return "";
792
+ }
793
+ }
794
+
795
+ // ─── Smart Content Generation ───────────────────────────────────────────────
796
+
797
+ const COPY_FRAMEWORKS: CopyFramework[] = ["PAS", "AIDA", "BAB", "StoryBrand"];
798
+
799
+ /**
800
+ * Generate platform-optimized content pieces.
801
+ */
802
+ export async function generateSmartContent(opts: {
803
+ intelligence: WebsiteIntelligence;
804
+ strategy: ContentStrategy;
805
+ platform: string;
806
+ journeyStage?: JourneyStage;
807
+ count?: number;
808
+ /**
809
+ * Handle of a SocialView role-model whose `StyleProfile` should drive the
810
+ * voice/structure of the generated posts. Pass e.g. "rene.remsik" to write
811
+ * "im Stil von Rene Remsik". If the profile doesn't exist for the given
812
+ * platform/handle, generation falls back to default few-shot only.
813
+ */
814
+ styleProfileHandle?: string;
815
+ }): Promise<ContentPiece[]> {
816
+ const { intelligence, strategy, platform, journeyStage, count = 5, styleProfileHandle } = opts;
817
+
818
+ const spec = getPlatform(platform);
819
+ if (!spec) {
820
+ throw new Error(`Unknown platform: ${platform}`);
821
+ }
822
+
823
+ const platformSummary = buildPlatformSummary(platform);
824
+ const fewShot = buildFewShotBlock(platform);
825
+
826
+ // Optional: pull a saved style profile for the requested handle.
827
+ let styleBlock = "";
828
+ if (styleProfileHandle) {
829
+ const plat = normalizePlatform(platform);
830
+ if (plat) {
831
+ const profile = getStyleProfile(plat, styleProfileHandle);
832
+ if (profile) styleBlock = buildStyleProfileBlock(profile);
833
+ }
834
+ }
835
+ const stage = journeyStage || "attract";
836
+ const pillar = strategy.pillars[Math.floor(Math.random() * strategy.pillars.length)]!;
837
+ const painPoint = pillar.painPoints[Math.floor(Math.random() * pillar.painPoints.length)] || "General challenge";
838
+
839
+ const prompt = `Generate ${count} social media content pieces for ${spec.name}.
840
+
841
+ BUSINESS CONTEXT:
842
+ - Company: ${intelligence.companyName}
843
+ - Industry: ${intelligence.industry}
844
+ - Business Type: ${intelligence.businessType}
845
+ - Target Audience: ${intelligence.targetAudience}
846
+ - USPs: ${intelligence.usp.join(", ")}
847
+ - Tone: ${intelligence.tone}
848
+ - Language: ${intelligence.language}
849
+
850
+ CONTENT PILLAR: ${pillar.name} — ${pillar.description}
851
+ CUSTOMER PAIN POINT TO ADDRESS: ${painPoint}
852
+ JOURNEY STAGE: ${stage} (${stage === "attract" ? "awareness, education" : stage === "convert" ? "consideration, comparison" : "decision, action"})
853
+
854
+ ${platformSummary}
855
+
856
+ COPYWRITING FRAMEWORKS TO USE (rotate between them):
857
+ - PAS: Problem → Agitate → Solution
858
+ - AIDA: Attention → Interest → Desire → Action
859
+ - BAB: Before → After → Bridge
860
+ - StoryBrand: Character has a problem → meets a guide → who gives them a plan → calls them to action → helps them avoid failure → ends in success
861
+
862
+ REQUIREMENTS:
863
+ - Each post must have a strong hook in the first line
864
+ - Follow platform best practices for length and format
865
+ - Include relevant hashtags (${spec.hashtags.optimal})
866
+ - Write in ${intelligence.language === "de" ? "German" : intelligence.language === "fr" ? "French" : "English"}
867
+ - Include an image generation prompt for each post
868
+ ${await getHashtagPoolContext(intelligence.industry, intelligence.language)}
869
+ ${styleBlock}
870
+ ${fewShot}
871
+
872
+ Return ONLY valid JSON array (no markdown, no explanation):
873
+ [
874
+ {
875
+ "framework": "PAS|AIDA|BAB|StoryBrand",
876
+ "pillar": "${pillar.name}",
877
+ "targetPain": "the pain point addressed",
878
+ "hook": "the opening hook line",
879
+ "headline": "post headline/title",
880
+ "body": "full post body text",
881
+ "cta": "call to action text",
882
+ "hashtags": ["tag1", "tag2"],
883
+ "imagePrompt": "detailed image generation prompt for this post"
884
+ }
885
+ ]`;
886
+
887
+ const result = await callInternalAI({
888
+ systemPrompt: "You are an expert social media content strategist and copywriter. Generate platform-optimized content. Return ONLY valid JSON arrays, no markdown fences, no explanation.",
889
+ userPrompt: prompt,
890
+ maxTokens: 4096,
891
+ temperature: 0.8,
892
+ timeoutMs: 60_000,
893
+ });
894
+
895
+ if (!result.ok || !result.text) {
896
+ console.error("[content-engine] Content generation failed:", result.error);
897
+ throw new Error(`Content generation failed: ${result.error || "Unknown error"}`);
898
+ }
899
+
900
+ try {
901
+ let jsonText = result.text.trim();
902
+ const fenceMatch = jsonText.match(/```(?:json)?\s*([\s\S]*?)```/);
903
+ if (fenceMatch) jsonText = fenceMatch[1].trim();
904
+
905
+ const rawPieces = JSON.parse(jsonText) as Array<{
906
+ framework?: string;
907
+ pillar?: string;
908
+ targetPain?: string;
909
+ hook?: string;
910
+ headline?: string;
911
+ body?: string;
912
+ cta?: string;
913
+ hashtags?: string[];
914
+ imagePrompt?: string;
915
+ }>;
916
+
917
+ return rawPieces.map((raw) => ({
918
+ id: randomUUID(),
919
+ platform,
920
+ type: "social-post" as const,
921
+ journeyStage: stage,
922
+ framework: (COPY_FRAMEWORKS.includes(raw.framework as CopyFramework) ? raw.framework : "PAS") as CopyFramework,
923
+ pillar: raw.pillar || pillar.name,
924
+ targetPain: raw.targetPain || painPoint,
925
+ hook: raw.hook || "",
926
+ headline: raw.headline || "",
927
+ body: raw.body || "",
928
+ cta: raw.cta || "",
929
+ hashtags: raw.hashtags || [],
930
+ imagePrompt: raw.imagePrompt,
931
+ status: "draft" as const,
932
+ }));
933
+ } catch (e) {
934
+ console.error("[content-engine] Failed to parse content response:", e);
935
+ throw new Error("Failed to parse generated content");
936
+ }
937
+ }
938
+
939
+ // ─── Ad Creative Generation ─────────────────────────────────────────────────
940
+
941
+ /**
942
+ * Generate ad creatives with copy, image prompts, and brand-aligned design specs.
943
+ */
944
+ export async function generateAdCreatives(opts: {
945
+ intelligence: WebsiteIntelligence;
946
+ platform: string;
947
+ count?: number;
948
+ }): Promise<AdCreative[]> {
949
+ const { intelligence, platform, count = 3 } = opts;
950
+
951
+ const spec = getPlatform(platform);
952
+ if (!spec) {
953
+ throw new Error(`Unknown platform: ${platform}`);
954
+ }
955
+
956
+ const prompt = `Generate ${count} ad creatives for ${spec.name}.
957
+
958
+ BUSINESS:
959
+ - Company: ${intelligence.companyName}
960
+ - Industry: ${intelligence.industry}
961
+ - USPs: ${intelligence.usp.join(", ")}
962
+ - Target Audience: ${intelligence.targetAudience}
963
+ - Tone: ${intelligence.tone}
964
+ - Brand Colors: ${intelligence.colors.slice(0, 5).join(", ") || "not specified"}
965
+ - Language: ${intelligence.language}
966
+
967
+ AD SPECS FOR ${spec.name.toUpperCase()}:
968
+ - Best Format: ${spec.adSpecs.bestFormat}
969
+ - Best Aspect Ratio: ${spec.adSpecs.bestAspectRatio}
970
+ - Best Resolution: ${spec.adSpecs.bestResolution}
971
+ - Headline Length: ${spec.adSpecs.headlineLength}
972
+ - Body Length: ${spec.adSpecs.bodyLength}
973
+
974
+ REQUIREMENTS:
975
+ - Create compelling ad copy with clear value propositions
976
+ - Headlines should be punchy and within the platform's recommended length
977
+ - Include a clear CTA
978
+ - Image prompts should incorporate brand colors and style
979
+ - Write in ${intelligence.language === "de" ? "German" : intelligence.language === "fr" ? "French" : "English"}
980
+
981
+ Return ONLY valid JSON array (no markdown, no explanation):
982
+ [
983
+ {
984
+ "format": "${spec.adSpecs.bestFormat}",
985
+ "headline": "ad headline",
986
+ "body": "ad body copy",
987
+ "cta": "call to action button text",
988
+ "imagePrompt": "detailed image prompt incorporating brand colors ${intelligence.colors.slice(0, 3).join(", ")} and brand style"
989
+ }
990
+ ]`;
991
+
992
+ const result = await callInternalAI({
993
+ systemPrompt: "You are an expert advertising creative director. Generate high-converting ad creatives. Return ONLY valid JSON arrays, no markdown fences, no explanation.",
994
+ userPrompt: prompt,
995
+ maxTokens: 2048,
996
+ temperature: 0.7,
997
+ timeoutMs: 30_000,
998
+ });
999
+
1000
+ if (!result.ok || !result.text) {
1001
+ console.error("[content-engine] Ad generation failed:", result.error);
1002
+ throw new Error(`Ad generation failed: ${result.error || "Unknown error"}`);
1003
+ }
1004
+
1005
+ try {
1006
+ let jsonText = result.text.trim();
1007
+ const fenceMatch = jsonText.match(/```(?:json)?\s*([\s\S]*?)```/);
1008
+ if (fenceMatch) jsonText = fenceMatch[1].trim();
1009
+
1010
+ const rawAds = JSON.parse(jsonText) as Array<{
1011
+ format?: string;
1012
+ headline?: string;
1013
+ body?: string;
1014
+ cta?: string;
1015
+ imagePrompt?: string;
1016
+ }>;
1017
+
1018
+ return rawAds.map((raw) => ({
1019
+ id: randomUUID(),
1020
+ platform,
1021
+ format: raw.format || spec.adSpecs.bestFormat,
1022
+ aspectRatio: spec.adSpecs.bestAspectRatio,
1023
+ resolution: spec.adSpecs.bestResolution,
1024
+ headline: raw.headline || "",
1025
+ body: raw.body || "",
1026
+ cta: raw.cta || "",
1027
+ imagePrompt: raw.imagePrompt || "",
1028
+ brandColors: intelligence.colors.slice(0, 5),
1029
+ tone: intelligence.tone,
1030
+ }));
1031
+ } catch (e) {
1032
+ console.error("[content-engine] Failed to parse ad response:", e);
1033
+ throw new Error("Failed to parse generated ad creatives");
1034
+ }
1035
+ }
1036
+
1037
+ // ─── Full Content Plan ──────────────────────────────────────────────────────
1038
+
1039
+ export interface ContentPlan {
1040
+ intelligence: WebsiteIntelligence;
1041
+ strategy: ContentStrategy;
1042
+ content: Record<string, ContentPiece[]>; // keyed by platform
1043
+ ads: Record<string, AdCreative[]>; // keyed by platform
1044
+ weeks: number;
1045
+ generatedAt: string;
1046
+ }
1047
+
1048
+ /**
1049
+ * Generate a complete content plan for multiple weeks across platforms.
1050
+ */
1051
+ export async function generateContentPlan(opts: {
1052
+ url: string;
1053
+ platforms?: string[];
1054
+ weeks?: number;
1055
+ }): Promise<ContentPlan> {
1056
+ const { url, platforms = ["instagram", "linkedin", "facebook"], weeks = 4 } = opts;
1057
+
1058
+ // 1. Analyze website
1059
+ const intelligence = await analyzeWebsite(url);
1060
+
1061
+ // 2. Create strategy
1062
+ const strategy = createContentStrategy(intelligence, platforms);
1063
+
1064
+ // 3. Generate content for each platform
1065
+ const content: Record<string, ContentPiece[]> = {};
1066
+ const ads: Record<string, AdCreative[]> = {};
1067
+ const stages: JourneyStage[] = ["attract", "convert", "close"];
1068
+
1069
+ for (const platform of platforms) {
1070
+ const spec = getPlatform(platform);
1071
+ if (!spec) continue;
1072
+
1073
+ const postsPerWeek = parsePostsPerWeek(spec.frequency.recommended);
1074
+ const totalPosts = Math.min(postsPerWeek * weeks, 20); // Cap at 20 per platform
1075
+
1076
+ // Generate content across journey stages
1077
+ const allPieces: ContentPiece[] = [];
1078
+ for (const stage of stages) {
1079
+ const stageCount = Math.max(1, Math.round(totalPosts * (stage === "attract" ? 0.5 : stage === "convert" ? 0.3 : 0.2)));
1080
+ try {
1081
+ const pieces = await generateSmartContent({
1082
+ intelligence,
1083
+ strategy,
1084
+ platform,
1085
+ journeyStage: stage,
1086
+ count: stageCount,
1087
+ });
1088
+ allPieces.push(...pieces);
1089
+ } catch (e) {
1090
+ console.error(`[content-engine] Failed to generate ${stage} content for ${platform}:`, e);
1091
+ }
1092
+ }
1093
+ content[platform] = allPieces;
1094
+
1095
+ // Generate ad creatives
1096
+ try {
1097
+ ads[platform] = await generateAdCreatives({ intelligence, platform, count: 3 });
1098
+ } catch (e) {
1099
+ console.error(`[content-engine] Failed to generate ads for ${platform}:`, e);
1100
+ ads[platform] = [];
1101
+ }
1102
+ }
1103
+
1104
+ return {
1105
+ intelligence,
1106
+ strategy,
1107
+ content,
1108
+ ads,
1109
+ weeks,
1110
+ generatedAt: new Date().toISOString(),
1111
+ };
1112
+ }