add validation
This commit is contained in:
@@ -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>")
|
||||
|
||||
Reference in New Issue
Block a user