feat: content generation UI, test script, and API docs
This commit is contained in:
176
CONTENT_GENERATION_API.md
Normal file
176
CONTENT_GENERATION_API.md
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
# Content Generation Engine - API Documentation
|
||||||
|
|
||||||
|
## 🎯 Overview
|
||||||
|
|
||||||
|
The Content Generation Engine takes JSON blueprints containing spintax and variables, generates unique combinations via Cartesian expansion, resolves spintax to create unique variations, and produces full 2000-word articles.
|
||||||
|
|
||||||
|
## 📡 API Endpoints
|
||||||
|
|
||||||
|
### 1. Create Campaign
|
||||||
|
**POST** `/api/god/campaigns/create`
|
||||||
|
|
||||||
|
Creates a new campaign from a JSON blueprint.
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
```
|
||||||
|
X-God-Token: <your_god_token>
|
||||||
|
Content-Type: application/json
|
||||||
|
```
|
||||||
|
|
||||||
|
**Request Body:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "Campaign Name (optional)",
|
||||||
|
"blueprint": {
|
||||||
|
"asset_name": "{{CITY}} Solar Revenue",
|
||||||
|
"deployment_target": "High-Value Funnel",
|
||||||
|
"variables": {
|
||||||
|
"STATE": "California",
|
||||||
|
"CITY": "San Diego|Irvine|Anaheim",
|
||||||
|
"AVATAR_A": "Solar CEO"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"url_path": "{{CITY}}.example.com",
|
||||||
|
"meta_description": "{Stop|Eliminate} waste in {{CITY}}",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"block_type": "Hero",
|
||||||
|
"content": "<h1>{Title A|Title B}</h1><p>Content for {{CITY}}</p>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"campaignId": "uuid",
|
||||||
|
"message": "Campaign created. Use /launch/<id> to generate."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Launch Campaign
|
||||||
|
**POST** `/api/god/campaigns/launch/:id`
|
||||||
|
|
||||||
|
Queues a campaign for content generation.
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
```
|
||||||
|
X-God-Token: <your_god_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"campaignId": "uuid",
|
||||||
|
"status": "processing"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Check Status
|
||||||
|
**GET** `/api/god/campaigns/status/:id`
|
||||||
|
|
||||||
|
Get campaign generation status and stats.
|
||||||
|
|
||||||
|
**Headers:**
|
||||||
|
```
|
||||||
|
X-God-Token: <your_god_token>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"campaignId": "uuid",
|
||||||
|
"name": "Campaign Name",
|
||||||
|
"status": "completed",
|
||||||
|
"postsCreated": 15,
|
||||||
|
"blockUsage": [
|
||||||
|
{ "block_type": "Hero", "total_uses": 15 }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 Blueprint Structure
|
||||||
|
|
||||||
|
### Variables
|
||||||
|
Pipe-separated values generate Cartesian products:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"CITY": "A|B|C", // 3 values
|
||||||
|
"STATE": "X|Y" // 2 values
|
||||||
|
// = 6 total combinations
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Spintax Syntax
|
||||||
|
```
|
||||||
|
{Option A|Option B|Option C}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Variable Placeholders
|
||||||
|
```
|
||||||
|
{{VARIABLE_NAME}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📊 Usage Tracking
|
||||||
|
|
||||||
|
Every block use and spintax choice is tracked:
|
||||||
|
- `block_usage_stats` - How many times each block was used
|
||||||
|
- `spintax_variation_stats` - Which spintax choices were selected
|
||||||
|
- `variation_registry` - Hash of every unique variation
|
||||||
|
|
||||||
|
## 🔧 Worker Process
|
||||||
|
|
||||||
|
The BullMQ worker (`contentGenerator.ts`):
|
||||||
|
1. Fetches blueprint from DB
|
||||||
|
2. Generates Cartesian product of variables
|
||||||
|
3. For each combination:
|
||||||
|
- Expands `{{VARIABLES}}`
|
||||||
|
- Resolves `{spin|tax}`
|
||||||
|
- Checks uniqueness hash
|
||||||
|
- Creates post in DB
|
||||||
|
- Records variation & updates stats
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
### Via UI
|
||||||
|
1. Go to `/admin/content-generator`
|
||||||
|
2. Paste JSON blueprint
|
||||||
|
3. Click "Create Campaign"
|
||||||
|
4. Launch from campaigns list
|
||||||
|
|
||||||
|
### Via API
|
||||||
|
```bash
|
||||||
|
# 1. Create
|
||||||
|
curl -X POST https://spark.jumpstartscaling.com/api/god/campaigns/create \
|
||||||
|
-H "X-God-Token: YOUR_TOKEN" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d @blueprint.json
|
||||||
|
|
||||||
|
# 2. Launch
|
||||||
|
curl -X POST https://spark.jumpstartscaling.com/api/god/campaigns/launch/CAMPAIGN_ID \
|
||||||
|
-H "X-God-Token: YOUR_TOKEN"
|
||||||
|
|
||||||
|
# 3. Check status
|
||||||
|
curl https://spark.jumpstartscaling.com/api/god/campaigns/status/CAMPAIGN_ID \
|
||||||
|
-H "X-God-Token: YOUR_TOKEN"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 Database Schema
|
||||||
|
|
||||||
|
Key tables:
|
||||||
|
- `campaign_masters` - Stores blueprints
|
||||||
|
- `content_fragments` - Individual blocks
|
||||||
|
- `variation_registry` - Unique variations
|
||||||
|
- `block_usage_stats` - Block usage counts
|
||||||
|
- `posts` - Final generated content
|
||||||
76
scripts/test-campaign.js
Normal file
76
scripts/test-campaign.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* CLI Test Runner for Content Generation
|
||||||
|
* Usage: node scripts/test-campaign.js
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { pool } from '../src/lib/db.js';
|
||||||
|
import { batchQueue } from '../src/lib/queue/config.js';
|
||||||
|
|
||||||
|
const testBlueprint = {
|
||||||
|
"asset_name": "{{CITY}} Test Campaign",
|
||||||
|
"deployment_target": "Test Funnel",
|
||||||
|
"variables": {
|
||||||
|
"STATE": "California",
|
||||||
|
"CITY": "San Diego|Irvine",
|
||||||
|
"AVATAR_A": "Solar CEO"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"url_path": "{{CITY}}.test.com",
|
||||||
|
"meta_description": "Test campaign for {{CITY}}",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"block_type": "Hero",
|
||||||
|
"content": "<h1>{Welcome|Hello} to {{CITY}}</h1><p>This is a {test|demo} for {{AVATAR_A}}s.</p>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
async function runTest() {
|
||||||
|
console.log('🧪 Content Generation Test\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Get site ID
|
||||||
|
const siteResult = await pool.query(
|
||||||
|
`SELECT id FROM sites WHERE domain = 'spark.jumpstartscaling.com' LIMIT 1`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (siteResult.rows.length === 0) {
|
||||||
|
throw new Error('Admin site not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const siteId = siteResult.rows[0].id;
|
||||||
|
console.log(`✓ Site ID: ${siteId}`);
|
||||||
|
|
||||||
|
// 2. Create campaign
|
||||||
|
const campaignResult = await pool.query(
|
||||||
|
`INSERT INTO campaign_masters (site_id, name, blueprint_json, status)
|
||||||
|
VALUES ($1, $2, $3, 'pending')
|
||||||
|
RETURNING id`,
|
||||||
|
[siteId, 'Test Campaign', JSON.stringify(testBlueprint)]
|
||||||
|
);
|
||||||
|
|
||||||
|
const campaignId = campaignResult.rows[0].id;
|
||||||
|
console.log(`✓ Campaign created: ${campaignId}`);
|
||||||
|
|
||||||
|
// 3. Queue job
|
||||||
|
await batchQueue.add('generate_campaign_content', {
|
||||||
|
campaignId,
|
||||||
|
campaignName: 'Test Campaign'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`✓ Job queued for campaign ${campaignId}`);
|
||||||
|
console.log('\n📊 Expected output: 2 posts (San Diego, Irvine)');
|
||||||
|
console.log('🔍 Check generation_jobs table for status');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Test failed:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await pool.end();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runTest();
|
||||||
@@ -59,4 +59,8 @@ export const queues = {
|
|||||||
cleanup: new Queue('cleanup', queueOptions),
|
cleanup: new Queue('cleanup', queueOptions),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Batch queue for campaign generation
|
||||||
|
export const batchQueue = new Queue('generate_campaign_content', queueOptions);
|
||||||
|
|
||||||
export { connection };
|
export { connection };
|
||||||
|
export const redisConnection = connection;
|
||||||
|
|||||||
172
src/pages/admin/content-generator.astro
Normal file
172
src/pages/admin/content-generator.astro
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
---
|
||||||
|
import AdminLayout from '../../../layouts/AdminLayout.astro';
|
||||||
|
import PageHeader from '../../../components/admin/PageHeader.astro';
|
||||||
|
---
|
||||||
|
|
||||||
|
<AdminLayout title="Content Generator">
|
||||||
|
<div class="space-y-8">
|
||||||
|
<PageHeader
|
||||||
|
icon="🎯"
|
||||||
|
title="Content Generation Engine"
|
||||||
|
description="Submit JSON blueprints to generate full 2000-word articles with spintax"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Campaign Input -->
|
||||||
|
<div class="bg-titanium border border-edge-normal rounded-xl p-8">
|
||||||
|
<h2 class="text-2xl font-bold text-gold-500 mb-6">📝 Submit Campaign Blueprint</h2>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
Campaign Name (Optional)
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="campaignName"
|
||||||
|
class="w-full bg-obsidian border border-edge-subtle rounded-lg px-4 py-3 text-white focus:border-gold-500 focus:outline-none"
|
||||||
|
placeholder="e.g., Solar California Campaign"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium text-gray-300 mb-2">
|
||||||
|
JSON Blueprint
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="blueprintJson"
|
||||||
|
rows="16"
|
||||||
|
class="w-full bg-obsidian border border-edge-subtle rounded-lg px-4 py-3 text-white font-mono text-sm focus:border-gold-500 focus:outline-none"
|
||||||
|
placeholder='Paste your JSON blueprint here...'
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<button
|
||||||
|
id="createCampaign"
|
||||||
|
class="px-6 py-3 bg-gold-500 hover:bg-gold-600 text-obsidian font-semibold rounded-lg transition-all"
|
||||||
|
>
|
||||||
|
Create Campaign
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
id="loadExample"
|
||||||
|
class="px-6 py-3 bg-gray-700 hover:bg-gray-600 text-white font-semibold rounded-lg transition-all"
|
||||||
|
>
|
||||||
|
Load Example
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Active Campaigns -->
|
||||||
|
<div class="bg-titanium border border-edge-normal rounded-xl p-8">
|
||||||
|
<h2 class="text-2xl font-bold text-gold-500 mb-6">⚡ Active Campaigns</h2>
|
||||||
|
<div id="campaignsList" class="space-y-4">
|
||||||
|
<p class="text-gray-400">No campaigns yet. Create one above.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Generated Content Stats -->
|
||||||
|
<div class="bg-titanium border border-edge-normal rounded-xl p-8">
|
||||||
|
<h2 class="text-2xl font-bold text-gold-500 mb-6">📊 Generation Stats</h2>
|
||||||
|
<div id="statsContainer" class="grid grid-cols-4 gap-4">
|
||||||
|
<div class="bg-obsidian rounded-lg p-4 border border-edge-subtle">
|
||||||
|
<div class="text-sm text-gray-400 mb-1">Total Campaigns</div>
|
||||||
|
<div id="statCampaigns" class="text-3xl font-bold text-gold-500">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-obsidian rounded-lg p-4 border border-edge-subtle">
|
||||||
|
<div class="text-sm text-gray-400 mb-1">Posts Created</div>
|
||||||
|
<div id="statPosts" class="text-3xl font-bold text-blue-400">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-obsidian rounded-lg p-4 border border-edge-subtle">
|
||||||
|
<div class="text-sm text-gray-400 mb-1">Blocks Used</div>
|
||||||
|
<div id="statBlocks" class="text-3xl font-bold text-green-400">0</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-obsidian rounded-lg p-4 border border-edge-subtle">
|
||||||
|
<div class="text-sm text-gray-400 mb-1">Variations</div>
|
||||||
|
<div id="statVariations" class="text-3xl font-bold text-purple-400">0</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</AdminLayout>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const exampleBlueprint = {
|
||||||
|
"asset_name": "{{CITY}} Solar Revenue Accelerator",
|
||||||
|
"deployment_target": "High-Value Residential Solar Funnel",
|
||||||
|
"variables": {
|
||||||
|
"STATE": "California",
|
||||||
|
"COUNTY": "San Diego County|Orange County",
|
||||||
|
"CITY": "San Diego|Irvine|Anaheim",
|
||||||
|
"LANDMARK": "Del Mar Heights|Saddleback Peak|Balboa Island",
|
||||||
|
"AVATAR_A": "Solar CEO",
|
||||||
|
"AVATAR_B": "Lead Installer",
|
||||||
|
"AVATAR_C": "Sales Manager"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"url_path": "{{CITY}}.solarinstall.com/revenue-engine",
|
||||||
|
"meta_description": "{Stop|Eliminate} lead waste. We install conversion infrastructure for Solar CEOs in {{CITY}}.",
|
||||||
|
"body": [
|
||||||
|
{
|
||||||
|
"block_type": "Hero (1)",
|
||||||
|
"content": "<h1>Why {{CITY}} Solar Firms {Lose|Burn} $4K Per Job</h1><p>{The Data is clear|The reality is harsh}: If you operate in {{CITY}}, you are paying for the highest CPL in {the industry|your sector}.</p>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"block_type": "Agitation (2)",
|
||||||
|
"content": "<h2>{{AVATAR_A}}s: Friction is Rejection in the {{STATE}} Market</h2><p>In the {luxury|high-demand} market of {{CITY}}, **friction is rejection**.</p>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.getElementById('loadExample')?.addEventListener('click', () => {
|
||||||
|
const textarea = document.getElementById('blueprintJson') as HTMLTextAreaElement;
|
||||||
|
if (textarea) {
|
||||||
|
textarea.value = JSON.stringify(exampleBlueprint, null, 2);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('createCampaign')?.addEventListener('click', async () => {
|
||||||
|
const nameInput = document.getElementById('campaignName') as HTMLInputElement;
|
||||||
|
const jsonInput = document.getElementById('blueprintJson') as HTMLTextAreaElement;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const blueprint = JSON.parse(jsonInput.value);
|
||||||
|
const name = nameInput.value || undefined;
|
||||||
|
|
||||||
|
const response = await fetch('/api/god/campaigns/create', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-God-Token': localStorage.getItem('god_token') || ''
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ name, blueprint })
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
alert(`✅ Campaign created! ID: ${result.campaignId}`);
|
||||||
|
jsonInput.value = '';
|
||||||
|
nameInput.value = '';
|
||||||
|
loadCampaigns();
|
||||||
|
} else {
|
||||||
|
alert(`❌ Error: ${result.error}`);
|
||||||
|
}
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(`❌ Invalid JSON: ${err.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadCampaigns() {
|
||||||
|
// This would fetch from an API - for now just placeholder
|
||||||
|
const list = document.getElementById('campaignsList');
|
||||||
|
if (list) {
|
||||||
|
list.innerHTML = '<p class="text-gray-400">Loading campaigns...</p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load stats on page load
|
||||||
|
loadCampaigns();
|
||||||
|
</script>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
// API Endpoint: POST /api/god/campaigns/create
|
// API Endpoint: POST /api/god/campaigns/create
|
||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import { pool } from '../../../../lib/db/db';
|
import { pool } from '../../../../lib/db';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
|
||||||
interface CampaignBlueprint {
|
interface CampaignBlueprint {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// API Endpoint: POST /api/god/campaigns/launch/[id]
|
// API Endpoint: POST /api/god/campaigns/launch/[id]
|
||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import { pool } from '../../../../../lib/db/db';
|
import { pool } from '../../../../../lib/db';
|
||||||
import { batchQueue } from '../../../../../lib/queue/config';
|
import { batchQueue } from '../../../../../lib/queue/config';
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ params, request }) => {
|
export const POST: APIRoute = async ({ params, request }) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// API Endpoint: GET /api/god/campaigns/status/[id]
|
// API Endpoint: GET /api/god/campaigns/status/[id]
|
||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import { pool } from '../../../../../lib/db/db';
|
import { pool } from '../../../../../lib/db';
|
||||||
|
|
||||||
export const GET: APIRoute = async ({ params, request }) => {
|
export const GET: APIRoute = async ({ params, request }) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// BullMQ Worker: Content Generator
|
// BullMQ Worker: Content Generator
|
||||||
import { Worker } from 'bullmq';
|
import { Worker } from 'bullmq';
|
||||||
import { pool } from '../lib/db/db';
|
import { pool } from '../lib/db';
|
||||||
import { redisConnection } from '../lib/queue/config';
|
import { redisConnection } from '../lib/queue/config';
|
||||||
import { SpintaxResolver, expandVariables, generateCartesianProduct } from '../lib/spintax/resolver';
|
import { SpintaxResolver, expandVariables, generateCartesianProduct } from '../lib/spintax/resolver';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
|||||||
Reference in New Issue
Block a user