Discode / app.py
VirusDumb's picture
gemma 12b
3aad08f
Raw
History Blame Contribute Delete
10.6 kB
"""
Discode — chat your way to a live web app.
Left: a slim, collapsible chat rail. Talk to the AI, ask for an app, then ask
for changes ("make the snake green", "add a score counter").
Right: the generated app, rendered live with full JavaScript.
Theme: Frutiger Aero / skeuomorphic glass.
Model: Gemma 4 12B via llama.cpp.
Local: start `llama-server -hf ggml-org/gemma-4-12B-it-GGUF:Q4_K_M --jinja -c 4096`
before running this app, or install llama-cpp-python so the app can spawn it.
Space: the app spawns llama_cpp.server on CPU Basic unless a server is already running.
"""
import os
import re
import sys
import time
import html as html_lib
import subprocess
import gradio as gr
import requests
from agno.agent import Agent
from agno.models.llama_cpp import LlamaCpp
MODEL_REPO = os.environ.get("MODEL_REPO", "ggml-org/gemma-4-12B-it-GGUF")
MODEL_FILE = os.environ.get("MODEL_FILE", "gemma-4-12B-it-Q4_K_M.gguf")
HOST = os.environ.get("LLAMACPP_HOST", "127.0.0.1")
PORT = int(os.environ.get("LLAMACPP_PORT", "8080"))
BASE_URL = os.environ.get("LLAMACPP_BASE_URL", f"http://{HOST}:{PORT}/v1")
N_CTX = os.environ.get("LLAMACPP_CTX", "4096")
N_THREADS = os.environ.get("LLAMACPP_THREADS", "2")
SYSTEM_PROMPT = """You are Discode, a friendly expert front-end engineer who builds and edits ONE single-page web app for the user through conversation.
On every turn where the user wants an app or a change:
1. First write ONE short, friendly sentence (what you built or changed).
2. Then output the COMPLETE, updated HTML document inside a single ```html ... ``` code block.
The HTML must be:
- A full self-contained document: <!DOCTYPE html>, <html>, <head>, <body>.
- Inline CSS (in <style>) and JS (in <script>) only — NO external files, CDNs, or network calls.
- Polished, responsive, and fully functional, using full JavaScript freely (canvas, Web Audio, localStorage, timers, etc.).
When the user asks for a change, MODIFY the current app (it will be given to you) and return the ENTIRE updated document again — never a diff or a partial snippet.
If the user is only chatting (greeting, a question) and not asking for an app or change, reply normally with NO code block.
"""
def server_is_up() -> bool:
try:
return requests.get(f"{BASE_URL}/models", timeout=2).status_code == 200
except requests.exceptions.RequestException:
return False
server_process = None
if server_is_up():
print(f"[startup] Found llama.cpp server at {BASE_URL}", flush=True)
else:
print("[startup] No llama.cpp server found; downloading GGUF and spawning llama_cpp.server ...", flush=True)
from huggingface_hub import hf_hub_download
model_path = hf_hub_download(repo_id=MODEL_REPO, filename=MODEL_FILE)
server_process = subprocess.Popen(
[
sys.executable,
"-m",
"llama_cpp.server",
"--model",
model_path,
"--host",
HOST,
"--port",
str(PORT),
"--n_ctx",
N_CTX,
"--n_threads",
N_THREADS,
]
)
print("[startup] Waiting for llama.cpp server ...", flush=True)
for _ in range(360):
if server_is_up():
print("[startup] llama.cpp server is ready.", flush=True)
break
time.sleep(1)
else:
raise RuntimeError("llama.cpp server did not start in time")
agent = Agent(
model=LlamaCpp(id=MODEL_FILE, base_url=BASE_URL, api_key="sk-no-key-needed"),
instructions=SYSTEM_PROMPT,
markdown=False,
debug_mode=True,
)
# --- HTML extraction / parsing ----------------------------------------------
def _extract_doc(text: str) -> str:
lower = text.lower()
start = lower.find("<!doctype")
if start == -1:
start = lower.find("<html")
end = lower.rfind("</html>")
if start != -1 and end != -1:
return text[start:end + len("</html>")]
return text.strip()
def parse_response(text: str):
"""Split the model output into (chat_message, html_or_None)."""
text = (text or "").strip()
fence = re.search(r"```(?:html)?\s*(.*?)```", text, re.DOTALL | re.IGNORECASE)
if fence:
html = _extract_doc(fence.group(1).strip())
chat = (text[:fence.start()] + text[fence.end():]).strip()
return (chat or "Here's your app! ✨"), html
# No fence, but maybe a raw document.
if "<html" in text.lower() and "</html>" in text.lower():
return "Here's your app! ✨", _extract_doc(text)
# Pure chat, no app change.
return text, None
def render_iframe(site_html: str) -> str:
"""Embed the generated site with full JS capability (no restrictive sandbox)."""
srcdoc = html_lib.escape(site_html or "", quote=True)
return (
f'<iframe srcdoc="{srcdoc}" '
'allow="autoplay; fullscreen; clipboard-write; gamepad; accelerometer; gyroscope" '
'class="aero-frame"></iframe>'
)
WELCOME = """
<div class="aero-welcome">
<div class="bubble"></div>
<h2>✨ Your app appears here</h2>
<p>Ask me in the chat to build something — a game, a tool, a toy.</p>
</div>
"""
# --- Chat handler -----------------------------------------------------------
def on_send(user_msg, messages, current_html):
messages = messages or []
if not user_msg or not user_msg.strip():
return messages, gr.update(), current_html, ""
messages.append({"role": "user", "content": user_msg})
if current_html:
prompt = (
"The current app HTML is:\n```html\n" + current_html + "\n```\n\n"
"User request: " + user_msg
)
else:
prompt = user_msg
started = time.time()
result = agent.run(prompt)
elapsed = time.time() - started
chat_text, new_html = parse_response(result.content)
chat_text = f"{chat_text}\n\n_responded in {elapsed:.1f}s via Gemma 4 12B Q4 on llama.cpp_"
messages.append({"role": "assistant", "content": chat_text})
if new_html:
return messages, render_iframe(new_html), new_html, ""
return messages, gr.update(), current_html, ""
def toggle_chat(is_visible):
is_visible = not is_visible
label = "◀ Hide chat" if is_visible else "Chat ▶"
return gr.update(visible=is_visible), label, is_visible
# --- Frutiger Aero / skeuomorphic CSS ---------------------------------------
AERO_CSS = """
.gradio-container {
background: linear-gradient(180deg,#5db4e0 0%,#9fe0ef 30%,#cdf3d4 70%,#9bd86f 100%) fixed !important;
font-family: 'Segoe UI','Frutiger','Myriad Pro',sans-serif !important;
}
/* floating bubbles overlay */
.gradio-container::before {
content:""; position:fixed; inset:0; pointer-events:none; z-index:0;
background:
radial-gradient(circle at 12% 80%, rgba(255,255,255,0.5) 0 8px, transparent 9px),
radial-gradient(circle at 22% 60%, rgba(255,255,255,0.35) 0 14px, transparent 15px),
radial-gradient(circle at 85% 75%, rgba(255,255,255,0.4) 0 20px, transparent 21px),
radial-gradient(circle at 70% 30%, rgba(255,255,255,0.3) 0 10px, transparent 11px);
}
#aero-title { text-align:center; }
#aero-title h1 {
color:#fff; font-weight:800; letter-spacing:.5px;
text-shadow: 0 1px 0 rgba(255,255,255,.5), 0 2px 6px rgba(0,70,110,.6);
}
/* glassy panels */
#chat-col, #preview-col {
background: rgba(255,255,255,0.30) !important;
border: 1px solid rgba(255,255,255,0.75) !important;
border-radius: 20px !important;
box-shadow: 0 10px 34px rgba(0,60,90,0.30), inset 0 1px 0 rgba(255,255,255,0.95) !important;
backdrop-filter: blur(14px) saturate(170%);
-webkit-backdrop-filter: blur(14px) saturate(170%);
padding: 12px !important;
}
/* glossy buttons */
.aero-btn, button.primary {
background: linear-gradient(180deg,#c8f99a 0%,#86d943 47%,#5cb52a 53%,#9ae866 100%) !important;
border: 1px solid #4e9c1f !important;
border-radius: 13px !important;
color: #133f08 !important; font-weight: 700 !important;
box-shadow: inset 0 1px 0 rgba(255,255,255,0.85), 0 3px 9px rgba(0,0,0,0.22) !important;
text-shadow: 0 1px 0 rgba(255,255,255,0.6) !important;
}
.aero-btn:hover, button.primary:hover { filter: brightness(1.07); }
/* inputs / chatbot glassy */
#chat-col textarea, #chat-col input {
background: rgba(255,255,255,0.7) !important;
border: 1px solid rgba(255,255,255,0.9) !important;
border-radius: 12px !important;
box-shadow: inset 0 2px 5px rgba(0,60,90,0.15) !important;
}
#chatbox { background: transparent !important; border: none !important; }
/* live preview frame */
.aero-frame {
width:100%; height:78vh; border:none; border-radius:14px; background:#fff;
box-shadow: inset 0 0 0 1px rgba(255,255,255,.8), 0 6px 18px rgba(0,50,80,.25);
}
.aero-welcome {
height:78vh; display:flex; flex-direction:column; align-items:center; justify-content:center;
color:#0a3a52; text-align:center; border-radius:14px;
background: linear-gradient(180deg, rgba(255,255,255,.55), rgba(255,255,255,.25));
box-shadow: inset 0 1px 0 rgba(255,255,255,.9);
}
.aero-welcome h2 { text-shadow: 0 1px 0 rgba(255,255,255,.7); }
"""
# --- UI ---------------------------------------------------------------------
with gr.Blocks(css=AERO_CSS, theme=gr.themes.Soft(), title="Discode") as demo:
chat_visible = gr.State(True)
current_html = gr.State("")
gr.Markdown("# 🏃 Discode", elem_id="aero-title")
with gr.Row():
# Left: slim chat rail
with gr.Column(scale=2, min_width=280, elem_id="chat-col") as chat_col:
chatbot = gr.Chatbot(
height="62vh",
elem_id="chatbox",
show_label=False,
avatar_images=(None, None),
)
prompt = gr.Textbox(
placeholder="Build me a neon snake game…",
show_label=False,
lines=2,
)
send = gr.Button("Send ✨", variant="primary", elem_classes=["aero-btn"])
# Right: big live preview
with gr.Column(scale=7, elem_id="preview-col"):
with gr.Row():
toggle = gr.Button("◀ Hide chat", elem_classes=["aero-btn"], scale=0)
preview = gr.HTML(WELCOME)
# wiring
send.click(on_send, [prompt, chatbot, current_html], [chatbot, preview, current_html, prompt])
prompt.submit(on_send, [prompt, chatbot, current_html], [chatbot, preview, current_html, prompt])
toggle.click(toggle_chat, [chat_visible], [chat_col, toggle, chat_visible])
if __name__ == "__main__":
demo.launch()