VirusDumb commited on
Commit
1ee6c4a
·
1 Parent(s): ac46b12
Files changed (2) hide show
  1. __pycache__/app.cpython-312.pyc +0 -0
  2. app.py +174 -66
__pycache__/app.cpython-312.pyc ADDED
Binary file (10.6 kB). View file
 
app.py CHANGED
@@ -1,14 +1,13 @@
1
  """
2
- Discode — describe a web app in plain language, get a live, self-contained
3
- HTML site rendered right inside the Gradio app.
4
 
5
- A Gradio reimagining of the original Discord bot (50umy4j1t/DiscodeAlpha):
6
- instead of saving the HTML and exposing it via ngrok, we render the model's
7
- output inline in an isolated <iframe srcdoc>.
8
 
 
9
  Model: Gemma 4 31B via Ollama Cloud (prototyping).
10
  Requires: pip install -U ollama agno gradio and OLLAMA_API_KEY set.
11
- (Cloud API = prototyping only; swap to llama.cpp for the badge-winning build.)
12
  """
13
 
14
  import os
@@ -21,17 +20,20 @@ from agno.models.ollama import Ollama
21
 
22
  OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "gemma4:31b-cloud")
23
 
24
- SYSTEM_PROMPT = """You are Discode, an expert front-end engineer.
25
 
26
- Given a user's request, you produce a COMPLETE, self-contained HTML document
27
- that fulfils it.
 
28
 
29
- Hard rules:
30
- - Output ONLY raw HTML. No markdown, no code fences, no commentary before or after.
31
- - Start with <!DOCTYPE html> and include <html>, <head>, and <body>.
32
- - Everything must be inline: put CSS in a <style> tag and JS in a <script> tag.
33
- - NO external files, CDNs, or network requests — it must work fully offline.
34
- - Make it visually polished, responsive, and fully functional (client-side only).
 
 
35
  """
36
 
