// @ts-nocheck import { SpintaxParser } from './SpintaxParser'; import { GrammarEngine } from './GrammarEngine'; import { HTMLRenderer } from './HTMLRenderer'; import { createDirectus, rest, staticToken, readItems, readItem } from '@directus/sdk'; // Config // In a real app, client should be passed in or singleton // For this class, we assume data is passed in or we have a method to fetch it. export interface GenerationContext { avatar: any; niche: string; city: any; site: any; template: any; } export class CartesianEngine { private client: any; constructor(directusClient: any) { this.client = directusClient; } /** * Generate a single article based on specific inputs. */ async generateArticle(context: GenerationContext) { const { avatar, niche, city, site, template } = context; const variant = await this.getAvatarVariant(avatar.id, 'neutral'); // Default to neutral or specific // 1. Process Template Blocks const blocksData = []; // Parse structure_json (assuming array of block IDs) const blockIds = Array.isArray(template.structure_json) ? template.structure_json : []; for (const blockId of blockIds) { // Fetch Universal Block // In production, fetch specific fields to optimize let universal: any = {}; try { // Assuming blockId is the ID in offer_blocks_universal (or key) // Since we stored them as items, we query by block_id field or id const result = await this.client.request(readItems('offer_blocks_universal' as any, { filter: { block_id: { _eq: blockId } }, limit: 1 })); universal = result[0] || {}; } catch (e) { console.error(`Block not found: ${blockId}`); } // Fetch Personalized Expansion let personal: any = {}; try { // Need a way to match block_id + avatar_id. // Our schema imported flat structure? // Ideally we query the offer_blocks_personalized collection // filtering by block_related_id AND avatar_related_id // For prototype, we might have stored it loosely. // Let's assume we can fetch. } catch (e) { } // MERGE (Simplified for now - using universal only + placeholder) // Real merge adds personal pains to universal pains const mergedBlock = { id: blockId, title: universal.title, hook: universal.hook_generator, pains: universal.universal_pains || [], solutions: universal.universal_solutions || [], value_points: universal.universal_value_points || [], cta: universal.cta_spintax, spintax: universal.spintax_content // Assuming a new field for full block spintax }; // 2. Resolve Tokens Per Block const solvedBlock = this.resolveBlock(mergedBlock, context, variant); blocksData.push(solvedBlock); } // 3. Assemble HTML const html = HTMLRenderer.renderArticle(blocksData); // 4. Generate Meta const metaTitle = this.generateMetaTitle(context, variant); return { title: metaTitle, html_content: html, slug: this.generateSlug(metaTitle), meta_desc: "Generated description..." // Implementation TBD }; } private resolveBlock(block: any, ctx: GenerationContext, variant: any): any { const resolve = (text: string) => { if (!text) return ''; let t = text; // Level 1: Variables t = t.replace(/{{NICHE}}/g, ctx.niche || 'Business'); t = t.replace(/{{CITY}}/g, ctx.city.city); t = t.replace(/{{STATE}}/g, ctx.city.state); t = t.replace(/{{ZIP_FOCUS}}/g, ctx.city.zip_focus || ''); t = t.replace(/{{AGENCY_NAME}}/g, "Spark Agency"); // Config t = t.replace(/{{AGENCY_URL}}/g, ctx.site.url); // Level 2: Spintax t = SpintaxParser.parse(t); // Level 3: Grammar t = GrammarEngine.resolve(t, variant); return t; }; const resolvedBlock: any = { id: block.id, title: resolve(block.title), hook: resolve(block.hook), pains: (block.pains || []).map(resolve), solutions: (block.solutions || []).map(resolve), value_points: (block.value_points || []).map(resolve), cta: resolve(block.cta) }; // Handle Spintax Content & Components if (block.spintax) { let content = SpintaxParser.parse(block.spintax); // Dynamic Component Replacement if (content.includes('{{COMPONENT_AVATAR_GRID}}')) { content = content.replace('{{COMPONENT_AVATAR_GRID}}', this.generateAvatarGrid()); } if (content.includes('{{COMPONENT_OPTIN_FORM}}')) { content = content.replace('{{COMPONENT_OPTIN_FORM}}', this.generateOptinForm()); } content = GrammarEngine.resolve(content, variant); resolvedBlock.content = content; } return resolvedBlock; } private generateAvatarGrid(): string { const avatars = [ "Scaling Founder", "Marketing Director", "Ecom Owner", "SaaS CEO", "Local Biz Owner", "Real Estate Agent", "Coach/Consultant", "Agency Owner", "Startup CTO", "Enterprise VP" ]; let html = '
'; avatars.forEach(a => { html += `
${a[0]}
${a}
`; }); html += '
'; return html; } private generateOptinForm(): string { return `

Book Your Strategy Session

Stop guessing. Get a custom roadmap consisting of the exact systems we used to scale.

No spam. Unsubscribe anytime.

`; } private generateMetaTitle(ctx: GenerationContext, variant: any): string { // Simple random pattern selection for now // In reality, this should come from "cartesian_patterns" loaded in context // But for robust fail-safe: const patterns = [ `Top Rated ${ctx.niche} Company in ${ctx.city.city}`, `${ctx.city.city} ${ctx.niche} Experts - ${ctx.site.name || 'Official Site'}`, `The #1 ${ctx.niche} Service in ${ctx.city.city}, ${ctx.city.state}`, `Best ${ctx.niche} Agency Serving ${ctx.city.city}` ]; const raw = patterns[Math.floor(Math.random() * patterns.length)]; return raw; } private generateSlug(title: string): string { return title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, ''); } private async getAvatarVariant(avatarId: string, gender: string) { // Try to fetch from Directus "avatar_variants" // If fail, return default neutral try { // We assume variants are stored in a singleton or we query by avatar // Since we don't have the ID handy, we return a safe default for this MVP test // to ensure it works without complex relation queries right now. // The GrammarEngine handles defaults if keys are missing. return { pronoun: 'they', ppronoun: 'them', pospronoun: 'their', isare: 'are', has_have: 'have', does_do: 'do' }; } catch (e) { return {}; } } }