add validation

This commit is contained in:
2026-08-01 20:29:40 -04:00
parent 7c761df010
commit 2b544c21ae
3 changed files with 235 additions and 0 deletions
+170
View File
@@ -1,8 +1,10 @@
import base64
import importlib
import json
import logging
import os
import random
import re
import time
import uuid
from collections.abc import Generator
@@ -12,6 +14,8 @@ import httpx
from dotenv import load_dotenv
from flask import Flask, Request, Response, request, stream_with_context
lxml_html: Any = importlib.import_module("lxml.html")
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
@@ -127,6 +131,8 @@ const interval = setInterval(() => {
window.__wotf_html = '';
es.onmessage = (e) => {
if (e.data === '[DONE]') {
titleEl.textContent = 'Validating HTML...';
} else if (e.data === '[RENDER]') {
clearInterval(interval);
es.close();
titleEl.textContent = 'Rendering...';
@@ -136,6 +142,13 @@ es.onmessage = (e) => {
document.write(fullHtml);
document.close();
injectMenu();
} else if (e.data === '[CLEAR]') {
htmlChunks = [];
htmlSize = 0;
chars = 0;
} else if (e.data.startsWith('[STATUS:')) {
const msg = e.data.slice(9);
stats.textContent = msg;
} else if (e.data === '[ERROR]') {
clearInterval(interval);
es.close();
@@ -271,6 +284,112 @@ function injectMenu() {
</html>"""
def call_llm(prompt: str, system: str = "You are a helpful assistant.") -> str:
headers = {"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"}
payload: dict[str, Any] = {
"model": MODEL,
"messages": [
{"role": "system", "content": system},
{"role": "user", "content": prompt},
],
"temperature": 0.7,
"stream": False,
}
try:
with httpx.Client(timeout=300) as client:
resp = client.post(API_URL, headers=headers, json=payload)
resp.raise_for_status()
data: dict[str, Any] = resp.json()
return data["choices"][0]["message"]["content"]
except BaseException as e: # noqa: BLE001
logger.error(f"call_llm failed: {e}")
return ""
def validate_html(html: str) -> list[str]:
errors: list[str] = []
if not html.strip().lower().startswith("<!doctype"):
errors.append("Missing DOCTYPE")
try:
doc: Any = lxml_html.fromstring(html)
html_tags: list[Any] = doc.xpath("//html")
body_tags: list[Any] = doc.xpath("//body")
head_tags: list[Any] = doc.xpath("//head")
if len(html_tags) != 1:
errors.append(f"Expected 1 <html> tag, found {len(html_tags)}")
if len(body_tags) != 1:
errors.append(f"Expected 1 <body> tag, found {len(body_tags)}")
if len(head_tags) != 1:
errors.append(f"Expected 1 <head> tag, found {len(head_tags)}")
except BaseException as e: # noqa: BLE001
errors.append(f"HTML parse error: {e}")
style_blocks = re.findall(r"<style[^>]*>(.*?)</style>", html, re.DOTALL | re.IGNORECASE)
for i, block in enumerate(style_blocks):
if block.count("{") != block.count("}"):
errors.append(f"CSS block {i + 1}: mismatched braces")
script_blocks = re.findall(r"<script[^>]*>(.*?)</script>", html, re.DOTALL | re.IGNORECASE)
for i, block in enumerate(script_blocks):
if block.count("{") != block.count("}"):
errors.append(f"JS block {i + 1}: mismatched braces")
if block.count("(") != block.count(")"):
errors.append(f"JS block {i + 1}: mismatched parentheses")
if block.count("[") != block.count("]"):
errors.append(f"JS block {i + 1}: mismatched brackets")
if script_blocks:
js_text = "\n".join(script_blocks)
review = call_llm(
f"Review this JavaScript for runtime errors (undefined variables, wrong selectors, syntax issues). List each issue concisely. If no issues, say 'No issues found.'\n\n<code>\n{js_text}\n</code>"
)
if review and "no issue" not in review.lower():
for line in review.strip().split("\n"):
line = line.strip()
if line and not line.startswith("<"):
errors.append(f"JS review: {line}")
return errors
def fix_html(html: str, errors: list[str]) -> str:
error_text = "\n".join(f"- {e}" for e in errors)
prompt = f"""Fix this HTML document. Issues found:
{error_text}
Rules:
- Return ONLY the complete corrected HTML document (<!DOCTYPE html> ... </html>)
- Do NOT wrap in markdown code blocks
- Do NOT add explanations
- Preserve the design and content, only fix errors
HTML to fix:
{html}"""
fixed = call_llm(prompt, system="You are an expert HTML/CSS/JS developer. You fix broken code.")
fixed = re.sub(r"^```html?\s*", "", fixed.strip(), flags=re.MULTILINE)
fixed = re.sub(r"\s*```$", "", fixed.strip(), flags=re.MULTILINE)
if fixed and "<html" in fixed.lower():
return fixed
logger.warning("fix_html: LLM returned invalid output, returning original")
return html
def summarize_errors(errors: list[str]) -> str:
seen: list[str] = []
for e in errors:
short = e[:60]
if short not in seen:
seen.append(short)
if len(seen) >= 3:
break
return ", ".join(seen)
def get_user_context(req: Request) -> dict[str, Any]:
user_agent = req.headers.get("User-Agent", "")
lang = req.headers.get("Accept-Language", "")
@@ -331,6 +450,16 @@ THEMES = [
("fake_other", "A fake 404 error page with an interactive game"),
("fake_other", "A fake loading screen for a fictional video game"),
("fake_other", "A fake terminal emulator with ASCII art and commands"),
("web_game", "A browser-based snake game with score tracking and increasing difficulty"),
("web_game", "A memory card matching game with colorful icons and timer"),
("web_game", "A clicker/idle game where you build a fictional empire"),
("web_game", "A whack-a-mole style arcade game with combos and high scores"),
("web_game", "A platformer game with a cute character and multiple levels"),
("web_game", "A typing speed test game with word races and accuracy stats"),
("web_game", "A 2048-style number sliding puzzle game"),
("web_game", "A reaction time test game with random color flashes"),
("web_game", "A virtual pet game where you feed, play, and care for a creature"),
("web_game", "A maze runner game with procedurally generated levels"),
]
@@ -426,11 +555,52 @@ def stream_llm(context: dict[str, Any]) -> Generator[str]:
def sse_stream(context: dict[str, Any]) -> Generator[str]:
buffer: list[str] = []
def status(msg: str) -> str:
return f"data: [STATUS:{msg}]\n\n"
yield status("Generating HTML...")
for chunk in stream_llm(context):
buffer.append(chunk)
encoded = base64.b64encode(chunk.encode("utf-8")).decode("ascii")
yield f"data: {encoded}\n\n"
html = "".join(buffer)
yield "data: [DONE]\n\n"
yield status("Validating HTML structure...")
errors = validate_html(html)
if errors:
short = summarize_errors(errors)
yield status(f"Found {len(errors)} issue(s): {short}. Fixing (attempt 1/2)...")
html = fix_html(html, errors)
errors = validate_html(html)
if errors:
short = summarize_errors(errors)
yield status("Still invalid. Fixing (attempt 2/2)...")
html = fix_html(html, errors)
errors = validate_html(html)
if errors:
short = summarize_errors(errors)
yield status(f"Still has {len(errors)} issue(s): {short}. Serving as-is.")
else:
yield status("Fixed successfully.")
yield "data: [CLEAR]\n\n"
for i in range(0, len(html), 1000):
chunk = html[i : i + 1000]
encoded = base64.b64encode(chunk.encode("utf-8")).decode("ascii")
yield f"data: {encoded}\n\n"
else:
yield status("Validation passed.")
yield "data: [RENDER]\n\n"
@app.route("/", defaults={"path": ""})
@app.route("/<path:path>")