From e588711c9865d80e9ea44ab779c896d3f2258757 Mon Sep 17 00:00:00 2001 From: cawcenter Date: Fri, 12 Dec 2025 18:45:50 -0500 Subject: [PATCH] Phase 6: Jumpstart Wizard UI & WP Client --- frontend/public/assets/rocket_man.webp | Bin 0 -> 8754 bytes .../admin/jumpstart/JumpstartWizard.tsx | 176 ++++++++++++++++ .../components/admin/wordpress/WPImporter.tsx | 192 ++++++++++++++++++ frontend/src/lib/cartesian/CartesianEngine.ts | 31 +-- frontend/src/lib/wordpress/WordPressClient.ts | 75 +++++++ frontend/src/pages/admin/sites/import.astro | 18 ++ frontend/src/pages/admin/sites/index.astro | 16 +- .../src/pages/admin/sites/jumpstart.astro | 10 + frontend/src/pages/api/generate-content.ts | 60 +++++- 9 files changed, 550 insertions(+), 28 deletions(-) create mode 100644 frontend/public/assets/rocket_man.webp create mode 100644 frontend/src/components/admin/jumpstart/JumpstartWizard.tsx create mode 100644 frontend/src/components/admin/wordpress/WPImporter.tsx create mode 100644 frontend/src/lib/wordpress/WordPressClient.ts create mode 100644 frontend/src/pages/admin/sites/import.astro create mode 100644 frontend/src/pages/admin/sites/jumpstart.astro diff --git a/frontend/public/assets/rocket_man.webp b/frontend/public/assets/rocket_man.webp new file mode 100644 index 0000000000000000000000000000000000000000..b3b6f1f0762ca03dffe33a0cd63793378b18f27c GIT binary patch literal 8754 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 literal 0 HcmV?d00001 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'