diff --git a/frontend/public/assets/rocket_man.webp b/frontend/public/assets/rocket_man.webp new file mode 100644 index 0000000..b3b6f1f Binary files /dev/null and b/frontend/public/assets/rocket_man.webp differ diff --git a/frontend/src/components/admin/jumpstart/JumpstartWizard.tsx b/frontend/src/components/admin/jumpstart/JumpstartWizard.tsx new file mode 100644 index 0000000..a275a7f --- /dev/null +++ b/frontend/src/components/admin/jumpstart/JumpstartWizard.tsx @@ -0,0 +1,176 @@ + +// @ts-nocheck +import React, { useState, useEffect } from 'react'; +import { WordPressClient } from '@/lib/wordpress/WordPressClient'; +import { getDirectusClient, createItem } from '@/lib/directus/client'; +import { Card, CardContent } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Progress } from '@/components/ui/progress'; +import { Badge } from '@/components/ui/badge'; +import { Input } from '@/components/ui/input'; + +type Step = 'connect' | 'inventory' | 'qc' | 'launch'; + +export default function JumpstartWizard() { + const [step, setStep] = useState('connect'); + const [logs, setLogs] = useState([]); + + // Connection State + const [siteUrl, setSiteUrl] = useState(''); + const [username, setUsername] = useState(''); + const [appPassword, setAppPassword] = useState(''); + + // Inventory State + const [inventory, setInventory] = useState(null); + const [qcItems, setQcItems] = useState([]); + + const addLog = (msg: string) => setLogs(prev => [`[${new Date().toLocaleTimeString()}] ${msg}`, ...prev]); + + // 1. CONNECT THE CABLES + const handleConnect = async () => { + addLog(`๐Ÿ”Œ Connecting to ${siteUrl}...`); + try { + const wp = new WordPressClient(siteUrl, appPassword ? `${username}:${appPassword}` : undefined); + const alive = await wp.testConnection(); + if (alive) { + addLog("โœ… Connection Successful."); + setStep('inventory'); + await scanInventory(wp); + } else { + addLog("โŒ Connection Failed. Check URL."); + } + } catch (e) { addLog(`โŒ Error: ${e.message}`); } + }; + + // 2. INVENTORY & FILTER + const scanInventory = async (wp: WordPressClient) => { + addLog("๐Ÿ“ฆ Fetching Inventory (Posts, Pages, Taxonomies)..."); + // Mocking inventory scan for UI dev + // In real impl, we fetch categories/tags and filter < 10 + setTimeout(() => { + addLog("๐Ÿ”Ž Filtering Taxonomies (<10 ignored)..."); + addLog("๐Ÿ“Š Found 124 Post, 12 Good Categories."); + setInventory({ total_posts: 124, valid_categories: 12 }); + setStep('qc'); + generateQC(wp); + }, 1500); + }; + + // 3. QC GENERATION (First 3) + const generateQC = async (wp: WordPressClient) => { + addLog("๐Ÿงช Generating QC Batch (First 3 Articles)..."); + // Trigger API with limit=3 + setTimeout(() => { + setQcItems([ + { id: 1, title: 'AI Refactored: Post One', status: 'Review Needed' }, + { id: 2, title: 'AI Refactored: Post Two', status: 'Review Needed' }, + { id: 3, title: 'AI Refactored: Post Three', status: 'Review Needed' } + ]); + addLog("โš ๏ธ QC Paused. Waiting for Approval."); + }, 2000); + }; + + // 4. IGNITION + const handleLaunch = () => { + setStep('launch'); + addLog("๐Ÿš€ IGNITION! Starting Mass Generation & Deployment..."); + }; + + return ( +
+ {/* Header / Animation */} +
+
+

Guided Jumpstart Test

+

Phase 6: Connection, Inventory, QC, Ignition.

+
+ +
+ +
+ {/* Main Control Panel */} + + + {step === 'connect' && ( +
+

1. Connect the Cables

+
+ + setSiteUrl(e.target.value)} className="bg-slate-900 border-slate-600" placeholder="https://..." /> +
+
+
+ + setUsername(e.target.value)} className="bg-slate-900 border-slate-600" /> +
+
+ + setAppPassword(e.target.value)} className="bg-slate-900 border-slate-600" /> +
+
+ +
+ )} + + {step === 'qc' && ( +
+

2. Quality Control Gate

+
+ {qcItems.map(item => ( +
+ {item.title} + Review Needed +
+ ))} +
+
+ + +
+
+ )} + + {step === 'launch' && ( +
+

Engine Running

+

Deployment in progress. Do not close this window.

+ +
+
+
124
+
Total
+
+
+
45
+
Processed
+
+
+
42
+
Deployed
+
+
+
+ )} +
+
+ + {/* Live Logs */} + +
+

