#!/usr/bin/env python3 """ DreamWiki - A Single-File, AI-Generated Wikipedia Clone A web application that generates Wikipedia-style articles on-demand using LLMs via OpenRouter API. """ import os import re import json from pathlib import Path from typing import Optional import uvicorn from fastapi import FastAPI, HTTPException from fastapi.responses import HTMLResponse from dotenv import load_dotenv import requests import markdown2 from jinja2 import Template # ============================================================================ # Configuration # ============================================================================ # Load environment variables load_dotenv() # Constants PAGES_DIR = Path("pages") OPENROUTER_API_URL = "https://openrouter.ai/api/v1/chat/completions" # System prompt for article generation SYSTEM_PROMPT = """You are an encyclopedia author for a fictional, dream-like universe called DreamWiki. Your sole purpose is to generate complete, imaginative, and engaging encyclopedia articles. You must follow these rules strictly: 1. Your entire response must be in Markdown format. 2. The article should be comprehensive, with a short introductory paragraph followed by several sections (e.g., ## History, ## Characteristics, ## In Popular Culture). 3. You MUST invent and include 3-5 links to other DreamWiki articles. The links must be relevant and use the exact format: `[Link Text](/wiki/Article_Title)`. Do not use full URLs. 4. You MUST include at least one Markdown table. 5. You MUST include at least one placeholder image using the format `![Image Description](https://picsum.photos/seed/REPLACE_WITH_SLUG/400/300)`. 6. Do not add any commentary, preamble, or sign-off. Respond only with the Markdown content of the article.""" # ============================================================================ # Templates and Styling # ============================================================================ CSS_STYLE = """ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Lato, Helvetica, Arial, sans-serif; line-height: 1.6; color: #222; max-width: 960px; margin: 0 auto; padding: 20px; background-color: #f8f9fa; } .header { border-bottom: 3px solid #a2a9b1; padding-bottom: 10px; margin-bottom: 20px; } .header h1 { margin: 0; color: #000; font-size: 2.2em; font-weight: normal; } .content { background: white; padding: 30px; border: 1px solid #a2a9b1; border-radius: 2px; } h1, h2, h3, h4, h5, h6 { color: #000; border-bottom: 1px solid #eaecf0; padding-bottom: 2px; margin-top: 30px; margin-bottom: 15px; } h1 { font-size: 2.2em; } h2 { font-size: 1.8em; } h3 { font-size: 1.4em; } a { color: #0645ad; text-decoration: none; } a:hover { text-decoration: underline; } a:visited { color: #0b0080; } table { border-collapse: collapse; margin: 20px 0; width: 100%; } table, th, td { border: 1px solid #a2a9b1; } th, td { padding: 8px 12px; text-align: left; } th { background-color: #eaecf0; font-weight: bold; } img { max-width: 100%; height: auto; margin: 15px 0; } .search-form { margin: 20px 0; } .search-input { padding: 8px 12px; font-size: 16px; border: 1px solid #a2a9b1; border-radius: 2px; width: 300px; } .search-button { padding: 8px 16px; font-size: 16px; background-color: #0645ad; color: white; border: none; border-radius: 2px; cursor: pointer; margin-left: 5px; } .search-button:hover { background-color: #0b0080; } .intro { background-color: #f8f9fa; padding: 20px; border-left: 5px solid #36c; margin: 20px 0; } """ HTML_TEMPLATE = """ {{ title }} - DreamWiki

DreamWiki

