227 lines
8.2 KiB
JavaScript
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 ║
|
|
╚══════════════════════════════════════════════╝
|
|
`);
|
|
});
|