System Logs

+
+
+ {logs.map((log, i) => ( +
+ {log} +
+ ))} +
+
+
+
+ ); +} diff --git a/frontend/src/components/admin/wordpress/WPImporter.tsx b/frontend/src/components/admin/wordpress/WPImporter.tsx new file mode 100644 index 0000000..8136e4c --- /dev/null +++ b/frontend/src/components/admin/wordpress/WPImporter.tsx @@ -0,0 +1,192 @@ +// @ts-nocheck +import React, { useState } from 'react'; +import { WordPressClient, type WPPost } from '@/lib/wordpress/WordPressClient'; +import { getDirectusClient, createItem } from '@/lib/directus/client'; // Import Directus helper +import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { Badge } from '@/components/ui/badge'; + +export default function WPImporter() { + const [url, setUrl] = useState(''); + const [connected, setConnected] = useState(false); + const [loading, setLoading] = useState(false); + const [status, setStatus] = useState(''); + const [items, setItems] = useState([]); + const [selection, setSelection] = useState>(new Set()); + + const connect = async () => { + setLoading(true); + setStatus('Scanning site...'); + try { + const wp = new WordPressClient(url); + const isAlive = await wp.testConnection(); + if (isAlive) { + const pages = await wp.getPages(); + const posts = await wp.getPosts(); + setItems([...pages, ...posts].map(i => ({...i, id: i.id}))); // Ensure ID + setConnected(true); + } else { + alert("Could not connect to WordPress site."); + } + } catch (e) { + console.error(e); + alert("Connection error"); + } finally { + setLoading(false); + setStatus(''); + } + }; + + const toggleSelection = (id: number) => { + const next = new Set(selection); + if (next.has(id)) next.delete(id); + else next.add(id); + setSelection(next); + }; + + const handleImport = async () => { + setLoading(true); + setStatus('Creating Site & Job...'); + + try { + const client = getDirectusClient(); + + // 1. Create Site + // We assume url is like 'https://domain.com' + const domain = new URL(url).hostname; + const sitePayload = { + name: domain, + url: url, + domain: domain, + status: 'setup' + }; + const site = await client.request(createItem('sites', sitePayload)); + + // 2. Prepare Import Queue + const selectedItems = items.filter(i => selection.has(i.id)).map(i => ({ + original_id: i.id, + slug: i.slug, + title: i.title.rendered, + type: i.type + })); + + // 3. Create Generation Job + const jobPayload = { + site_id: site.id, + status: 'Pending', + target_quantity: selectedItems.length, + filters: { + mode: 'refactor', + items: selectedItems + } + }; + const job = await client.request(createItem('generation_jobs', jobPayload)); + + // 4. Trigger Generation API + setStatus('Starting Refactor Engine...'); + const res = await fetch('/api/generate-content', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({ jobId: job.id, mode: 'refactor' }) + }); + + if (res.ok) { + alert(`Success! Site created and ${selectedItems.length} items queued for refactoring.`); + window.location.href = `/admin/sites/${site.id}`; // Redirect to site + } else { + const err = await res.json(); + alert('Error starting job: ' + err.error); + } + + } catch (e) { + console.error(e); + alert("Import failed: " + e.message); + } finally { + setLoading(false); + setStatus(''); + } + }; + + return ( +
+ {!connected ? ( + + + Connect WordPress Site + + +
+ + setUrl(e.target.value)} + className="bg-slate-900 border-slate-700 text-white" + /> +
+ +
+
+ ) : ( +
+
+
+

Select Content to Import

+

Found {items.length} items from {url}

+
+
+ + +
+
+ +
+ + + + + + + + + + + {items.map(item => ( + + + + + + ))} + +
+ + TitleTypeSlug
+ toggleSelection(item.id)} + className="rounded border-slate-600 bg-slate-800" + /> + + + {item.type} + {item.slug}
+
+
+ )} +
+ ); +} diff --git a/frontend/src/lib/cartesian/CartesianEngine.ts b/frontend/src/lib/cartesian/CartesianEngine.ts index 27be5c6..e98b791 100644 --- a/frontend/src/lib/cartesian/CartesianEngine.ts +++ b/frontend/src/lib/cartesian/CartesianEngine.ts @@ -26,8 +26,9 @@ export class CartesianEngine { /** * Generate a single article based on specific inputs. + * @param overrides Optional overrides for slug, title, etc. */ - async generateArticle(context: GenerationContext) { + async generateArticle(context: GenerationContext, overrides?: any) { const { avatar, niche, city, site, template } = context; const variant = await this.getAvatarVariant(avatar.id, 'neutral'); // Default to neutral or specific @@ -51,19 +52,9 @@ export class CartesianEngine { 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) { } + // Fetch Personalized Expansion (Skipped for MVP) - // MERGE (Simplified for now - using universal only + placeholder) - // Real merge adds personal pains to universal pains + // MERGE const mergedBlock = { id: blockId, title: universal.title, @@ -84,12 +75,12 @@ export class CartesianEngine { const html = HTMLRenderer.renderArticle(blocksData); // 4. Generate Meta - const metaTitle = this.generateMetaTitle(context, variant); + const metaTitle = overrides?.title || this.generateMetaTitle(context, variant); return { title: metaTitle, html_content: html, - slug: this.generateSlug(metaTitle), + slug: overrides?.slug || this.generateSlug(metaTitle), meta_desc: "Generated description..." // Implementation TBD }; } @@ -129,17 +120,17 @@ export class CartesianEngine { // 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()); + content = content.replace('{{COMPONENT_AVATAR_GRID}}', this.generateAvatarGrid()); } if (content.includes('{{COMPONENT_OPTIN_FORM}}')) { - content = content.replace('{{COMPONENT_OPTIN_FORM}}', this.generateOptinForm()); + content = content.replace('{{COMPONENT_OPTIN_FORM}}', this.generateOptinForm()); } content = GrammarEngine.resolve(content, variant); - resolvedBlock.content = content; + resolvedBlock.content = content; } return resolvedBlock; @@ -150,7 +141,7 @@ export class CartesianEngine { "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 += ` diff --git a/frontend/src/lib/wordpress/WordPressClient.ts b/frontend/src/lib/wordpress/WordPressClient.ts new file mode 100644 index 0000000..e42fe2d --- /dev/null +++ b/frontend/src/lib/wordpress/WordPressClient.ts @@ -0,0 +1,75 @@ + +export interface WPPost { + id: number; + date: string; + slug: string; + status: string; + type: string; + link: string; + title: { rendered: string }; + content: { rendered: string }; + excerpt: { rendered: string }; +} + +export class WordPressClient { + private baseUrl: string; + private authHeader: string | null = null; + + constructor(domain: string, appPassword?: string) { + // Normalize domain + this.baseUrl = domain.replace(/\/$/, ''); + if (!this.baseUrl.startsWith('http')) { + this.baseUrl = `https://${this.baseUrl}`; + } + + if (appPassword) { + // Assumes username is 'admin' or handled in the pass string if formatted 'user:pass' + // Usually Application Passwords are just the pwd, requiring a user. + // For now, let's assume the user passes "username:app_password" string or implemented later. + // We'll stick to public GET for now which doesn't need auth for reading content usually. + // If auth is needed: + // this.authHeader = `Basic ${btoa(appPassword)}`; + } + } + + async testConnection(): Promise { + try { + const res = await fetch(`${this.baseUrl}/wp-json/`); + return res.ok; + } catch (e) { + console.error("WP Connection Failed", e); + return false; + } + } + + async getPages(limit = 100): Promise { + const url = `${this.baseUrl}/wp-json/wp/v2/pages?per_page=${limit}`; + return this.fetchCollection(url); + } + + async getPosts(limit = 100): Promise { + const url = `${this.baseUrl}/wp-json/wp/v2/posts?per_page=${limit}`; + return this.fetchCollection(url); + } + + async getCategories(): Promise { + // Fetch all categories + return this.fetchCollection(`${this.baseUrl}/wp-json/wp/v2/categories?per_page=100`); + } + + async getTags(): Promise { + // Fetch all tags + return this.fetchCollection(`${this.baseUrl}/wp-json/wp/v2/tags?per_page=100`); + } + + private async fetchCollection(url: string): Promise { + try { + const res = await fetch(url); + if (!res.ok) throw new Error(`WP API Error: ${res.status}`); + return await res.json(); + } catch (e) { + console.error("Fetch Error", e); + throw e; + } + } +} diff --git a/frontend/src/pages/admin/sites/import.astro b/frontend/src/pages/admin/sites/import.astro new file mode 100644 index 0000000..9fe6d12 --- /dev/null +++ b/frontend/src/pages/admin/sites/import.astro @@ -0,0 +1,18 @@ +--- +import Layout from '@/layouts/AdminLayout.astro'; +import WPImporter from '@/components/admin/wordpress/WPImporter'; +--- + + +
+
+ Sites + / + Import Wizard +
+ +

Content Import & Refactor

+ + +
+
diff --git a/frontend/src/pages/admin/sites/index.astro b/frontend/src/pages/admin/sites/index.astro index 680ee32..f123638 100644 --- a/frontend/src/pages/admin/sites/index.astro +++ b/frontend/src/pages/admin/sites/index.astro @@ -10,12 +10,16 @@ import SiteList from '@/components/admin/sites/SiteList';

My Sites

Manage your connected WordPress and Webflow sites.

- - - - - Add Site - + diff --git a/frontend/src/pages/admin/sites/jumpstart.astro b/frontend/src/pages/admin/sites/jumpstart.astro new file mode 100644 index 0000000..97aa5da --- /dev/null +++ b/frontend/src/pages/admin/sites/jumpstart.astro @@ -0,0 +1,10 @@ +--- +import Layout from '@/layouts/AdminLayout.astro'; +import JumpstartWizard from '@/components/admin/jumpstart/JumpstartWizard'; +--- + + +
+ +
+
diff --git a/frontend/src/pages/api/generate-content.ts b/frontend/src/pages/api/generate-content.ts index ffa714f..54e0353 100644 --- a/frontend/src/pages/api/generate-content.ts +++ b/frontend/src/pages/api/generate-content.ts @@ -76,7 +76,63 @@ export const POST: APIRoute = async ({ request }) => { generatedCount++; } - // 4. Generate Standard Batch + // 4. REFACTOR MODE (WordPress Import) + if (mode === 'refactor') { + console.log("โ™ป๏ธ Executing Refactor Mode..."); + const queue = filters.items || []; + + // Loop through queue items starting from current offset + while (generatedCount + offset < queue.length) { + const item = queue[generatedCount + offset]; + + // Context for Refactor + // We use a generic 'Business' avatar for now or try to infer from content? + // Let's stick to a safe default: "Scaling Founder" + const avatarItem = await client.request(readItem('avatars' as any, 'scaling_founder')); + const city = { city: 'Online', state: 'World' }; // Generic + + const context = { + avatar: avatarItem, + niche: 'Business', + city: city, + site: site, + // Use a generic article structure + template: { structure_json: ['block_03_fix_first_scale_second', 'block_04_market_domination'] } + }; + + // Generate with Overrides + const article = await engine.generateArticle(context, { + slug: item.slug, // PRESERVE SLUG + title: `Refactored: ${item.title}` // Indicate change + }); + + // Save + await client.request(createItem('generated_articles' as any, { + site_id: siteId, + title: article.title, + slug: article.slug, + html_content: article.html_content, + meta_desc: article.meta_desc, + is_published: true, + job_id: jobId + })); + generatedCount++; + } + + // Complete safely + await client.request(updateItem('generation_jobs' as any, jobId, { + current_offset: offset + generatedCount, + status: 'Complete' + })); + + return new Response(JSON.stringify({ + generated: generatedCount, + completed: true + }), { status: 200 }); + } + + + // 5. Generate Standard Batch // We will loop until batchSize is met or limit reached. // Load Resources needed for randomization @@ -133,7 +189,7 @@ export const POST: APIRoute = async ({ request }) => { generatedCount++; } - // 5. Update Job + // 6. Update Job standard await client.request(updateItem('generation_jobs' as any, jobId, { current_offset: offset + generatedCount, status: (offset + generatedCount >= limit) ? 'Complete' : 'Processing'