Files
website-ai-generator/index.php
2026-05-17 20:08:52 +02:00

190 lines
7.1 KiB
PHP

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
header('Content-Type: text/html; charset=UTF-8');
if (PHP_SAPI !== 'cli') {
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
if ($method !== 'GET') {
http_response_code(405);
header('Allow: GET');
echo 'Method Not Allowed';
exit;
}
$secPurpose = strtolower((string) ($_SERVER['HTTP_SEC_PURPOSE'] ?? ''));
$purpose = strtolower((string) ($_SERVER['HTTP_PURPOSE'] ?? ''));
$xMoz = strtolower((string) ($_SERVER['HTTP_X_MOZ'] ?? ''));
$isSpeculative = str_contains($secPurpose, 'prefetch')
|| str_contains($secPurpose, 'prerender')
|| str_contains($purpose, 'prefetch')
|| str_contains($purpose, 'prerender')
|| str_contains($xMoz, 'prefetch');
if (! $isSpeculative) {
$secFetchMode = strtolower((string) ($_SERVER['HTTP_SEC_FETCH_MODE'] ?? ''));
$secFetchDest = strtolower((string) ($_SERVER['HTTP_SEC_FETCH_DEST'] ?? ''));
$secFetchUser = (string) ($_SERVER['HTTP_SEC_FETCH_USER'] ?? '');
// Treat obvious non-navigation fetches as non-user document requests.
if (($secFetchMode !== '' && $secFetchMode !== 'navigate')
|| ($secFetchDest !== '' && $secFetchDest !== 'document')
|| ($secFetchUser !== '' && $secFetchUser !== '?1')) {
$isSpeculative = true;
}
}
if ($isSpeculative) {
http_response_code(204);
header('X-Skipped-Generation: speculative-request');
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
exit;
}
}
$startedAt = microtime(true);
$apiKey = getenv('LLAMA_API_KEY') ?: '';
$model = getenv('LLAMA_MODEL') ?: 'local-model';
$maxTokens = (int) (getenv('LLAMA_MAX_TOKENS') ?: 4096);
$reasoningEffort = getenv('LLAMA_REASONING_EFFORT') ?: 'none';
$topicHint = isset($_GET['topic']) ? trim((string) $_GET['topic']) : '';
$topicHint = mb_substr($topicHint, 0, 120);
$seed = sprintf('seed-%08x%08x', random_int(0, 0xffffffff), random_int(0, 0xffffffff));
$topicCategories = [
'local restaurant landing page',
'fitness coaching brand site',
'travel destination mini-guide',
'event festival one-page promo',
'indie game launch page',
'architect portfolio page',
'music artist release page',
'bookstore seasonal campaign',
'pet adoption center homepage',
'artisan coffee roaster website',
'online course product page',
'nonprofit donation campaign page',
];
$selectedCategory = $topicCategories[array_rand($topicCategories)];
$bannedTerms = [
'chrono',
'temporal',
'chrono-',
'timewarp',
'timescape',
'timeshift',
'quantum',
'aether',
'epoch',
'singularity',
];
$bannedTermsList = implode(', ', $bannedTerms);
$factory = OpenAI::factory()->withBaseUri('http://localhost:8080/v1');
if ($apiKey !== '') {
$factory = $factory->withApiKey($apiKey);
}
$client = $factory->make();
$systemPrompt = <<<PROMPT
You are a senior creative web designer and front-end engineer.
Task:
- On every run, first choose a unique page concept with a specific topic, visual theme, and style direction.
- Then generate one complete, production-ready HTML document from scratch.
Hard output rules:
- Return only raw HTML. No markdown. No code fences. No explanations.
- Output must begin with <!doctype html> and include full <html>, <head>, and <body>.
- Use Tailwind CSS via CDN in the same HTML document.
- Do not rely on local/external project files. Everything required must be inside this single HTML file.
- Inline any custom CSS and JavaScript in <style> and <script> tags.
- Make the page responsive and fully usable on mobile and desktop.
- Include accessible semantic structure and visible focus states.
Creative direction:
- Avoid generic templates.
- Use bold, intentional typography and layout.
- Include subtle but meaningful animation/interaction.
- Provide complete content, not placeholder lorem ipsum.
Diversity constraints:
- Do not make a sci-fi, futuristic, or time-themed website unless the user explicitly asks for it.
- Avoid these words in brand names, title, headings, and body copy: {$bannedTermsList}
- If no topic is provided by user, choose a grounded real-world business/topic from the requested category.
PROMPT;
$userPrompt = 'Create a complete one-page website now. Use random token: ' . $seed . '.';
if ($topicHint === '') {
$userPrompt .= ' Required category for this run: ' . $selectedCategory . '.';
}
if ($topicHint !== '') {
$userPrompt .= ' Topic request: ' . $topicHint . '.';
}
$request = [
'model' => $model,
'temperature' => 0.95,
'top_p' => 0.95,
'max_tokens' => $maxTokens,
'messages' => [
['role' => 'system', 'content' => $systemPrompt],
['role' => 'user', 'content' => $userPrompt],
],
'reasoning_effort' => $reasoningEffort,
];
try {
$response = $client->chat()->create($request);
$html = trim((string) ($response->choices[0]->message->content ?? ''));
if ($topicHint === '' && preg_match('/\b(?:chrono\w*|temporal\w*|quantum\w*|aether\w*|epoch\w*|singularity\w*|timeshift\w*|timescape\w*|timewarp\w*)\b/i', $html) === 1) {
$request['messages'][] = [
'role' => 'system',
'content' => 'Retry with a completely different, non-sci-fi, non-time-themed concept. Do not use banned words.',
];
$response = $client->chat()->create($request);
$html = trim((string) ($response->choices[0]->message->content ?? ''));
}
$elapsedSeconds = number_format(microtime(true) - $startedAt, 2);
// If the model wraps output in a markdown fence, unwrap it safely.
if (preg_match('/^```(?:html)?\s*(.*?)\s*```$/is', $html, $matches) === 1) {
$html = $matches[1];
}
if ($html === '' || stripos($html, '<html') === false) {
throw new RuntimeException('Model did not return a full HTML document.');
}
$timingBlock = '<div style="position:fixed;right:12px;bottom:12px;z-index:2147483647;background:rgba(17,24,39,.92);color:#fff;padding:8px 10px;border-radius:10px;font:12px/1.3 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;box-shadow:0 8px 20px rgba(0,0,0,.28)">Generated in ' . $elapsedSeconds . 's</div>';
if (stripos($html, '</body>') !== false) {
$html = preg_replace('/<\/body>/i', $timingBlock . '</body>', $html, 1) ?? ($html . $timingBlock);
} else {
$html .= $timingBlock;
}
echo $html;
file_put_contents(__DIR__ . '/pages/' . sha1($seed) . '.html', $html);
} catch (Throwable $e) {
http_response_code(500);
$message = htmlspecialchars($e->getMessage(), ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
echo '<!doctype html><html><head><meta charset="UTF-8"><title>Generation Error</title>';
echo '<script src="https://cdn.tailwindcss.com"></script></head><body class="min-h-screen bg-zinc-950 text-zinc-100 p-8">';
echo '<h1 class="text-2xl font-bold mb-4">Page generation failed</h1>';
echo '<p class="text-zinc-300 mb-3">' . $message . '</p>';
echo '<p class="text-zinc-400 text-sm">Tip: set LLAMA_MODEL to a loaded model and optionally pass ?topic=your-idea in the URL.</p>';
echo '</body></html>';
}