#!/usr/bin/env python3

# /// script
# requires-python = ">=3.10"
# dependencies = []
# ///

"""
Web Launcher for Gateworks ARA Demo Apps
============================================
"""

import os
import sys
import time
import json
import glob
import socket
import signal
import threading
import subprocess
import urllib.request
import urllib.error
from http.server import HTTPServer, BaseHTTPRequestHandler

VERSION = "1.5.0"
PORT = 8888
TARGET_DEMO_PORT = 8080
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
VENV_PYTHON = os.path.join(SCRIPT_DIR, ".venv", "bin", "python")
DETECTION_MODEL_DIR = "/usr/share/cnn/detection"

def discover_cnn_models():
    """Scans /usr/share/cnn/detection for object detection .dvm models and cleans display names."""
    models = []
    if os.path.exists(DETECTION_MODEL_DIR):
        for root, _, files in os.walk(DETECTION_MODEL_DIR):
            for file in sorted(files):
                if file.endswith(".dvm"):
                    full_path = os.path.join(root, file)
                    folder_name = os.path.basename(os.path.dirname(full_path))
                    clean_name = folder_name if folder_name and folder_name != "detection" else os.path.splitext(file)[0]
                    models.append((clean_name, full_path))
    if not models:
        default_p = "/usr/share/cnn/detection/yolov8n/model.dvm"
        models.append(("yolov8n", default_p))
    return models

DISCOVERED_MODELS = discover_cnn_models()

# Fallback to current sys.executable if .venv/bin/python doesn't exist yet
PYTHON_BIN = VENV_PYTHON if os.path.exists(VENV_PYTHON) else sys.executable

DEMO_CONFIGS = {
    "vision-webapp": {
        "type": "web",
        "title": "Vision WebApp (YOLO Stream)",
        "desc": "Real-time object detection with web camera and video playback.",
        "cwd": SCRIPT_DIR,
        "cmd": [
            PYTHON_BIN, "vision-webapp.py",
            "--camera", "/dev/video_webcam",
            "--mp4", "/usr/share/media/sample_videos/",
            "--port", str(TARGET_DEMO_PORT)
        ],
        "est_seconds": 12
    },
    "webchat": {
        "type": "web",
        "title": "LLM Edge Web Chat (Qwen2.5-7B)",
        "desc": "Web-based conversational LLM assistant on NPU.",
        "cwd": SCRIPT_DIR,
        "cmd": [PYTHON_BIN, "webchat.py", "--port", str(TARGET_DEMO_PORT)],
        "est_seconds": 45
    },
    "webvlm": {
        "type": "web",
        "title": "VLM Edge Studio (Qwen2.5-VL-7B)",
        "desc": "Multi-modal vision-language video analyzer.",
        "cwd": SCRIPT_DIR,
        "cmd": [
            PYTHON_BIN, "webvlm.py",
            "--host", "0.0.0.0",
            "--port", str(TARGET_DEMO_PORT),
            "--video-dir", "/usr/share/media/sample_videos",
            "--aaf-server", "http://127.0.0.1:8000"
        ],
        "est_seconds": 60
    },
    "chat": {
        "type": "cli_interactive",
        "title": "Terminal LLM Chat (Interactive)",
        "desc": "Interactive LLM chat session with web console text input.",
        "cwd": SCRIPT_DIR,
        "cmd": [PYTHON_BIN, "chat.py"],
        "est_seconds": 5
    },
    "image-detect": {
        "type": "cli_image",
        "title": "Image Object Detection (Upload)",
        "desc": "Upload an image to detect objects via NPU and render annotated output.",
        "cwd": SCRIPT_DIR,
        "cmd": [PYTHON_BIN, "image_detect.py"],
        "est_seconds": 5
    }
}

state_lock = threading.Lock()
app_state = {
    "version": VERSION,
    "active_key": None,
    "demo_type": None,
    "status": "stopped",
    "progress": 0,
    "latest_log": "",
    "console_text": "",
    "executed_cmd": "",
    "output_image_url": None,
    "start_time": 0,
    "target_port": TARGET_DEMO_PORT
}
active_subprocess = None

def get_external_ip():
    try:
        s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        s.settimeout(0.5)
        s.connect(('10.254.254.254', 1))
        ip = s.getsockname()[0]
        s.close()
        return ip
    except Exception:
        try:
            return socket.gethostbyname(socket.gethostname())
        except Exception:
            return "127.0.0.1"

