227 lines
8.2 KiB
JavaScript

import http from 'http';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PORT = 8895;
const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
if (!ANTHROPIC_API_KEY) {
console.error('❌ ANTHROPIC_API_KEY environment variable is required');
console.error(' Run: export ANTHROPIC_API_KEY=$(op item get "Anthropic API Key" --fields password --reveal)');
process.exit(1);
}
const server = http.createServer(async (req, res) => {
// CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
// Serve static files
if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
const html = fs.readFileSync(path.join(__dirname, 'index.html'), 'utf-8');
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html);
return;
}
// Proxy: fetch a URL's content
if (req.method === 'POST' && req.url === '/api/fetch-url') {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', async () => {
try {
const { url } = JSON.parse(body);
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);
const response = await fetch(url, {
signal: controller.signal,
headers: {
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
}
});
clearTimeout(timeout);
let html = await response.text();
// Strip scripts, styles, and extract meaningful text
html = html
.replace(/<script[\s\S]*?<\/script>/gi, '')
.replace(/<style[\s\S]*?<\/style>/gi, '')
.replace(/<[^>]+>/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 12000); // Limit context size
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ content: html, success: true }));
} catch (err) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ content: '', success: false, error: err.message }));
}
});
return;
}
// Proxy: Claude API call
if (req.method === 'POST' && req.url === '/api/generate') {
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', async () => {
try {
const { websiteContent, url } = JSON.parse(body);
const systemPrompt = `You are a world-class creative director and brand strategist working at an elite AI advertising agency. Your job is to analyze a brand's website and generate brilliant ad creative concepts.
You MUST respond with valid JSON only — no markdown, no code fences, no explanation outside the JSON. The JSON must match this exact schema:
{
"brand": {
"name": "Brand Name",
"tagline": "Their core tagline or value prop",
"voice": "Description of brand voice (2-3 sentences)",
"positioning": "Market positioning statement",
"primaryColor": "#hexcolor (best guess from brand)",
"secondaryColor": "#hexcolor",
"accentColor": "#hexcolor",
"industry": "Industry/category",
"targetAudience": "Who they're targeting",
"keyBenefits": ["benefit1", "benefit2", "benefit3"],
"emotionalTone": "The emotional tone they use"
},
"ads": {
"meme": {
"topText": "TOP TEXT FOR MEME (punchy, funny, relatable)",
"bottomText": "BOTTOM TEXT PUNCHLINE",
"context": "Brief description of what image would show"
},
"iMessage": {
"messages": [
{"sender": "friend", "text": "message text"},
{"sender": "user", "text": "response text"},
{"sender": "friend", "text": "another message"},
{"sender": "user", "text": "final response mentioning the brand naturally"}
]
},
"tweet": {
"handle": "@brandhandle",
"displayName": "Display Name",
"text": "Tweet text (max 280 chars, make it viral-worthy, include emoji)",
"likes": "realistic number as string like 4.2K",
"retweets": "realistic number as string like 1.1K",
"replies": "realistic number as string",
"views": "realistic number as string like 847K"
},
"statCard": {
"bigNumber": "A bold stat (e.g., '10x', '93%', '2.4M')",
"label": "What the stat represents",
"subtext": "Supporting context sentence",
"source": "Source attribution"
},
"ugc": {
"reviewerName": "Realistic first name + last initial",
"rating": 5,
"title": "Review title",
"body": "Authentic-sounding review (3-4 sentences, conversational, specific details)",
"platform": "Where this review would appear",
"verified": true
},
"billboard": {
"headline": "BOLD STATEMENT (5-8 words max, all caps impact)",
"subline": "Supporting line underneath",
"cta": "Call to action text"
}
}
}
Make the ad copy INCREDIBLE — it should feel like it came from a top creative agency. Each format should tell a different angle of the brand story. Be specific to the actual brand, not generic. Make the meme actually funny, the tweet actually viral-worthy, and the UGC review feel genuinely authentic.`;
const userPrompt = `Analyze this website and generate ad creative concepts.
Website URL: ${url}
Website Content:
${websiteContent}
Generate the JSON response with brand analysis and ad concepts. Remember: ONLY valid JSON, no other text.`;
const response = await fetch('https://api.anthropic.com/v1/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-key': ANTHROPIC_API_KEY,
'anthropic-version': '2023-06-01'
},
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 4096,
messages: [
{ role: 'user', content: userPrompt }
],
system: systemPrompt
})
});
const data = await response.json();
if (data.error) {
throw new Error(data.error.message || 'API error');
}
// Extract text content from Claude's response
const textContent = data.content.find(c => c.type === 'text');
let resultText = textContent?.text || '';
// Try to parse JSON from the response (handle markdown code fences)
let parsed;
try {
// Strip markdown code fences if present
resultText = resultText.replace(/```json\s*/g, '').replace(/```\s*/g, '').trim();
parsed = JSON.parse(resultText);
} catch (e) {
// Try to find JSON in the response
const jsonMatch = resultText.match(/\{[\s\S]*\}/);
if (jsonMatch) {
parsed = JSON.parse(jsonMatch[0]);
} else {
throw new Error('Failed to parse Claude response as JSON');
}
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, data: parsed }));
} catch (err) {
console.error('Generate error:', err.message);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: false, error: err.message }));
}
});
return;
}
// 404
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not found');
});
server.listen(PORT, () => {
console.log(`
╔══════════════════════════════════════════════╗
║ 🎨 Vibe Ads Creative Engine ║
║ Running on http://localhost:${PORT}
║ ║
║ Open in your browser to start generating ║
╚══════════════════════════════════════════════╝
`);
});