Phase 6: Jumpstart Wizard UI & WP Client
This commit is contained in:
BIN
frontend/public/assets/rocket_man.webp
Normal file
BIN
frontend/public/assets/rocket_man.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
176
frontend/src/components/admin/jumpstart/JumpstartWizard.tsx
Normal file
176
frontend/src/components/admin/jumpstart/JumpstartWizard.tsx
Normal file
@@ -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<Step>('connect');
|
||||
const [logs, setLogs] = useState<string[]>([]);
|
||||
|
||||
// Connection State
|
||||
const [siteUrl, setSiteUrl] = useState('');
|
||||
const [username, setUsername] = useState('');
|
||||
const [appPassword, setAppPassword] = useState('');
|
||||
|
||||
// Inventory State
|
||||
const [inventory, setInventory] = useState<any>(null);
|
||||
const [qcItems, setQcItems] = useState<any[]>([]);
|
||||
|
||||
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 (
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{/* Header / Animation */}
|
||||
<div className="flex items-center justify-between bg-slate-900 p-6 rounded-xl border border-slate-700 relative overflow-hidden">
|
||||
<div className="z-10 relative">
|
||||
<h1 className="text-3xl font-extrabold text-white mb-2">Guided Jumpstart Test</h1>
|
||||
<p className="text-slate-400">Phase 6: Connection, Inventory, QC, Ignition.</p>
|
||||
</div>
|
||||
<img
|
||||
src="/assets/rocket_man.webp"
|
||||
className={`w-32 h-32 object-contain transition-transform duration-1000 ${step === 'launch' ? 'translate-x-[200px] -translate-y-[100px] opacity-0' : ''}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-6">
|
||||
{/* Main Control Panel */}
|
||||
<Card className="col-span-2 bg-slate-800 border-slate-700">
|
||||
<CardContent className="p-6 space-y-6">
|
||||
{step === 'connect' && (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-bold text-white">1. Connect the Cables</h2>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm text-slate-400">Site URL</label>
|
||||
<Input value={siteUrl} onChange={e => setSiteUrl(e.target.value)} className="bg-slate-900 border-slate-600" placeholder="https://..." />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm text-slate-400">Username</label>
|
||||
<Input value={username} onChange={e => setUsername(e.target.value)} className="bg-slate-900 border-slate-600" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm text-slate-400">App Password</label>
|
||||
<Input type="password" value={appPassword} onChange={e => setAppPassword(e.target.value)} className="bg-slate-900 border-slate-600" />
|
||||
</div>
|
||||
</div>
|
||||
<Button onClick={handleConnect} className="w-full bg-blue-600 hover:bg-blue-500">Connect & Scan</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'qc' && (
|
||||
<div className="space-y-4">
|
||||
<h2 className="text-xl font-bold text-white">2. Quality Control Gate</h2>
|
||||
<div className="bg-slate-900 p-4 rounded-lg space-y-2">
|
||||
{qcItems.map(item => (
|
||||
<div key={item.id} className="flex justify-between items-center bg-slate-800 p-3 rounded border border-slate-700">
|
||||
<span className="text-slate-200 font-medium">{item.title}</span>
|
||||
<Badge variant="outline" className="text-yellow-400 border-yellow-400">Review Needed</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<Button variant="outline" className="flex-1 border-slate-600 text-slate-300">Reject / Regenerate</Button>
|
||||
<Button onClick={handleLaunch} className="flex-1 bg-green-600 hover:bg-green-500">Approve & Ignite 🚀</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{step === 'launch' && (
|
||||
<div className="space-y-4 text-center py-8">
|
||||
<h2 className="text-2xl font-bold text-green-400 animate-pulse">Engine Running</h2>
|
||||
<p className="text-slate-400">Deployment in progress. Do not close this window.</p>
|
||||
<Progress value={45} className="h-4 bg-slate-900" />
|
||||
<div className="grid grid-cols-3 gap-4 pt-4">
|
||||
<div className="bg-slate-900 p-3 rounded">
|
||||
<div className="text-2xl font-bold text-white">124</div>
|
||||
<div className="text-xs text-slate-500">Total</div>
|
||||
</div>
|
||||
<div className="bg-slate-900 p-3 rounded">
|
||||
<div className="text-2xl font-bold text-blue-400">45</div>
|
||||
<div className="text-xs text-slate-500">Processed</div>
|
||||
</div>
|
||||
<div className="bg-slate-900 p-3 rounded">
|
||||
<div className="text-2xl font-bold text-green-400">42</div>
|
||||
<div className="text-xs text-slate-500">Deployed</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Live Logs */}
|
||||
<Card className="bg-slate-900 border-slate-800 shadow-inner h-[500px] overflow-hidden flex flex-col">
|
||||
<div className="p-3 border-b border-slate-800 bg-slate-950">
|
||||
<h3 className="text-xs font-mono text-green-500 uppercase">System Logs</h3>
|
||||
</div>
|
||||
<div className="flex-1 p-4 overflow-y-auto font-mono text-xs space-y-2">
|
||||
{logs.map((log, i) => (
|
||||
<div key={i} className="text-slate-400 border-l-2 border-slate-800 pl-2">
|
||||
{log}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
192
frontend/src/components/admin/wordpress/WPImporter.tsx
Normal file
192
frontend/src/components/admin/wordpress/WPImporter.tsx
Normal file
@@ -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<WPPost[]>([]);
|
||||
const [selection, setSelection] = useState<Set<number>>(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 (
|
||||
<div className="space-y-6">
|
||||
{!connected ? (
|
||||
<Card className="bg-slate-800 border-slate-700 max-w-xl mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle>Connect WordPress Site</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label>Site URL</Label>
|
||||
<Input
|
||||
placeholder="https://example.com"
|
||||
value={url}
|
||||
onChange={e => setUrl(e.target.value)}
|
||||
className="bg-slate-900 border-slate-700 text-white"
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
onClick={connect}
|
||||
disabled={loading || !url}
|
||||
className="w-full bg-blue-600 hover:bg-blue-700"
|
||||
>
|
||||
{loading ? 'Connecting...' : 'Scan Site'}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
<div className="flex justify-between items-center bg-slate-800 p-4 rounded-lg border border-slate-700">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white">Select Content to Import</h2>
|
||||
<p className="text-slate-400 text-sm">Found {items.length} items from {url}</p>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" onClick={() => setConnected(false)} className="border-slate-600 text-slate-300">
|
||||
Output
|
||||
</Button>
|
||||
<Button className="bg-green-600 hover:bg-green-700 text-white" onClick={handleImport} disabled={selection.size === 0}>
|
||||
Import {selection.size} Items
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-800 rounded-lg border border-slate-700 overflow-hidden">
|
||||
<table className="w-full text-left text-sm text-slate-400">
|
||||
<thead className="bg-slate-900/50 text-slate-200 uppercase font-medium">
|
||||
<tr>
|
||||
<th className="px-6 py-3 w-12">
|
||||
<input type="checkbox" className="rounded border-slate-600 bg-slate-800" />
|
||||
</th>
|
||||
<th className="px-6 py-3">Title</th>
|
||||
<th className="px-6 py-3">Type</th>
|
||||
<th className="px-6 py-3">Slug</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-700">
|
||||
{items.map(item => (
|
||||
<tr key={item.id} className="hover:bg-slate-700/50 transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selection.has(item.id)}
|
||||
onChange={() => toggleSelection(item.id)}
|
||||
className="rounded border-slate-600 bg-slate-800"
|
||||
/>
|
||||
</td>
|
||||
<td className="px-6 py-4 font-medium text-slate-200" dangerouslySetInnerHTML={{ __html: item.title.rendered }} />
|
||||
<td className="px-6 py-4">
|
||||
<Badge variant="outline">{item.type}</Badge>
|
||||
</td>
|
||||
<td className="px-6 py-4 font-mono text-xs">{item.slug}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
75
frontend/src/lib/wordpress/WordPressClient.ts
Normal file
75
frontend/src/lib/wordpress/WordPressClient.ts
Normal file
@@ -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<boolean> {
|
||||
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<WPPost[]> {
|
||||
const url = `${this.baseUrl}/wp-json/wp/v2/pages?per_page=${limit}`;
|
||||
return this.fetchCollection(url);
|
||||
}
|
||||
|
||||
async getPosts(limit = 100): Promise<WPPost[]> {
|
||||
const url = `${this.baseUrl}/wp-json/wp/v2/posts?per_page=${limit}`;
|
||||
return this.fetchCollection(url);
|
||||
}
|
||||
|
||||
async getCategories(): Promise<any[]> {
|
||||
// Fetch all categories
|
||||
return this.fetchCollection(`${this.baseUrl}/wp-json/wp/v2/categories?per_page=100`);
|
||||
}
|
||||
|
||||
async getTags(): Promise<any[]> {
|
||||
// Fetch all tags
|
||||
return this.fetchCollection(`${this.baseUrl}/wp-json/wp/v2/tags?per_page=100`);
|
||||
}
|
||||
|
||||
private async fetchCollection(url: string): Promise<any[]> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
18
frontend/src/pages/admin/sites/import.astro
Normal file
18
frontend/src/pages/admin/sites/import.astro
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
import Layout from '@/layouts/AdminLayout.astro';
|
||||
import WPImporter from '@/components/admin/wordpress/WPImporter';
|
||||
---
|
||||
|
||||
<Layout title="Import WordPress Site">
|
||||
<div class="p-6 space-y-6">
|
||||
<div class="flex items-center gap-2 text-slate-400 text-sm">
|
||||
<a href="/admin/sites" class="hover:text-blue-400">Sites</a>
|
||||
<span>/</span>
|
||||
<span class="text-white">Import Wizard</span>
|
||||
</div>
|
||||
|
||||
<h1 class="text-3xl font-bold text-slate-100">Content Import & Refactor</h1>
|
||||
|
||||
<WPImporter client:load />
|
||||
</div>
|
||||
</Layout>
|
||||
@@ -10,13 +10,17 @@ import SiteList from '@/components/admin/sites/SiteList';
|
||||
<h1 class="text-3xl font-bold text-slate-100">My Sites</h1>
|
||||
<p class="text-slate-400">Manage your connected WordPress and Webflow sites.</p>
|
||||
</div>
|
||||
<div class="flex gap-3">
|
||||
<a href="/admin/sites/import" class="bg-slate-700 hover:bg-slate-600 text-white px-4 py-2 rounded-lg font-medium transition-colors flex items-center gap-2 border border-slate-600">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" /></svg>
|
||||
Import Content
|
||||
</a>
|
||||
<a href="/admin/sites/new" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg font-medium transition-colors flex items-center gap-2">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /></svg>
|
||||
Add Site
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SiteList client:load />
|
||||
</div>
|
||||
|
||||
10
frontend/src/pages/admin/sites/jumpstart.astro
Normal file
10
frontend/src/pages/admin/sites/jumpstart.astro
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
import Layout from '@/layouts/AdminLayout.astro';
|
||||
import JumpstartWizard from '@/components/admin/jumpstart/JumpstartWizard';
|
||||
---
|
||||
|
||||
<Layout title="Guided Jumpstart Test">
|
||||
<div class="p-8">
|
||||
<JumpstartWizard client:load />
|
||||
</div>
|
||||
</Layout>
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user