def clean_shm():
    for f in glob.glob("/dev/shm/ara_inf_*"):
        try:
            os.remove(f)
        except Exception:
            pass

def terminate_active_demo():
    global active_subprocess
    with state_lock:
        if active_subprocess is not None:
            try:
                pgid = os.getpgid(active_subprocess.pid)
                os.killpg(pgid, signal.SIGTERM)
                time.sleep(0.3)
            except Exception:
                try:
                    active_subprocess.terminate()
                except Exception:
                    pass
            active_subprocess = None
        
        clean_shm()

        app_state["active_key"] = None
        app_state["demo_type"] = None
        app_state["status"] = "stopped"
        app_state["progress"] = 0
        app_state["latest_log"] = "Process stopped."
        app_state["console_text"] = ""
        app_state["executed_cmd"] = ""
        app_state["output_image_url"] = None

def monitor_web_demo_startup(key):
    config = DEMO_CONFIGS[key]
    est_time = config.get("est_seconds", 30)
    target_url = f"http://127.0.0.1:{TARGET_DEMO_PORT}/"

    while True:
        with state_lock:
            if app_state["active_key"] != key or app_state["status"] != "starting":
                break
            
            elapsed = time.time() - app_state["start_time"]
            calc_progress = min(92, int((elapsed / est_time) * 100))
            app_state["progress"] = calc_progress

            try:
                req = urllib.request.Request(target_url, headers={"User-Agent": "LauncherHealthCheck"})
                with urllib.request.urlopen(req, timeout=1.0) as resp:
                    if resp.status in (200, 302, 301):
                        app_state["status"] = "running"
                        app_state["progress"] = 100
                        break
            except Exception:
                pass

        time.sleep(0.8)

def read_process_logs(proc, key, expected_out_path=None):
    fd = proc.stdout.fileno()
    try:
        while True:
            chunk_bytes = os.read(fd, 1024)
            if not chunk_bytes:
                break
            chunk = chunk_bytes.decode('utf-8', errors='ignore')
            with state_lock:
                if app_state["active_key"] == key:
                    app_state["console_text"] += chunk
                    if len(app_state["console_text"]) > 50000:
                        app_state["console_text"] = app_state["console_text"][-50000:]
                    
                    recent_lines = [line for line in app_state["console_text"].splitlines() if line.strip()]
                    app_state["latest_log"] = "\n".join(recent_lines[-12:])
    except Exception:
        pass

    proc.wait()
    with state_lock:
        if app_state["active_key"] == key:
            if DEMO_CONFIGS[key]["type"] in ("cli", "cli_interactive", "cli_image"):
                app_state["status"] = "completed"
                app_state["progress"] = 100
                
                if expected_out_path and os.path.exists(expected_out_path):
                    filename = os.path.basename(expected_out_path)
                    app_state["output_image_url"] = f"/uploads/{filename}"

def launch_demo_process(key, extra_args=None):
    global active_subprocess
    terminate_active_demo()
    clean_shm()

    config = DEMO_CONFIGS[key]
    demo_type = config["type"]
    cwd = config["cwd"]

    cmd = list(config["cmd"])
    if extra_args:
        cmd.extend(extra_args)

    cmd_str = " ".join(cmd)
    expected_out_path = extra_args[1] if (extra_args and len(extra_args) >= 2) else None

    with state_lock:
        app_state["active_key"] = key
        app_state["demo_type"] = demo_type
        app_state["status"] = "starting" if demo_type == "web" else "running"
        app_state["progress"] = 5 if demo_type == "web" else 50
        app_state["latest_log"] = f"Spawning {config['title']}..."
        app_state["executed_cmd"] = cmd_str
        app_state["console_text"] = f"=== Executing Command ===\n$ {cmd_str}\n\n"
        app_state["output_image_url"] = None
        app_state["start_time"] = time.time()

    env = dict(os.environ, PYTHONUNBUFFERED="1")
    venv_dir = os.path.join(SCRIPT_DIR, ".venv")
    if os.path.exists(venv_dir):
        env["VIRTUAL_ENV"] = venv_dir
        env["PATH"] = f"{os.path.join(venv_dir, 'bin')}:{env.get('PATH', '')}"

    try:
        proc = subprocess.Popen(
            cmd,
            cwd=cwd,
            stdin=subprocess.PIPE,
            stdout=subprocess.PIPE,
            stderr=subprocess.STDOUT,
            text=True,
            bufsize=1,
            env=env,
            preexec_fn=os.setsid
        )
        active_subprocess = proc
        
        threading.Thread(target=read_process_logs, args=(proc, key, expected_out_path), daemon=True).start()
        
        if demo_type == "web":
            threading.Thread(target=monitor_web_demo_startup, args=(key,), daemon=True).start()

    except Exception as e:
        with state_lock:
            app_state["status"] = "error"
            app_state["latest_log"] = f"Failed to spawn command: {str(e)}"
            app_state["console_text"] += f"\n[ERROR] {str(e)}\n"