37
  agent = Agent(
@@ -41,16 +43,8 @@ agent = Agent(
41
  )
42
 
43
 
44
- def extract_html(text: str) -> str:
45
- """Pull a clean HTML document out of the model's raw output."""
46
- text = (text or "").strip()
47
-
48
- # Prefer a fenced code block if the model added one despite instructions.
49
- fence = re.search(r"```(?:html)?\s*(.*?)```", text, re.DOTALL | re.IGNORECASE)
50
- if fence:
51
- text = fence.group(1).strip()
52
-
53
- # If a full document is present, slice exactly from <!doctype>/<html> to </html>.
54
  lower = text.lower()
55
  start = lower.find("<!doctype")
56
  if start == -1:
@@ -58,66 +52,180 @@ def extract_html(text: str) -> str:
58
  end = lower.rfind("</html>")
59
  if start != -1 and end != -1:
60
  return text[start:end + len("</html>")]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
- return text
 
63
 
64
 
65
  def render_iframe(site_html: str) -> str:
66
- """Embed the generated site in an iframe with full JS capability.
67
-
68
- We intentionally do NOT add a restrictive `sandbox` attribute so generated
69
- apps get the full browser: localStorage/IndexedDB, canvas, Web Audio,
70
- timers, fullscreen, etc. `allow` grants commonly-needed feature policies.
71
- (Trade-off: generated JS runs with the page's origin — fine for a demo where
72
- users render their own requested apps.)
73
- """
74
- srcdoc = html_lib.escape(site_html, quote=True)
75
  return (
76
  f'<iframe srcdoc="{srcdoc}" '
77
  'allow="autoplay; fullscreen; clipboard-write; gamepad; accelerometer; gyroscope" '
78
- 'style="width:100%;height:70vh;border:1px solid #444;border-radius:12px;background:#fff;">'
79
- "</iframe>"
80
  )
81
 
82
 
83
- def generate(prompt: str):
84
- if not prompt or not prompt.strip():
85
- return "<p style='color:#888'>Type what you want me to build above. 👆</p>", ""
86
- if not os.environ.get("OLLAMA_API_KEY"):
87
- return "<p style='color:#c00'>OLLAMA_API_KEY is not set.</p>", ""
 
 
88
 
89
- result = agent.run(prompt)
90
- site_html = extract_html(result.content)
91
- return render_iframe(site_html), site_html
92
 
 
 
 
 
 
93
 
94
- EXAMPLES = [
95
- "A neon-themed Snake game with a score counter and arrow-key controls.",
96
- "A calming breathing exercise: a circle that scales in and out with a 4-7-8 timer.",
97
- "A retro pixel-art landing page for a fictional lo-fi radio station.",
98
- "A pomodoro timer with a tree that grows as the session progresses.",
99
- ]
100
 
101
- with gr.Blocks(title="Discode", theme=gr.themes.Soft()) as demo:
102
- gr.Markdown("# 🏃 Discode\nDescribe a web app — get a live, self-contained site, rendered right here.")
103
 
104
- with gr.Row():
105
- prompt = gr.Textbox(
106
- label="What should I build?",
107
- placeholder="e.g. a neon snake game",
108
- lines=2,
109
- scale=4,
110
  )
111
- btn = gr.Button("Generate ✨", variant="primary", scale=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
- gr.Examples(examples=EXAMPLES, inputs=prompt)
 
 
 
114
 
115
- preview = gr.HTML(label="Live preview")
116
- with gr.Accordion("View generated HTML", open=False):
117
- code = gr.Code(language="html", label="Source")
118
 
119
- btn.click(generate, inputs=prompt, outputs=[preview, code])
120
- prompt.submit(generate, inputs=prompt, outputs=[preview, code])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
  if __name__ == "__main__":
123
  demo.launch()
 
1
  """
2
+ Discode — chat your way to a live web app.
 
3
 
4
+ Left: a slim, collapsible chat rail. Talk to the AI, ask for an app, then ask
5
+ for changes ("make the snake green", "add a score counter").
6
+ Right: the generated app, rendered live with full JavaScript.
7
 
8
+ Theme: Frutiger Aero / skeuomorphic glass.
9
  Model: Gemma 4 31B via Ollama Cloud (prototyping).
10
  Requires: pip install -U ollama agno gradio and OLLAMA_API_KEY set.
 
11
  """
12
 
13
  import os
 
20
 
21
  OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "gemma4:31b-cloud")
22
 
23
+ 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.
24
 
25
+ On every turn where the user wants an app or a change:
26
+ 1. First write ONE short, friendly sentence (what you built or changed).
27
+ 2. Then output the COMPLETE, updated HTML document inside a single ```html ... ``` code block.
28
 
29
+ The HTML must be:
30
+ - A full self-contained document: <!DOCTYPE html>, <html>, <head>, <body>.
31
+ - Inline CSS (in <style>) and JS (in <script>) only — NO external files, CDNs, or network calls.
32
+ - Polished, responsive, and fully functional, using full JavaScript freely (canvas, Web Audio, localStorage, timers, etc.).
33
+
34
+ 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.
35
+
36
+ If the user is only chatting (greeting, a question) and not asking for an app or change, reply normally with NO code block.
37
  """
38
 
39
  agent = Agent(
 
43
  )
44
 
45
 
46
+ # --- HTML extraction / parsing ----------------------------------------------
47
+ def _extract_doc(text: str) -> str:
 
 
 
 
 
 
 
 
48
  lower = text.lower()
49
  start = lower.find("<!doctype")
50
  if start == -1:
 
52
  end = lower.rfind("</html>")
53
  if start != -1 and end != -1:
54
  return text[start:end + len("</html>")]
55
+ return text.strip()
56
+
57
+
58
+ def parse_response(text: str):
59
+ """Split the model output into (chat_message, html_or_None)."""
60
+ text = (text or "").strip()
61
+
62
+ fence = re.search(r"```(?:html)?\s*(.*?)```", text, re.DOTALL | re.IGNORECASE)
63
+ if fence:
64
+ html = _extract_doc(fence.group(1).strip())
65
+ chat = (text[:fence.start()] + text[fence.end():]).strip()
66
+ return (chat or "Here's your app! ✨"), html
67
+
68
+ # No fence, but maybe a raw document.
69
+ if "<html" in text.lower() and "</html>" in text.lower():
70
+ return "Here's your app! ✨", _extract_doc(text)
71
 
72
+ # Pure chat, no app change.
73
+ return text, None
74
 
75
 
76
  def render_iframe(site_html: str) -> str:
77
+ """Embed the generated site with full JS capability (no restrictive sandbox)."""
78
+ srcdoc = html_lib.escape(site_html or "", quote=True)
 
 
 
 
 
 
 
79
  return (
80
  f'<iframe srcdoc="{srcdoc}" '
81
  'allow="autoplay; fullscreen; clipboard-write; gamepad; accelerometer; gyroscope" '
82
+ 'class="aero-frame"></iframe>'
 
83
  )
84
 
85
 
86
+ WELCOME = """
87
+ <div class="aero-welcome">
88
+ <div class="bubble"></div>
89
+ <h2>✨ Your app appears here</h2>
90
+ <p>Ask me in the chat to build something — a game, a tool, a toy.</p>
91
+ </div>
92
+ """
93
 
 
 
 
94
 
95
+ # --- Chat handler -----------------------------------------------------------
96
+ def on_send(user_msg, messages, current_html):
97
+ messages = messages or []
98
+ if not user_msg or not user_msg.strip():
99
+ return messages, gr.update(), current_html, ""
100
 
101
+ if not os.environ.get("OLLAMA_API_KEY"):
102
+ messages.append({"role": "user", "content": user_msg})
103
+ messages.append({"role": "assistant", "content": "⚠️ OLLAMA_API_KEY is not set."})
104
+ return messages, gr.update(), current_html, ""
 
 
105
 
106
+ messages.append({"role": "user", "content": user_msg})
 
107
 
108
+ if current_html:
109
+ prompt = (
110
+ "The current app HTML is:\n```html\n" + current_html + "\n```\n\n"
111
+ "User request: " + user_msg
 
 
112
  )
113
+ else:
114
+ prompt = user_msg
115
+
116
+ result = agent.run(prompt)
117
+ chat_text, new_html = parse_response(result.content)
118
+
119
+ messages.append({"role": "assistant", "content": chat_text})
120
+
121
+ if new_html:
122
+ return messages, render_iframe(new_html), new_html, ""
123
+ return messages, gr.update(), current_html, ""
124
+
125
+
126
+ def toggle_chat(is_visible):
127
+ is_visible = not is_visible
128
+ label = "◀ Hide chat" if is_visible else "Chat ▶"
129
+ return gr.update(visible=is_visible), label, is_visible
130
+
131
+
132
+ # --- Frutiger Aero / skeuomorphic CSS ---------------------------------------
133
+ AERO_CSS = """
134
+ .gradio-container {
135
+ background: linear-gradient(180deg,#5db4e0 0%,#9fe0ef 30%,#cdf3d4 70%,#9bd86f 100%) fixed !important;
136
+ font-family: 'Segoe UI','Frutiger','Myriad Pro',sans-serif !important;
137
+ }
138
+ /* floating bubbles overlay */
139
+ .gradio-container::before {
140
+ content:""; position:fixed; inset:0; pointer-events:none; z-index:0;
141
+ background:
142
+ radial-gradient(circle at 12% 80%, rgba(255,255,255,0.5) 0 8px, transparent 9px),
143
+ radial-gradient(circle at 22% 60%, rgba(255,255,255,0.35) 0 14px, transparent 15px),
144
+ radial-gradient(circle at 85% 75%, rgba(255,255,255,0.4) 0 20px, transparent 21px),
145
+ radial-gradient(circle at 70% 30%, rgba(255,255,255,0.3) 0 10px, transparent 11px);
146
+ }
147
+ #aero-title { text-align:center; }
148
+ #aero-title h1 {
149
+ color:#fff; font-weight:800; letter-spacing:.5px;
150
+ text-shadow: 0 1px 0 rgba(255,255,255,.5), 0 2px 6px rgba(0,70,110,.6);
151
+ }
152
+ /* glassy panels */
153
+ #chat-col, #preview-col {
154
+ background: rgba(255,255,255,0.30) !important;
155
+ border: 1px solid rgba(255,255,255,0.75) !important;
156
+ border-radius: 20px !important;
157
+ box-shadow: 0 10px 34px rgba(0,60,90,0.30), inset 0 1px 0 rgba(255,255,255,0.95) !important;
158
+ backdrop-filter: blur(14px) saturate(170%);
159
+ -webkit-backdrop-filter: blur(14px) saturate(170%);
160
+ padding: 12px !important;
161
+ }
162
+ /* glossy buttons */
163
+ .aero-btn, button.primary {
164
+ background: linear-gradient(180deg,#c8f99a 0%,#86d943 47%,#5cb52a 53%,#9ae866 100%) !important;
165
+ border: 1px solid #4e9c1f !important;
166
+ border-radius: 13px !important;
167
+ color: #133f08 !important; font-weight: 700 !important;
168
+ box-shadow: inset 0 1px 0 rgba(255,255,255,0.85), 0 3px 9px rgba(0,0,0,0.22) !important;
169
+ text-shadow: 0 1px 0 rgba(255,255,255,0.6) !important;
170
+ }
171
+ .aero-btn:hover, button.primary:hover { filter: brightness(1.07); }
172
+ /* inputs / chatbot glassy */
173
+ #chat-col textarea, #chat-col input {
174
+ background: rgba(255,255,255,0.7) !important;
175
+ border: 1px solid rgba(255,255,255,0.9) !important;
176
+ border-radius: 12px !important;
177
+ box-shadow: inset 0 2px 5px rgba(0,60,90,0.15) !important;
178
+ }
179
+ #chatbox { background: transparent !important; border: none !important; }
180
+ /* live preview frame */
181
+ .aero-frame {
182
+ width:100%; height:78vh; border:none; border-radius:14px; background:#fff;
183
+ box-shadow: inset 0 0 0 1px rgba(255,255,255,.8), 0 6px 18px rgba(0,50,80,.25);
184
+ }
185
+ .aero-welcome {
186
+ height:78vh; display:flex; flex-direction:column; align-items:center; justify-content:center;
187
+ color:#0a3a52; text-align:center; border-radius:14px;
188
+ background: linear-gradient(180deg, rgba(255,255,255,.55), rgba(255,255,255,.25));
189
+ box-shadow: inset 0 1px 0 rgba(255,255,255,.9);
190
+ }
191
+ .aero-welcome h2 { text-shadow: 0 1px 0 rgba(255,255,255,.7); }
192
+ """
193
+
194
 
195
+ # --- UI ---------------------------------------------------------------------
196
+ with gr.Blocks(css=AERO_CSS, theme=gr.themes.Soft(), title="Discode") as demo:
197
+ chat_visible = gr.State(True)
198
+ current_html = gr.State("")
199
 
200
+ gr.Markdown("# 🏃 Discode", elem_id="aero-title")
 
 
201
 
202
+ with gr.Row():
203
+ # Left: slim chat rail
204
+ with gr.Column(scale=2, min_width=280, elem_id="chat-col") as chat_col:
205
+ chatbot = gr.Chatbot(
206
+ type="messages",
207
+ height="62vh",
208
+ elem_id="chatbox",
209
+ show_label=False,
210
+ avatar_images=(None, None),
211
+ )
212
+ prompt = gr.Textbox(
213
+ placeholder="Build me a neon snake game…",
214
+ show_label=False,
215
+ lines=2,
216
+ )
217
+ send = gr.Button("Send ✨", variant="primary", elem_classes=["aero-btn"])
218
+
219
+ # Right: big live preview
220
+ with gr.Column(scale=7, elem_id="preview-col"):
221
+ with gr.Row():
222
+ toggle = gr.Button("◀ Hide chat", elem_classes=["aero-btn"], scale=0)
223
+ preview = gr.HTML(WELCOME)
224
+
225
+ # wiring
226
+ send.click(on_send, [prompt, chatbot, current_html], [chatbot, preview, current_html, prompt])
227
+ prompt.submit(on_send, [prompt, chatbot, current_html], [chatbot, preview, current_html, prompt])
228
+ toggle.click(toggle_chat, [chat_visible], [chat_col, toggle, chat_visible])
229
 
230
  if __name__ == "__main__":
231
  demo.launch()