{{ content }}
""" # ============================================================================ # Helper Functions # ============================================================================ def slugify(title: str) -> str: """Convert a title into a safe filename slug.""" # Convert to lowercase and replace spaces with underscores slug = title.lower().replace(" ", "_") # Remove any characters that aren't alphanumeric, underscore, or hyphen slug = re.sub(r'[^a-z0-9_-]', '', slug) # Remove multiple consecutive underscores slug = re.sub(r'_+', '_', slug) # Remove leading/trailing underscores slug = slug.strip('_') return slug def page_exists(title_slug: str) -> bool: """Check if a page file exists for the given slug.""" file_path = PAGES_DIR / f"{title_slug}.md" return file_path.exists() def load_page(title_slug: str) -> str: """Load and return the content of a page file.""" file_path = PAGES_DIR / f"{title_slug}.md" try: with open(file_path, 'r', encoding='utf-8') as f: return f.read() except FileNotFoundError: raise HTTPException(status_code=404, detail="Page not found") def save_page(title_slug: str, content: str) -> None: """Save generated content to a page file.""" # Ensure pages directory exists PAGES_DIR.mkdir(exist_ok=True) file_path = PAGES_DIR / f"{title_slug}.md" with open(file_path, 'w', encoding='utf-8') as f: f.write(content) def generate_article(title: str) -> str: """Generate an article using the OpenRouter API.""" api_key = os.getenv('OPENROUTER_API_KEY') model = os.getenv('OPENROUTER_MODEL') if not api_key: raise HTTPException(status_code=500, detail="OPENROUTER_API_KEY not configured") if not model: raise HTTPException(status_code=500, detail="OPENROUTER_MODEL not configured") # Prepare the prompt title_slug = slugify(title) prompt = SYSTEM_PROMPT.replace("REPLACE_WITH_SLUG", title_slug) headers = { "Authorization": f"Bearer {api_key}", "Content-Type": "application/json", "HTTP-Referer": "https://dreamwiki.dws.rip" } payload = { "model": model, "messages": [ {"role": "system", "content": prompt}, {"role": "user", "content": f"Generate a comprehensive DreamWiki article about: {title}"} ] } try: response = requests.post(OPENROUTER_API_URL, headers=headers, json=payload, timeout=30) response.raise_for_status() data = response.json() content = data['choices'][0]['message']['content'] return content except requests.exceptions.RequestException as e: raise HTTPException(status_code=500, detail=f"Failed to generate article: {str(e)}") except (KeyError, IndexError) as e: raise HTTPException(status_code=500, detail="Invalid response from OpenRouter API") # ============================================================================ # FastAPI Application # ============================================================================ app = FastAPI(title="DreamWiki", description="AI-Generated Wikipedia Clone") @app.on_event("startup") async def startup_event(): """Initialize the application on startup.""" # Create pages directory PAGES_DIR.mkdir(exist_ok=True) # Log configuration status api_key = os.getenv('OPENROUTER_API_KEY') model = os.getenv('OPENROUTER_MODEL') print("🌟 DreamWiki Starting Up...") print(f"📁 Pages directory: {PAGES_DIR.absolute()}") print(f"🔑 API Key configured: {'✓' if api_key else '✗'}") print(f"🤖 Model configured: {model if model else '✗'}") print("🚀 Ready to generate dream articles!") @app.get("/", response_class=HTMLResponse) async def home(): """Render the home page.""" content = """

Welcome to DreamWiki

DreamWiki is a fictional encyclopedia where every article is generated on-demand by artificial intelligence. Enter any topic below and watch as an entire article materializes from the digital ether, complete with interconnected lore and references to other dream-like entities.

Note: This is a creative experiment. All content is fictional and generated by AI.

Recent Explorations

Try searching for anything that comes to mind: fictional creatures, imaginary places, made-up technologies, or abstract concepts. Each search creates a new page in our ever-expanding dream universe.

""" template = Template(HTML_TEMPLATE) return template.render( title="Home", style=CSS_STYLE, content=content ) @app.get("/wiki/{page_title:path}", response_class=HTMLResponse) async def wiki_page(page_title: str): """Render a wiki page, generating it if it doesn't exist.""" title_slug = slugify(page_title) if not title_slug: raise HTTPException(status_code=400, detail="Invalid page title") # Check if page exists if page_exists(title_slug): # Load existing page markdown_content = load_page(title_slug) else: # Generate new page try: markdown_content = generate_article(page_title) save_page(title_slug, markdown_content) except HTTPException: raise except Exception as e: raise HTTPException(status_code=500, detail=f"Failed to generate article: {str(e)}") # Convert Markdown to HTML html_content = markdown2.markdown(markdown_content, extras=['tables', 'fenced-code-blocks']) # Render the final page template = Template(HTML_TEMPLATE) return template.render( title=page_title, style=CSS_STYLE, content=html_content ) # ============================================================================ # Main Execution # ============================================================================ if __name__ == "__main__": uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True)