class LauncherHandler(BaseHTTPRequestHandler):
    def log_message(self, format, *args):
        return

    def do_GET(self):
        if self.path in ("/", "/index.html"):
            self.serve_dashboard()
        elif self.path == "/api/status":
            self.serve_status()
        elif self.path.startswith("/uploads/"):
            filename = os.path.basename(self.path.split("?")[0])
            file_path = os.path.join(SCRIPT_DIR, filename)

            if os.path.exists(file_path):
                self.serve_static_file(file_path, "image/png" if filename.lower().endswith(".png") else "image/jpeg")
            else:
                self.send_error(404, f"File Not Found: {filename}")
        else:
            self.send_error(404, "Not Found")

    def serve_static_file(self, filepath, content_type):
        try:
            with open(filepath, "rb") as f:
                content = f.read()
            self.send_response(200)
            self.send_header('Content-Type', content_type)
            self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
            self.send_header('Content-Length', str(len(content)))
            self.end_headers()
            self.wfile.write(content)
        except Exception:
            self.send_error(500, "Error reading file")

    def do_POST(self):
        if self.path == "/api/upload_image":
            self.handle_image_upload()
            return

        content_length = int(self.headers.get('Content-Length', 0))
        body = self.rfile.read(content_length).decode('utf-8') if content_length > 0 else "{}"
        try:
            data = json.loads(body)
        except Exception:
            data = {}

        if self.path == "/api/launch":
            demo_key = data.get("demo")
            selected_model = data.get("model", DISCOVERED_MODELS[0][1])

            if demo_key in DEMO_CONFIGS:
                if demo_key == "image-detect":
                    in_p = os.path.join(SCRIPT_DIR, "dog.jpg")
                    out_p = os.path.join(SCRIPT_DIR, "coco_detections.jpg")
                    extra_args = [in_p, out_p, selected_model]
                else:
                    extra_args = None

                threading.Thread(target=launch_demo_process, args=(demo_key, extra_args), daemon=True).start()
                self.send_json_response({"status": "acknowledged"})
            else:
                self.send_error(400, "Invalid Demo Key")

        elif self.path == "/api/stop":
            threading.Thread(target=terminate_active_demo, daemon=True).start()
            self.send_json_response({"status": "stopped"})

        elif self.path == "/api/input":
            user_input = data.get("input", "")
            with state_lock:
                if active_subprocess and active_subprocess.stdin:
                    try:
                        active_subprocess.stdin.write(user_input + "\n")
                        active_subprocess.stdin.flush()
                        app_state["console_text"] += user_input + "\n"
                    except Exception:
                        pass
            self.send_json_response({"status": "sent"})
        else:
            self.send_error(404, "Endpoint Not Found")

    def handle_image_upload(self):
        try:
            content_length = int(self.headers.get('Content-Length', 0))
            body = self.rfile.read(content_length)
            
            ct = self.headers.get('Content-Type', '')
            if 'boundary=' not in ct:
                self.send_error(400, "Missing boundary in upload")
                return

            boundary_str = ct.split('boundary=')[1].split(';')[0].strip().strip('"')
            boundary = boundary_str.encode('utf-8')
            parts = body.split(b'--' + boundary)
            
            file_data = None
            filename = "input.jpg"
            selected_model = DISCOVERED_MODELS[0][1]
            
            for part in parts:
                if b'name="model"' in part:
                    model_val = part.split(b'\r\n\r\n', 1)[1].rsplit(b'\r\n', 1)[0].decode('utf-8').strip()
                    if model_val:
                        selected_model = model_val

                if b'filename="' in part:
                    header_and_data = part.split(b'\r\n\r\n', 1)
                    if len(header_and_data) == 2:
                        headers_str = header_and_data[0].decode('utf-8', errors='ignore')
                        for line in headers_str.split('\r\n'):
                            if 'filename="' in line:
                                filename = line.split('filename="')[1].split('"')[0]
                        file_data = header_and_data[1].rsplit(b'\r\n', 1)[0]

            if file_data:
                clean_name = os.path.basename(filename).replace(" ", "_")
                in_filename = clean_name
                out_filename = f"detected_{clean_name}"
                
                in_path = os.path.join(SCRIPT_DIR, in_filename)
                out_path = os.path.join(SCRIPT_DIR, out_filename)
                
                with open(in_path, "wb") as f:
                    f.write(file_data)

                threading.Thread(target=launch_demo_process, args=("image-detect", [in_path, out_path, selected_model]), daemon=True).start()
                self.send_json_response({"status": "uploaded", "input": in_filename, "output": out_filename})
            else:
                self.send_error(400, "No file payload received")
        except Exception as e:
            self.send_error(500, f"Upload error: {str(e)}")

    def send_json_response(self, obj):
        payload = json.dumps(obj).encode('utf-8')
        self.send_response(200)
        self.send_header('Content-Type', 'application/json')
        self.send_header('Access-Control-Allow-Origin', '*')
        self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
        self.send_header('Content-Length', str(len(payload)))
        self.end_headers()
        self.wfile.write(payload)

    def serve_status(self):
        with state_lock:
            self.send_json_response(app_state)

    def serve_dashboard(self):
        model_options_html = "".join([f'<option value="{path}">{label}</option>' for label, path in DISCOVERED_MODELS])

        html = r"""<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Gateworks AI Demos</title>
    <style>
        :root { --bg: #0d0e12; --panel: #16181d; --accent: #6366f1; --accent-hover: #4f46e5; --text: #e2e8f0; --muted: #94a3b8; --border: #262931; }
        * { box-sizing: border-box; margin: 0; padding: 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
        html, body { height: 100%; width: 100%; background: var(--bg); color: var(--text); overflow: hidden; display: flex; flex-direction: column; }
        
        header { background: var(--panel); border-bottom: 1px solid var(--border); padding: 12px 24px; height: 56px; display: flex; justify-content: space-between; align-items: center; flex-shrink: 0; z-index: 10; }
        .logo-title { display: flex; align-items: center; gap: 8px; font-weight: 700; font-size: 1.1rem; }
        .version-tag { font-size: 0.7rem; color: var(--muted); font-weight: normal; font-family: monospace; }
        .status-badge { font-size: 0.75rem; font-weight: 600; padding: 4px 10px; border-radius: 20px; background: #222630; color: var(--muted); border: 1px solid var(--border); text-transform: uppercase; }
        .status-badge.running { background: rgba(16, 185, 129, 0.15); color: #10b981; border-color: #10b981; }
        .status-badge.starting { background: rgba(245, 158, 11, 0.15); color: #f59e0b; border-color: #f59e0b; }
        .status-badge.completed { background: rgba(59, 130, 246, 0.15); color: #60a5fa; border-color: #3b82f6; }
        
        main { flex: 1; position: relative; width: 100%; height: calc(100vh - 56px); min-height: 0; display: flex; flex-direction: column; }
        
        #launcher-view { padding: 30px 24px; max-width: 1100px; margin: 0 auto; width: 100%; display: flex; flex-direction: column; gap: 20px; overflow-y: auto; }
        .section-title { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--muted); border-bottom: 1px solid var(--border); padding-bottom: 6px; margin-top: 10px; }
        .cards-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 16px; }
        .card { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 20px; display: flex; flex-direction: column; justify-content: space-between; gap: 16px; transition: transform 0.2s, border-color 0.2s; }
        .card:hover { transform: translateY(-2px); border-color: var(--accent); }
        .card h3 { font-size: 1rem; color: #fff; }
        .card p { font-size: 0.825rem; color: var(--muted); line-height: 1.4; }
        .btn { background: var(--accent); color: white; border: none; padding: 8px 14px; border-radius: 6px; font-weight: 600; cursor: pointer; transition: background 0.2s; text-align: center; font-size: 0.85rem; }
        .btn:hover { background: var(--accent-hover); }
        .btn-cli { background: #334155; }
        .btn-cli:hover { background: #475569; }
        .btn-stop { background: #ef4444; }
        .btn-stop:hover { background: #dc2626; }
        
        /* STRETCH LOADING OVERLAY TO FILL FULL WINDOW */
        #loading-overlay { flex: 1; width: 100%; height: 100%; background: var(--bg); display: flex; flex-direction: column; align-items: stretch; justify-content: flex-start; gap: 16px; padding: 24px; z-index: 5; box-sizing: border-box; overflow: hidden; }
        .progress-box { width: 100%; display: flex; flex-direction: column; gap: 10px; flex-shrink: 0; }
        .progress-bar-bg { background: var(--panel); border: 1px solid var(--border); height: 16px; border-radius: 20px; overflow: hidden; width: 100%; }
        .progress-bar-fill { background: linear-gradient(90deg, #6366f1, #a855f7); height: 100%; width: 0%; transition: width 0.4s ease; border-radius: 20px; }
        .log-terminal { background: #000; border: 1px solid var(--border); border-radius: 8px; padding: 16px; font-family: 'Courier New', Courier, monospace; font-size: 0.85rem; color: #34d399; width: 100%; flex: 1; min-height: 250px; overflow-y: auto; white-space: pre-wrap; word-break: break-all; }
        .loading-cmd-box { background: #000; border: 1px solid var(--border); border-radius: 6px; padding: 12px 16px; font-family: monospace; font-size: 0.85rem; color: #a5b4fc; width: 100%; word-break: break-all; text-align: left; flex-shrink: 0; }

        /* WEB APP WRAPPER WITH HEADER BAR */
        #web-container { display: none; flex-direction: column; width: 100%; height: 100%; flex: 1; min-height: 0; }
        #demo-frame { width: 100%; height: 100%; border: none; flex: 1; min-height: 0; }
        
        #cli-console-view { display: none; flex-direction: column; width: 100%; height: 100%; background: #000; border-top: 1px solid var(--border); flex: 1; min-height: 0; }
        .cli-header-bar { background: var(--panel); border-bottom: 1px solid var(--border); padding: 8px 16px; font-size: 0.85rem; font-weight: 700; color: #a5b4fc; display: flex; align-items: center; justify-content: space-between; flex-shrink: 0; }
        .cli-split-layout { display: flex; width: 100%; height: 100%; min-height: 0; flex: 1; }
        .cli-text-side { flex: 1; display: flex; flex-direction: column; height: 100%; min-height: 0; border-right: 1px solid var(--border); }
        .cli-image-side { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 20px; background: #08090c; overflow: auto; }
        .cli-image-side img { max-width: 100%; max-height: 100%; object-fit: contain; border-radius: 8px; border: 1px solid var(--border); }

        #cli-output { flex: 1; padding: 16px; font-family: 'Courier New', Courier, monospace; color: #34d399; overflow-y: auto; white-space: pre-wrap; word-break: break-all; font-size: 0.9rem; }
        .cli-input-bar { display: flex; background: var(--panel); padding: 10px 16px; border-top: 1px solid var(--border); gap: 10px; align-items: center; }
        .cli-input-bar input[type="text"] { flex: 1; background: #000; border: 1px solid var(--border); color: #fff; padding: 10px 14px; border-radius: 6px; font-family: monospace; font-size: 0.9rem; outline: none; }
        .cli-input-bar input[type="text"]:focus { border-color: var(--accent); }
        
        .img-control-group { display: flex; align-items: center; gap: 8px; font-size: 0.8rem; color: var(--muted); }
        .img-control-group select { background: #000; border: 1px solid var(--border); color: #fff; padding: 6px 10px; border-radius: 4px; font-size: 0.8rem; outline: none; }
        .img-control-group input[type="file"] { font-size: 0.75rem; color: var(--muted); }
    </style>
</head>
<body>
    <header>
        <div class="logo-title">
            <span>⚡ Gateworks AI Demos</span>
            <span class="version-tag">v__VERSION_TAG__</span>
        </div>
        <div style="display: flex; align-items: center; gap: 16px;">
            <span id="badge" class="status-badge">System Idle</span>
            <button id="stop-btn" class="btn btn-stop" style="display:none;" onclick="stopDemo()">Return to Menu</button>
        </div>
    </header>

    <main>
        <div id="launcher-view">
            <div class="section-title">Web Application Demos</div>
            <div class="cards-grid">
                <div class="card">
                    <div>
                        <h3>Vision WebApp</h3>
                        <p>Real-time GStreamer object detection pipeline with live camera feed and video selection.</p>
                    </div>
                    <button class="btn" onclick="launchDemo('vision-webapp')">Launch Web App</button>
                </div>
                <div class="card">
                    <div>
                        <h3>LLM Web Chat</h3>
                        <p>Interactive web chat assistant powered by Qwen2.5-7B running on NPU acceleration.</p>
                    </div>
                    <button class="btn" onclick="launchDemo('webchat')">Launch Web App</button>
                </div>
                <div class="card">
                    <div>
                        <h3>VLM Edge Studio</h3>
                        <p>Multi-modal Qwen2.5-VL-7B vision model analyzing video clips in conversational mode.</p>
                    </div>
                    <button class="btn" onclick="launchDemo('webvlm')">Launch Web App</button>
                </div>
            </div>

            <div class="section-title">Command Line Console Demos</div>
            <div class="cards-grid">
                <div class="card">
                    <div>
                        <h3>Terminal LLM Chat</h3>
                        <p>Command-line LLM prompt utility outputting directly to console stream.</p>
                    </div>
                    <button class="btn btn-cli" onclick="launchDemo('chat')">Run in Console</button>
                </div>
                <div class="card">
                    <div>
                        <h3>Image Object Detection</h3>
                        <p>Run object detection on sample or uploaded image files and view annotated outputs.</p>
                    </div>
                    <button class="btn btn-cli" onclick="launchDemo('image-detect')">Run Console Demo</button>
                </div>
            </div>
        </div>

        <!-- FULL-WIDTH/HEIGHT LOADING OVERLAY -->
        <div id="loading-overlay" style="display: none;">
            <h3 id="load-title" style="font-size: 1.1rem; color: #fff;">Initializing Service...</h3>
            <div id="loading-cmd-box" class="loading-cmd-box">$ --</div>
            <div class="progress-box">
                <div class="progress-bar-bg">
                    <div id="progress-fill" class="progress-bar-fill"></div>
                </div>
                <div style="display: flex; justify-content: space-between; font-size: 0.85rem; color: var(--muted);">
                    <span id="progress-text">Preparing process...</span>
                    <span id="progress-pct">0%</span>
                </div>
            </div>
            <div id="log-terminal" class="log-terminal">Awaiting startup logs...</div>
        </div>

        <!-- WEB APP IFRAME VIEW WITH HEADER BAR -->
        <div id="web-container">
            <div class="cli-header-bar">
                <span id="web-header-title">Web Application</span>
                <span id="web-cmd-display" style="font-family: monospace; font-size: 0.75rem; color: var(--muted); font-weight: normal;"></span>
            </div>
            <iframe id="demo-frame"></iframe>
        </div>

        <div id="cli-console-view">
            <div class="cli-header-bar">
                <span id="cli-header-title">Console Output</span>
                <span id="cli-cmd-display" style="font-family: monospace; font-size: 0.75rem; color: var(--muted); font-weight: normal;"></span>
            </div>

            <div class="cli-split-layout">
                <div class="cli-text-side">
                    <div id="cli-output"></div>
                    
                    <div id="cli-input-container" class="cli-input-bar">
                        <input id="cli-input" type="text" placeholder="Type prompt here and press Enter..." onkeydown="handleCliKeyDown(event)" />
                        <button class="btn" onclick="sendCliInput()">Send</button>
                    </div>

                    <div id="cli-image-controls" class="cli-input-bar" style="display: none;">
                        <div class="img-control-group">
                            <label for="console-model-selector">Model:</label>
                            <select id="console-model-selector">__MODEL_OPTIONS__</select>
                        </div>
                        <div class="img-control-group">
                            <label for="console-file-selector">File:</label>
                            <input type="file" id="console-file-selector" accept="image/*" />
                        </div>
                        <button class="btn" onclick="runConsoleImageDetect()">Detect Objects</button>
                    </div>
                </div>
                
                <div id="cli-image-container" class="cli-image-side" style="display: none;">
                    <div style="color: var(--muted); font-size: 0.8rem; margin-bottom: 8px;">Annotated Target Output:</div>
                    <img id="cli-annotated-img" src="" alt="Detection Target Result" />
                </div>
            </div>
        </div>
    </main>

    <script>
        let lastRenderedImgUrl = "";

        window.launchDemo = async function(key) {
            lastRenderedImgUrl = "";
            try {
                await fetch('/api/launch', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ demo: key })
                });
            } catch (e) {
                console.error("Failed to launch demo:", e);
            }
        };

        window.runConsoleImageDetect = async function() {
            lastRenderedImgUrl = "";
            const fileInput = document.getElementById('console-file-selector');
            const modelSelector = document.getElementById('console-model-selector');
            
            if (fileInput.files && fileInput.files.length > 0) {
                const formData = new FormData();
                formData.append('image', fileInput.files[0]);
                formData.append('model', modelSelector.value);

                try {
                    await fetch('/api/upload_image', {
                        method: 'POST',
                        body: formData
                    });
                } catch (e) {
                    console.error("Upload failed:", e);
                }
            } else {
                try {
                    await fetch('/api/launch', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ demo: "image-detect", model: modelSelector.value })
                    });
                } catch (e) {}
            }
        };

        window.stopDemo = async function() {
            lastRenderedImgUrl = "";
            try {
                await fetch('/api/stop', { method: 'POST' });
                document.getElementById('demo-frame').src = "about:blank";
            } catch (e) {
                console.error("Failed to stop demo:", e);
            }
        };

        window.sendCliInput = async function() {
            const inputElem = document.getElementById('cli-input');
            const val = inputElem.value;
            if (!val) return;
            inputElem.value = '';
            try {
                await fetch('/api/input', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ input: val })
                });
            } catch (e) {
                console.error("Failed to send input:", e);
            }
        };

        window.handleCliKeyDown = function(event) {
            if (event.key === 'Enter') {
                event.preventDefault();
                sendCliInput();
            }
        };

        async function pollStatus() {
            try {
                const res = await fetch('/api/status');
                const data = await res.json();

                const badge = document.getElementById('badge');
                const stopBtn = document.getElementById('stop-btn');
                const launcherView = document.getElementById('launcher-view');
                const loadingOverlay = document.getElementById('loading-overlay');
                const loadingCmdBox = document.getElementById('loading-cmd-box');
                const webContainer = document.getElementById('web-container');
                const demoFrame = document.getElementById('demo-frame');
                const webHeaderTitle = document.getElementById('web-header-title');
                const webCmdDisplay = document.getElementById('web-cmd-display');
                const cliConsoleView = document.getElementById('cli-console-view');
                const cliHeaderTitle = document.getElementById('cli-header-title');
                const cliCmdDisplay = document.getElementById('cli-cmd-display');
                const cliOutput = document.getElementById('cli-output');
                const cliInputContainer = document.getElementById('cli-input-container');
                const cliImageControls = document.getElementById('cli-image-controls');
                const cliImageContainer = document.getElementById('cli-image-container');
                const cliAnnotatedImg = document.getElementById('cli-annotated-img');

                badge.textContent = data.status.toUpperCase();
                badge.className = 'status-badge ' + data.status;

                if (data.status === 'starting' && data.demo_type === 'web') {
                    launcherView.style.display = 'none';
                    webContainer.style.display = 'none';
                    cliConsoleView.style.display = 'none';
                    loadingOverlay.style.display = 'flex';
                    stopBtn.style.display = 'inline-block';

                    document.getElementById('load-title').textContent = 'Loading ' + data.active_key.toUpperCase() + ' Model...';
                    loadingCmdBox.textContent = data.executed_cmd ? '$ ' + data.executed_cmd : '$ --';
                    document.getElementById('progress-fill').style.width = data.progress + '%';
                    document.getElementById('progress-pct').textContent = data.progress + '%';
                    document.getElementById('progress-text').textContent = data.progress < 90 ? "Initializing NPU & Pipeline..." : "Verifying endpoint readiness...";
                    
                    const logTerminal = document.getElementById('log-terminal');
                    logTerminal.textContent = data.latest_log || "Bootstrapping background worker...";
                    logTerminal.scrollTop = logTerminal.scrollHeight;

                } else if (data.status === 'running' && data.demo_type === 'web') {
                    launcherView.style.display = 'none';
                    loadingOverlay.style.display = 'none';
                    cliConsoleView.style.display = 'none';
                    webContainer.style.display = 'flex';
                    stopBtn.style.display = 'inline-block';

                    webHeaderTitle.textContent = (data.active_key || 'Web App') + ': ' + data.active_key + '.py';
                    webCmdDisplay.textContent = data.executed_cmd ? '$ ' + data.executed_cmd : '';

                    const targetSrc = 'http://' + window.location.hostname + ':8080/';
                    if (demoFrame.src !== targetSrc) {
                        demoFrame.src = targetSrc;
                    }

                } else if ((data.status === 'running' || data.status === 'completed') && (data.demo_type === 'cli' || data.demo_type === 'cli_interactive' || data.demo_type === 'cli_image')) {
                    launcherView.style.display = 'none';
                    loadingOverlay.style.display = 'none';
                    webContainer.style.display = 'none';
                    cliConsoleView.style.display = 'flex';
                    stopBtn.style.display = 'inline-block';

                    if (data.active_key === 'chat') {
                        cliHeaderTitle.textContent = 'LLM Chat: chat.py';
                    } else if (data.active_key === 'image-detect') {
                        cliHeaderTitle.textContent = 'Image Object Detection: image_detect.py';
                    } else {
                        cliHeaderTitle.textContent = data.active_key || 'Console Output';
                    }

                    cliCmdDisplay.textContent = data.executed_cmd ? '$ ' + data.executed_cmd : '';

                    cliInputContainer.style.display = (data.demo_type === 'cli_interactive' && data.status === 'running') ? 'flex' : 'none';
                    cliImageControls.style.display = (data.demo_type === 'cli_image') ? 'flex' : 'none';

                    if (data.output_image_url && data.status === 'completed') {
                        cliImageContainer.style.display = 'flex';
                        if (lastRenderedImgUrl !== data.output_image_url) {
                            lastRenderedImgUrl = data.output_image_url;
                            cliAnnotatedImg.src = data.output_image_url;
                        }
                    } else {
                        cliImageContainer.style.display = 'none';
                    }

                    if (cliOutput.textContent !== data.console_text) {
                        const isAtBottom = (cliOutput.scrollHeight - cliOutput.scrollTop - cliOutput.clientHeight) < 60;
                        cliOutput.textContent = data.console_text;
                        if (isAtBottom || data.status === 'starting') {
                            cliOutput.scrollTop = cliOutput.scrollHeight;
                        }
                    }

                } else {
                    launcherView.style.display = 'flex';
                    loadingOverlay.style.display = 'none';
                    webContainer.style.display = 'none';
                    cliConsoleView.style.display = 'none';
                    stopBtn.style.display = 'none';
                    demoFrame.src = "about:blank";
                }
            } catch (e) {}
        }

        setInterval(pollStatus, 500);
        pollStatus();
    </script>
</body>
</html>""".replace("__VERSION_TAG__", VERSION).replace("__MODEL_OPTIONS__", model_options_html)

        payload = html.encode('utf-8')
        self.send_response(200)
        self.send_header('Content-Type', 'text/html; charset=utf-8')
        self.send_header('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
        self.send_header('Content-Length', str(len(payload)))
        self.end_headers()
        self.wfile.write(payload)

class ReusableHTTPServer(HTTPServer):
    allow_reuse_address = True

def run_server():
    clean_shm()
    external_ip = get_external_ip()
    server_address = ('', PORT)
    httpd = ReusableHTTPServer(server_address, LauncherHandler)
    print(f"\n=======================================================")
    print(f"🚀 Gateworks AI Demos Launcher v{VERSION} listening on http://{external_ip}:{PORT}")
    print(f"=======================================================\n", flush=True)
    try:
        httpd.serve_forever()
    except KeyboardInterrupt:
        print("\nShutting down launcher...")
        terminate_active_demo()
        httpd.server_close()

if __name__ == '__main__':
    run_server()
