alexrs commited on
Commit
adcd7fa
·
0 Parent(s):

Initial commit

Browse files
Files changed (7) hide show
  1. .gitattributes +36 -0
  2. .gitignore +4 -0
  3. README.md +30 -0
  4. app.py +1227 -0
  5. pyproject.toml +10 -0
  6. requirements.txt +3 -0
  7. uv.lock +0 -0
.gitattributes ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+
.gitignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ .env
README.md ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: North Mini Code Demo
3
+ emoji: 💻
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: gradio
7
+ sdk_version: 6.14.0
8
+ python_version: '3.13'
9
+ app_file: app.py
10
+ pinned: false
11
+ ---
12
+
13
+ ## North Mini Code Demo
14
+
15
+ A Gradio Space that streams code from Cohere's [North-Mini-Code-1.0](https://huggingface.co/CohereLabs/North-Mini-Code-1.0) model, executes the generated Python in a constrained subprocess, and renders generated HTML/CSS/JavaScript in a sandboxed browser iframe.
16
+
17
+ North-Mini-Code-1.0 is built for agentic coding and works best in your terminal with [OpenCode](https://opencode.ai).
18
+
19
+ ## Configuration
20
+
21
+ Set `COHERE_API_KEY` as a Hugging Face Space secret.
22
+
23
+ ## Local Run
24
+
25
+ ```bash
26
+ export COHERE_API_KEY=...
27
+ uv run python app.py
28
+ ```
29
+
30
+ `requirements.txt` is kept for Hugging Face Spaces; `pyproject.toml` is for local uv runs.
app.py ADDED
@@ -0,0 +1,1227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Gradio Space that showcases execution-grounded code generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import html
6
+ import logging
7
+ import os
8
+ import re
9
+ import subprocess
10
+ import sys
11
+ import tempfile
12
+ import textwrap
13
+ from collections.abc import Iterator
14
+ from dataclasses import dataclass
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ import gradio as gr
19
+ from cohere import ClientV2
20
+ from cohere.core.api_error import ApiError
21
+
22
+ APP_TITLE = "North Mini Code 1.0 Demo"
23
+ CLIENT_NAME = "hf-space-north-mini-code"
24
+ MODEL_ID = "north-mini-code-1-0"
25
+ MODEL_URL = "https://huggingface.co/CohereLabs/North-Mini-Code-1.0"
26
+ OPENCODE_URL = "https://opencode.ai"
27
+
28
+ DEFAULT_TEMPERATURE = 0.2
29
+ PY_TIMEOUT_S = 12
30
+ PY_MEM_LIMIT_MB = 1024
31
+ MAX_STDIO_CHARS = 16_000
32
+ OUTPUT_PNG = "output.png"
33
+
34
+ THINKING_BLOCK_RE = re.compile(r"<\s*think\s*>.*?<\s*/\s*think\s*>", re.IGNORECASE | re.DOTALL)
35
+ CODE_BLOCK_RE = re.compile(r"```([a-zA-Z0-9_+.#-]*)\s*\n(.*?)```", re.DOTALL)
36
+
37
+ logger = logging.getLogger(__name__)
38
+ logging.basicConfig(level=logging.INFO)
39
+
40
+
41
+ SYSTEM_PROMPT = """You are a coding model in a demo where generated code can be run.
42
+
43
+ Only respond to coding-related requests: code generation, debugging, code review,
44
+ software design, developer tooling, programming concepts, or reasoning about code.
45
+ If the user asks for something unrelated to coding, briefly say you can only help
46
+ with coding-related requests.
47
+
48
+ If the user asks a coding question that does not require runnable code, answer it
49
+ directly and do not force a code block.
50
+ If the user asks you to generate, modify, or fix runnable code, return exactly one
51
+ fenced code block and no extra prose. Use a correct language tag: ```python for
52
+ Python, or ```html for Web.
53
+
54
+ For Python, prefer standard library or common packages such as matplotlib.
55
+ For Python, do not use network calls, subprocesses, shell commands, or long-running loops.
56
+ For Web, return a single self-contained HTML document with any CSS and JavaScript inline.
57
+ For Web, make the page fully responsive so it fills the area it is given: set html and body
58
+ to margin:0 and 100% width/height, prefer relative sizes (100%, 100vw/100vh, flexbox) over
59
+ fixed pixel dimensions, and size any <canvas> to its container and re-fit it on window resize
60
+ so nothing is clipped or scrolled.
61
+ """
62
+
63
+ # Curated starter prompts. Each entry is (chip label, prompt, target language).
64
+ EXAMPLE_PROMPTS: list[tuple[str, str, str]] = [
65
+ (
66
+ "🌀 Spiral plot",
67
+ "Create a Python script that plots a colorful spiral with matplotlib, prints a short "
68
+ "description of what it drew, and does not require any external files.",
69
+ "Python",
70
+ ),
71
+ (
72
+ "📊 Sine waves",
73
+ "Plot three sine waves with different frequencies and amplitudes on one matplotlib "
74
+ "figure with a legend and grid, and print the equation of each wave.",
75
+ "Python",
76
+ ),
77
+ (
78
+ "✨ Particles",
79
+ "Create a self-contained HTML/CSS/JavaScript demo with an animated particle field "
80
+ "that reacts to the mouse and includes a small title.",
81
+ "Web",
82
+ ),
83
+ (
84
+ "🖌️ Blackboard",
85
+ "Create a self-contained HTML/CSS/JavaScript blackboard drawing app: a dark chalkboard "
86
+ "canvas you can draw on with the mouse (and touch), a small palette of chalk colors, an "
87
+ "adjustable brush size, and a button to clear the board.",
88
+ "Web",
89
+ ),
90
+ (
91
+ "🎲 Monte Carlo π",
92
+ "Estimate π with a Monte Carlo simulation: sample random points in a unit square, count "
93
+ "how many fall inside the quarter circle, and print the estimate, the true value, and the "
94
+ "error. Plot the sampled points with matplotlib, colored by whether they land inside the "
95
+ "circle.",
96
+ "Python",
97
+ ),
98
+ (
99
+ "✅ Todo app",
100
+ "Create a self-contained HTML/CSS/JavaScript todo app: add tasks, mark them complete, "
101
+ "delete them, filter by all/active/completed, and show a live count of remaining tasks, "
102
+ "with a clean, modern, responsive UI.",
103
+ "Web",
104
+ ),
105
+ ]
106
+
107
+
108
+ @dataclass
109
+ class PythonExecutionResult:
110
+ stdout: str
111
+ stderr: str
112
+ image_path: str | None
113
+ returncode: int | None
114
+ timed_out: bool = False
115
+
116
+
117
+ def _disable_parent_proc_inspection() -> None:
118
+ """Best-effort hardening against same-UID reads of the parent process env.
119
+
120
+ The subprocess receives a scrubbed env, but Linux /proc can sometimes expose a
121
+ same-user process's environment. Marking the Gradio process non-dumpable helps
122
+ prevent generated code from reading `/proc/<parent>/environ`.
123
+ """
124
+
125
+ if sys.platform != "linux":
126
+ return
127
+ try:
128
+ import ctypes
129
+
130
+ pr_set_dumpable = 4
131
+ libc = ctypes.CDLL(None)
132
+ libc.prctl(pr_set_dumpable, 0, 0, 0, 0)
133
+ except Exception:
134
+ logger.warning("Could not disable parent /proc inspection", exc_info=True)
135
+
136
+
137
+ def _build_client(api_key: str) -> ClientV2 | None:
138
+ if api_key:
139
+ return ClientV2(api_key=api_key, client_name=CLIENT_NAME)
140
+ else:
141
+ logger.warning("COHERE_API_KEY is not set; inference is disabled until configured.")
142
+ return None
143
+
144
+
145
+ _raw_api_key = os.getenv("COHERE_API_KEY", "").strip()
146
+ API_KEY_CONFIGURED = bool(_raw_api_key)
147
+ CLIENT = _build_client(_raw_api_key)
148
+
149
+ # Do not keep the secret in the parent environment longer than needed. The Cohere
150
+ # client has already been constructed, and subprocesses are passed explicit envs.
151
+ if _raw_api_key:
152
+ os.environ.pop("COHERE_API_KEY", None)
153
+ _disable_parent_proc_inspection()
154
+ _raw_api_key = ""
155
+
156
+
157
+ def _extract_content_parts(content: object) -> tuple[str, str]:
158
+ """Extract visible text and reasoning text from Cohere content shapes."""
159
+
160
+ if content is None:
161
+ return "", ""
162
+ if isinstance(content, str):
163
+ return content, ""
164
+ if isinstance(content, list):
165
+ parts = [_extract_content_parts(block) for block in content]
166
+ return "".join(text for text, _ in parts), "".join(thinking for _, thinking in parts)
167
+ if isinstance(content, dict):
168
+ text = str(content.get("text") or "")
169
+ thinking = str(content.get("thinking") or "")
170
+ if not text and not thinking and "content" in content:
171
+ return _extract_content_parts(content.get("content"))
172
+ return text, thinking
173
+
174
+ text = getattr(content, "text", None)
175
+ thinking = getattr(content, "thinking", None)
176
+ return (str(text) if text is not None else ""), (str(thinking) if thinking is not None else "")
177
+
178
+
179
+ def _strip_thinking_blocks(text: str) -> str:
180
+ return THINKING_BLOCK_RE.sub("", text).strip()
181
+
182
+
183
+ def _format_response(output: str, thinking: str) -> str:
184
+ thinking = thinking.strip()
185
+ if not thinking:
186
+ return output
187
+ if not output:
188
+ return f"<think>{thinking}</think>"
189
+ return f"<think>{thinking}</think>\n\n{output}"
190
+
191
+
192
+ def _no_output_note(finish_reason: str) -> str:
193
+ if finish_reason == "MAX_TOKENS":
194
+ return "_The model hit its output-token cap before producing visible code._"
195
+ if finish_reason == "ERROR":
196
+ return "_The model returned an error before producing code. Please try again._"
197
+ return f"_The model finished without visible output (finish_reason={finish_reason})._"
198
+
199
+
200
+ def _format_api_error(exc: ApiError) -> str:
201
+ body = exc.body
202
+ if isinstance(body, dict):
203
+ message = body.get("message") or body.get("error") or ""
204
+ body_text = str(message) if message else str(body)
205
+ else:
206
+ body_text = str(body or "").strip()
207
+
208
+ if exc.status_code == 404:
209
+ return f"Model `{MODEL_ID}` was not found on the configured Cohere endpoint."
210
+ if exc.status_code in (401, 403):
211
+ return "The `COHERE_API_KEY` secret was rejected. Check the Space secret."
212
+ if exc.status_code == 429:
213
+ return "The Cohere API rate limit was reached. Please wait and try again."
214
+ return body_text[:240] or f"HTTP {exc.status_code}"
215
+
216
+
217
+ def call_model(messages: list[dict[str, Any]]) -> Iterator[str]:
218
+ """Stream cumulative model text.
219
+
220
+ All Cohere-specific details are intentionally isolated here: model name,
221
+ client method, streaming event shape, and reasoning handling.
222
+ """
223
+
224
+ if CLIENT is None:
225
+ if not API_KEY_CONFIGURED:
226
+ yield "This Space needs a `COHERE_API_KEY` secret before it can call Cohere."
227
+ else:
228
+ yield "Cohere client is not configured."
229
+ return
230
+
231
+ output = ""
232
+ thinking_output = ""
233
+ finish_reason: str | None = None
234
+ event_counts: dict[str, int] = {}
235
+
236
+ try:
237
+ stream = CLIENT.chat_stream(
238
+ model=MODEL_ID,
239
+ messages=messages,
240
+ temperature=DEFAULT_TEMPERATURE,
241
+ thinking={"type": "enabled"},
242
+ )
243
+ for event in stream:
244
+ event_type = getattr(event, "type", None) or "unknown"
245
+ event_counts[event_type] = event_counts.get(event_type, 0) + 1
246
+ delta = getattr(event, "delta", None)
247
+
248
+ if event_type in ("content-delta", "content-start"):
249
+ msg = getattr(delta, "message", None) if delta is not None else None
250
+ if msg is None:
251
+ continue
252
+ text, thinking = _extract_content_parts(getattr(msg, "content", None))
253
+ if thinking:
254
+ thinking_output += thinking
255
+ yield _format_response(output, thinking_output)
256
+ if text:
257
+ output += text
258
+ yield _format_response(output, thinking_output)
259
+ elif event_type == "message-end":
260
+ finish_reason = getattr(delta, "finish_reason", None)
261
+ if finish_reason is None and isinstance(delta, dict):
262
+ finish_reason = delta.get("finish_reason")
263
+
264
+ logger.info(
265
+ "Cohere stream ended: finish_reason=%s, output_len=%d, thinking_len=%d, events=%s",
266
+ finish_reason,
267
+ len(output),
268
+ len(thinking_output),
269
+ event_counts,
270
+ )
271
+ if not output:
272
+ yield _format_response(_no_output_note((finish_reason or "unknown").upper()), thinking_output)
273
+ except ApiError as exc:
274
+ logger.exception("Cohere API error (status=%s)", exc.status_code)
275
+ yield _format_response(f"_Cohere API error_: {_format_api_error(exc)}", thinking_output)
276
+ except Exception as exc:
277
+ logger.exception("Unexpected error calling Cohere API")
278
+ yield _format_response(f"_Unexpected error calling Cohere_: {exc}", thinking_output)
279
+
280
+
281
+ def _chat_history_to_messages(history: list[dict[str, str]]) -> list[dict[str, Any]]:
282
+ messages: list[dict[str, Any]] = [{"role": "system", "content": SYSTEM_PROMPT}]
283
+ for item in history:
284
+ role = item.get("role")
285
+ content = str(item.get("content") or "").strip()
286
+ if role not in {"user", "assistant"} or not content:
287
+ continue
288
+ if role == "assistant":
289
+ content = _strip_thinking_blocks(content)
290
+ messages.append({"role": role, "content": content})
291
+ return messages
292
+
293
+
294
+ def _clip_context(text: str, limit: int = 6_000) -> str:
295
+ if len(text) <= limit:
296
+ return text
297
+ return text[:limit] + f"\n... truncated {len(text) - limit} characters ..."
298
+
299
+
300
+ def _iteration_context(execution_context: dict[str, Any] | None) -> str:
301
+ if not execution_context or not execution_context.get("code"):
302
+ return ""
303
+
304
+ code = _clip_context(str(execution_context.get("code") or ""), 8_000)
305
+ target = str(execution_context.get("target") or "code")
306
+ fence_lang = str(execution_context.get("fence_lang") or target)
307
+ status = str(execution_context.get("status") or "")
308
+ stdout = _clip_context(str(execution_context.get("stdout") or ""), 2_000)
309
+ stderr = _clip_context(str(execution_context.get("stderr") or ""), 2_000)
310
+
311
+ parts = [
312
+ "Previous generated code and run result are available for iteration.",
313
+ f"Previous target: {target}",
314
+ f"Previous status: {status}",
315
+ f"Previous code:\n```{fence_lang}\n{code}\n```",
316
+ ]
317
+ if stdout:
318
+ parts.append(f"Previous stdout:\n{stdout}")
319
+ if stderr:
320
+ parts.append(f"Previous stderr / traceback:\n{stderr}")
321
+ parts.append("If the user asks to revise, debug, extend, or explain the prior code, use this context.")
322
+ return "\n\n".join(parts)
323
+
324
+
325
+ def _targeted_prompt(
326
+ prompt: str,
327
+ target_language: str,
328
+ execution_context: dict[str, Any] | None = None,
329
+ ) -> str:
330
+ target = "Python" if target_language == "Python" else "Web"
331
+ iteration_context = _iteration_context(execution_context)
332
+ context_block = f"\n\n{iteration_context}" if iteration_context else ""
333
+ if target == "Python":
334
+ return (
335
+ "Target: Python. Stay within coding-related requests only. "
336
+ "If the user asks a coding question or wants reasoning that does not require running code, "
337
+ "answer directly without a fenced block. If they ask to generate, revise, or fix runnable "
338
+ "code, return one ```python fenced block only. The code will be executed in a short-lived "
339
+ "subprocess."
340
+ f"{context_block}\n\n"
341
+ f"User request:\n{prompt}"
342
+ )
343
+ return (
344
+ "Target: Web. Stay within coding-related requests only. "
345
+ "If the user asks a coding question or wants reasoning that does not require running code, "
346
+ "answer directly without a fenced block. If they ask to generate, revise, or fix runnable "
347
+ "web code, return one ```html fenced block only. The code is rendered inside a sandboxed "
348
+ "iframe that spans the full width of the preview panel and is about 680px tall, so design "
349
+ "the page to fill that iframe responsively: html/body at margin:0 and 100% width/height, "
350
+ "avoid fixed widths larger than the iframe, and resize any <canvas> to its container "
351
+ "(including on window resize) so the whole app is visible without horizontal scrolling."
352
+ f"{context_block}\n\n"
353
+ f"User request:\n{prompt}"
354
+ )
355
+
356
+
357
+ def extract_code(response: str) -> tuple[str, str | None]:
358
+ """Return the first fenced code block and its language tag."""
359
+
360
+ visible_response = _strip_thinking_blocks(response)
361
+ match = CODE_BLOCK_RE.search(visible_response)
362
+ if not match:
363
+ return "", None
364
+ return match.group(2).strip(), (match.group(1).strip().lower() or None)
365
+
366
+
367
+ def _normalize_language(target_language: str | None, fence_lang: str | None) -> str:
368
+ if fence_lang in {"python", "py"}:
369
+ return "python"
370
+ if fence_lang in {"html", "web", "javascript", "js", "css"}:
371
+ return "web"
372
+ if target_language in {"Python", "Web"}:
373
+ return target_language.lower()
374
+ return "python"
375
+
376
+
377
+ def _truncate_output(text: str) -> str:
378
+ if len(text) <= MAX_STDIO_CHARS:
379
+ return text
380
+ remaining = len(text) - MAX_STDIO_CHARS
381
+ return text[:MAX_STDIO_CHARS] + f"\n\n... truncated {remaining} characters ..."
382
+
383
+
384
+ def _decode_timeout_output(value: str | bytes | None) -> str:
385
+ if value is None:
386
+ return ""
387
+ if isinstance(value, bytes):
388
+ return value.decode("utf-8", errors="replace")
389
+ return value
390
+
391
+
392
+ def _apply_subprocess_limits() -> None:
393
+ """Apply child-only CPU and memory caps before Python user code starts."""
394
+
395
+ import resource
396
+
397
+ mem_bytes = PY_MEM_LIMIT_MB * 1024 * 1024
398
+ resource.setrlimit(resource.RLIMIT_AS, (mem_bytes, mem_bytes))
399
+ resource.setrlimit(resource.RLIMIT_CPU, (PY_TIMEOUT_S, PY_TIMEOUT_S))
400
+
401
+
402
+ def _python_runner_source() -> str:
403
+ return textwrap.dedent(
404
+ f"""
405
+ import os
406
+ import runpy
407
+ import sys
408
+ import traceback
409
+
410
+ os.environ.setdefault("MPLBACKEND", "Agg")
411
+ exit_code = 0
412
+ try:
413
+ runpy.run_path(os.path.join(os.getcwd(), "user_code.py"), run_name="__main__")
414
+ except SystemExit as exc:
415
+ code = exc.code
416
+ exit_code = code if isinstance(code, int) else 1
417
+ except Exception:
418
+ traceback.print_exc()
419
+ exit_code = 1
420
+ finally:
421
+ try:
422
+ import matplotlib
423
+
424
+ matplotlib.use("Agg", force=True)
425
+ import matplotlib.pyplot as plt
426
+
427
+ if plt.get_fignums():
428
+ plt.savefig(os.environ["OUTPUT_PNG"], bbox_inches="tight")
429
+ except ModuleNotFoundError as exc:
430
+ if exc.name != "matplotlib":
431
+ traceback.print_exc()
432
+ except Exception:
433
+ traceback.print_exc()
434
+
435
+ raise SystemExit(exit_code)
436
+ """
437
+ ).strip()
438
+
439
+
440
+ def run_python(code: str) -> PythonExecutionResult:
441
+ """Execute generated Python in a subprocess with baseline containment.
442
+
443
+ Security boundary for v1:
444
+ - Scrubbed env: never pass os.environ, so COHERE_API_KEY is absent.
445
+ - Hard timeout: kill code that hangs the Space.
446
+ - Memory/CPU caps: reduce the blast radius of runaway code.
447
+
448
+ Accepted limitation: a standard non-privileged Gradio Space cannot reliably
449
+ block network egress or filesystem reads from this subprocess. Full isolation
450
+ would require a privileged Docker Space with nsjail/gVisor, or an external
451
+ executor such as E2B/Modal. Do not refactor these comments away; they define
452
+ the risk boundary of this demo.
453
+ """
454
+
455
+ with tempfile.TemporaryDirectory(prefix="coding_model_run_") as tmp:
456
+ workdir = Path(tmp)
457
+ runner_path = workdir / "runner.py"
458
+ user_path = workdir / "user_code.py"
459
+ image_path = workdir / OUTPUT_PNG
460
+
461
+ runner_path.write_text(_python_runner_source(), encoding="utf-8")
462
+ user_path.write_text(code, encoding="utf-8")
463
+
464
+ env = {
465
+ # SECURITY: explicit scrubbed env. Never pass parent os.environ, which
466
+ # may contain COHERE_API_KEY or other HF Space secrets.
467
+ "PATH": "/usr/bin:/bin",
468
+ "HOME": str(workdir),
469
+ "TMPDIR": str(workdir),
470
+ "MPLBACKEND": "Agg",
471
+ "MPLCONFIGDIR": str(workdir / ".matplotlib"),
472
+ "OUTPUT_PNG": str(image_path),
473
+ "PYTHONIOENCODING": "utf-8",
474
+ "PYTHONNOUSERSITE": "1",
475
+ "PYTHONUNBUFFERED": "1",
476
+ "LANG": "C.UTF-8",
477
+ # Keep numeric libraries from spawning many workers inside the capped process.
478
+ "OPENBLAS_NUM_THREADS": "1",
479
+ "OMP_NUM_THREADS": "1",
480
+ "MKL_NUM_THREADS": "1",
481
+ "NUMEXPR_NUM_THREADS": "1",
482
+ }
483
+
484
+ try:
485
+ completed = subprocess.run(
486
+ [sys.executable, "-I", str(runner_path)],
487
+ cwd=workdir,
488
+ env=env,
489
+ capture_output=True,
490
+ text=True,
491
+ encoding="utf-8",
492
+ errors="replace",
493
+ timeout=PY_TIMEOUT_S,
494
+ preexec_fn=_apply_subprocess_limits if sys.platform == "linux" else None,
495
+ check=False,
496
+ )
497
+ stdout = _truncate_output(completed.stdout)
498
+ stderr = _truncate_output(completed.stderr)
499
+ if completed.returncode and not stderr:
500
+ stderr = f"Process exited with status {completed.returncode}."
501
+
502
+ saved_image: str | None = None
503
+ if image_path.exists() and image_path.stat().st_size > 0:
504
+ saved = tempfile.NamedTemporaryFile(
505
+ prefix="coding_model_plot_", suffix=".png", delete=False
506
+ )
507
+ saved.close()
508
+ Path(saved.name).write_bytes(image_path.read_bytes())
509
+ saved_image = saved.name
510
+
511
+ return PythonExecutionResult(
512
+ stdout=stdout,
513
+ stderr=stderr,
514
+ image_path=saved_image,
515
+ returncode=completed.returncode,
516
+ )
517
+ except subprocess.TimeoutExpired as exc:
518
+ stdout = _truncate_output(_decode_timeout_output(exc.stdout))
519
+ stderr = _truncate_output(_decode_timeout_output(exc.stderr))
520
+ timeout_note = f"Timed out after {PY_TIMEOUT_S} seconds; the process was killed."
521
+ stderr = f"{stderr}\n{timeout_note}".strip()
522
+ return PythonExecutionResult(
523
+ stdout=stdout,
524
+ stderr=stderr,
525
+ image_path=None,
526
+ returncode=None,
527
+ timed_out=True,
528
+ )
529
+
530
+
531
+ def _web_document(code: str, fence_lang: str | None) -> str:
532
+ lang = (fence_lang or "").lower()
533
+ if lang in {"javascript", "js"}:
534
+ return f"<!doctype html><html><body><script>\n{code}\n</script></body></html>"
535
+ if lang == "css":
536
+ return f"<!doctype html><html><head><style>\n{code}\n</style></head><body></body></html>"
537
+ if re.search(r"<!doctype|<html[\s>]", code, flags=re.IGNORECASE):
538
+ return code
539
+ return f"<!doctype html><html><head><meta charset='utf-8'></head><body>\n{code}\n</body></html>"
540
+
541
+
542
+ def build_iframe(code: str, fence_lang: str | None = None) -> str:
543
+ """Render web code in a sandboxed iframe.
544
+
545
+ SECURITY: sandbox allows scripts so demos can run, but deliberately omits
546
+ allow-same-origin. Without same-origin, generated code cannot share the
547
+ parent origin, cookies, or storage.
548
+ """
549
+
550
+ document = _web_document(code, fence_lang)
551
+ srcdoc = html.escape(document, quote=True)
552
+ return (
553
+ '<iframe class="web-frame" '
554
+ 'sandbox="allow-scripts" '
555
+ 'allow="fullscreen" '
556
+ "allowfullscreen "
557
+ f'srcdoc="{srcdoc}" '
558
+ 'style="width:100%; min-height:680px; border:0; border-radius:14px; '
559
+ 'background:white;"></iframe>'
560
+ )
561
+
562
+
563
+ TAB_PREVIEW = "preview"
564
+ TAB_CONSOLE = "console"
565
+ TAB_CODE = "code"
566
+
567
+
568
+ def _status_badge(text: str, state: str = "idle") -> str:
569
+ spinner = '<span class="status-spinner"></span>' if state == "working" else '<span class="status-dot"></span>'
570
+ return (
571
+ f'<div class="status-badge status-{state}">{spinner}'
572
+ f"<span>{html.escape(text)}</span></div>"
573
+ )
574
+
575
+
576
+ def _preview_placeholder(message: str = "Run code to see the result here.") -> str:
577
+ return (
578
+ '<div class="preview-placeholder">'
579
+ '<div class="pp-emoji">🖼️</div>'
580
+ f"<strong>{html.escape(message)}</strong>"
581
+ "<span>A matplotlib figure or a live web preview will appear in this panel.</span>"
582
+ "</div>"
583
+ )
584
+
585
+
586
+ def _write_download(code: str, language: str) -> str | None:
587
+ if not code:
588
+ return None
589
+ ext = "py" if language == "python" else "html"
590
+ try:
591
+ directory = tempfile.mkdtemp(prefix="coding_model_dl_")
592
+ path = Path(directory) / f"generated.{ext}"
593
+ path.write_text(code, encoding="utf-8")
594
+ return str(path)
595
+ except Exception:
596
+ logger.warning("Could not write downloadable snippet", exc_info=True)
597
+ return None
598
+
599
+
600
+ def _display_kwargs(execution_context: dict[str, Any] | None) -> dict[str, Any]:
601
+ """Map the persisted execution context onto concrete component updates."""
602
+
603
+ base = {
604
+ "show_image": False,
605
+ "image": None,
606
+ "show_web": False,
607
+ "web_html": "",
608
+ "show_placeholder": True,
609
+ "placeholder_msg": "Run code to see the result here.",
610
+ "stdout": "",
611
+ "stderr": "",
612
+ "code": "",
613
+ "code_language": "python",
614
+ "download_path": None,
615
+ "suggested_tab": None,
616
+ }
617
+ if not execution_context or not execution_context.get("code"):
618
+ return base
619
+
620
+ target = str(execution_context.get("target") or "")
621
+ code = str(execution_context.get("code") or "")
622
+ fence_lang = str(execution_context.get("fence_lang") or target)
623
+ download = execution_context.get("download_path")
624
+
625
+ if target == "web":
626
+ base.update(
627
+ show_web=True,
628
+ web_html=build_iframe(code, fence_lang),
629
+ show_placeholder=False,
630
+ code=code,
631
+ code_language="html",
632
+ download_path=download,
633
+ suggested_tab=TAB_PREVIEW,
634
+ )
635
+ return base
636
+
637
+ image = execution_context.get("image_path")
638
+ base.update(
639
+ show_image=bool(image),
640
+ image=image,
641
+ show_placeholder=not bool(image),
642
+ placeholder_msg="No figure produced — open the Console tab for program output.",
643
+ stdout=str(execution_context.get("stdout") or ""),
644
+ stderr=str(execution_context.get("stderr") or ""),
645
+ code=code,
646
+ code_language="python",
647
+ download_path=download,
648
+ suggested_tab=TAB_PREVIEW if image else TAB_CONSOLE,
649
+ )
650
+ return base
651
+
652
+
653
+ def _output_state(
654
+ history: list[dict[str, str]],
655
+ prompt_value: str,
656
+ status_text: str,
657
+ status_state: str,
658
+ execution_context: dict[str, Any] | None,
659
+ *,
660
+ selected_tab: str | None = None,
661
+ display: dict[str, Any] | None = None,
662
+ ) -> tuple[Any, ...]:
663
+ data = display if display is not None else _display_kwargs(execution_context)
664
+ tab_update = gr.update(selected=selected_tab) if selected_tab else gr.update()
665
+ return (
666
+ history,
667
+ prompt_value,
668
+ _status_badge(status_text, status_state),
669
+ tab_update,
670
+ gr.update(value=_preview_placeholder(data["placeholder_msg"]), visible=data["show_placeholder"]),
671
+ gr.update(value=data["image"], visible=data["show_image"]),
672
+ gr.update(value=data["web_html"], visible=data["show_web"]),
673
+ gr.update(visible=data["show_web"]),
674
+ data["stdout"],
675
+ data["stderr"],
676
+ gr.update(value=data["code"], language=data["code_language"]),
677
+ gr.update(value=data["download_path"], visible=bool(data["download_path"])),
678
+ execution_context or {},
679
+ )
680
+
681
+
682
+ def _run_extracted_code(
683
+ code: str,
684
+ target: str,
685
+ ) -> tuple[str, str, str | None, str, str]:
686
+ """Execute code and return (stdout, stderr, image_path, status_text, status_state)."""
687
+
688
+ if target == "python":
689
+ result = run_python(code)
690
+ if result.timed_out:
691
+ return result.stdout, result.stderr, result.image_path, f"Timed out after {PY_TIMEOUT_S}s", "error"
692
+ if result.returncode:
693
+ return result.stdout, result.stderr, result.image_path, "Finished with errors", "error"
694
+ return result.stdout, result.stderr, result.image_path, "Ran successfully", "success"
695
+
696
+ return "", "", None, "Preview ready", "success"
697
+
698
+
699
+ def _updated_execution_context(
700
+ *,
701
+ code: str,
702
+ target: str,
703
+ fence_lang: str | None,
704
+ stdout: str,
705
+ stderr: str,
706
+ image_path: str | None,
707
+ status: str,
708
+ download_path: str | None,
709
+ ) -> dict[str, Any]:
710
+ return {
711
+ "code": code,
712
+ "target": target,
713
+ "fence_lang": fence_lang or target,
714
+ "stdout": stdout,
715
+ "stderr": stderr,
716
+ "image_path": image_path,
717
+ "status": status,
718
+ "download_path": download_path,
719
+ }
720
+
721
+
722
+ def handle_submit(
723
+ prompt: str,
724
+ target_language: str,
725
+ history: list[dict[str, str]] | None,
726
+ execution_context: dict[str, Any] | None,
727
+ ) -> Iterator[tuple[Any, ...]]:
728
+ history = list(history or [])
729
+ execution_context = execution_context or {}
730
+ prompt = (prompt or "").strip()
731
+ if not prompt:
732
+ yield _output_state(history, "", "Enter a prompt to get started.", "info", execution_context)
733
+ return
734
+
735
+ generation_history = history + [
736
+ {"role": "user", "content": prompt},
737
+ {"role": "assistant", "content": ""},
738
+ ]
739
+ assistant_index = len(generation_history) - 1
740
+ yield _output_state(generation_history, "", "Thinking…", "working", execution_context)
741
+
742
+ cohere_history = history + [
743
+ {"role": "user", "content": _targeted_prompt(prompt, target_language, execution_context)}
744
+ ]
745
+ messages = _chat_history_to_messages(cohere_history)
746
+
747
+ final_response = ""
748
+ for partial in call_model(messages):
749
+ final_response = partial
750
+ generation_history[assistant_index]["content"] = partial
751
+ yield _output_state(generation_history, "", "Generating…", "working", execution_context)
752
+
753
+ if not final_response:
754
+ generation_history[assistant_index]["content"] = "The model did not return a response."
755
+ yield _output_state(generation_history, "", "No model response.", "error", execution_context)
756
+ return
757
+
758
+ code, fence_lang = extract_code(final_response)
759
+ target = _normalize_language(target_language, fence_lang)
760
+ if not code:
761
+ yield _output_state(generation_history, "", "Answered without running code.", "info", execution_context)
762
+ return
763
+
764
+ yield _output_state(generation_history, "", "Running…", "working", execution_context)
765
+
766
+ stdout, stderr, image, status_text, status_state = _run_extracted_code(code, target)
767
+ download = _write_download(code, "python" if target == "python" else "html")
768
+ execution_context = _updated_execution_context(
769
+ code=code,
770
+ target=target,
771
+ fence_lang=fence_lang,
772
+ stdout=stdout,
773
+ stderr=stderr,
774
+ image_path=image,
775
+ status=status_text,
776
+ download_path=download,
777
+ )
778
+ data = _display_kwargs(execution_context)
779
+ yield _output_state(
780
+ generation_history,
781
+ "",
782
+ status_text,
783
+ status_state,
784
+ execution_context,
785
+ selected_tab=data["suggested_tab"],
786
+ display=data,
787
+ )
788
+
789
+
790
+ def _make_example_runner(prompt: str, target: str):
791
+ def runner(*_ignored: Any) -> Iterator[tuple[Any, ...]]:
792
+ # Starters always begin a fresh conversation rather than appending to the
793
+ # current thread, so prior history and execution context are discarded.
794
+ yield from handle_submit(prompt, target, [], {})
795
+
796
+ return runner
797
+
798
+
799
+ def start_new_conversation() -> tuple[Any, ...]:
800
+ return _output_state(
801
+ [],
802
+ "",
803
+ "New conversation — ask anything about code.",
804
+ "idle",
805
+ {},
806
+ selected_tab=TAB_PREVIEW,
807
+ )
808
+
809
+
810
+ def _cancelled_status() -> str:
811
+ return _status_badge("Stopped.", "info")
812
+
813
+
814
+ APP_THEME = gr.themes.Soft(
815
+ primary_hue="indigo",
816
+ secondary_hue="violet",
817
+ neutral_hue="slate",
818
+ radius_size=gr.themes.sizes.radius_lg,
819
+ font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"],
820
+ font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "SFMono-Regular", "monospace"],
821
+ ).set(
822
+ body_background_fill="*neutral_50",
823
+ body_background_fill_dark="*neutral_950",
824
+ block_shadow="0 1px 2px rgba(16,24,40,.04), 0 10px 28px -18px rgba(16,24,40,.22)",
825
+ button_primary_shadow="0 8px 22px -10px rgba(99,102,241,.7)",
826
+ )
827
+
828
+ CUSTOM_CSS = """
829
+ .gradio-container { max-width: 1720px !important; margin: 0 auto !important; padding-left: 1.25rem !important; padding-right: 1.25rem !important; }
830
+
831
+ /* ---------- Hero ---------- */
832
+ .hero { padding: 1.2rem 0 0.2rem; }
833
+ .hero-row { display: flex; align-items: center; gap: 0.95rem; flex-wrap: wrap; }
834
+ .hero-mark {
835
+ width: 54px; height: 54px; border-radius: 16px; flex: none;
836
+ background: linear-gradient(135deg, #3b82f6, #6366f1 45%, #8b5cf6);
837
+ display: flex; align-items: center; justify-content: center;
838
+ font-size: 1.75rem; box-shadow: 0 10px 26px -10px rgba(99,102,241,.7);
839
+ }
840
+ .hero h1 {
841
+ font-size: 1.95rem; font-weight: 800; line-height: 1.1; margin: 0;
842
+ letter-spacing: -0.025em;
843
+ background: linear-gradient(110deg, #2563eb, #7c3aed 55%, #9333ea);
844
+ -webkit-background-clip: text; background-clip: text; color: transparent;
845
+ }
846
+ .hero .subtitle { margin: 0.28rem 0 0; color: var(--body-text-color-subdued); font-size: 1.02rem; }
847
+ .hero-tip {
848
+ margin: 0.7rem 0 0; padding: 0.5rem 0.8rem; font-size: 0.86rem; line-height: 1.45;
849
+ display: flex; align-items: baseline; gap: 0.45rem;
850
+ border: 1px solid var(--border-color-primary); border-left: 3px solid #7c3aed;
851
+ border-radius: 12px; background: var(--block-background-fill);
852
+ color: var(--body-text-color-subdued);
853
+ }
854
+ .hero-tip a { color: #6d28d9; font-weight: 600; text-decoration: none; white-space: nowrap; }
855
+ .hero-tip a:hover { text-decoration: underline; }
856
+ .dark .hero-tip a { color: #c4b5fd; }
857
+ .hero-spacer { flex: 1 1 auto; min-width: 1rem; }
858
+ .hero-pills { display: flex; flex-wrap: wrap; gap: 0.4rem; justify-content: flex-end; }
859
+ .brand-pill {
860
+ display: inline-flex; align-items: center; gap: 0.4rem;
861
+ font-size: 0.78rem; font-weight: 600; padding: 0.34rem 0.7rem;
862
+ border-radius: 999px; border: 1px solid var(--border-color-primary);
863
+ background: var(--block-background-fill); color: var(--body-text-color-subdued);
864
+ white-space: nowrap; text-decoration: none;
865
+ transition: transform .12s ease, box-shadow .12s ease, border-color .12s ease, color .12s ease;
866
+ }
867
+ a.brand-pill:hover {
868
+ transform: translateY(-1px);
869
+ border-color: #8b5cf6; color: var(--body-text-color);
870
+ box-shadow: 0 8px 18px -12px rgba(124,58,237,.75);
871
+ }
872
+ .brand-pill .pill-ext { opacity: 0.55; font-size: 0.72rem; }
873
+ .brand-pill.accent {
874
+ border-color: transparent; color: #fff;
875
+ background: linear-gradient(110deg, #4f46e5, #7c3aed);
876
+ }
877
+
878
+ /* ---------- Config alert ---------- */
879
+ .config-alert {
880
+ border: 1px solid #f4c7c7; background: #fdeeee; color: #8a1f1f;
881
+ border-radius: 14px; padding: 0.75rem 0.95rem; font-size: 0.9rem; margin: 0.55rem 0 0;
882
+ line-height: 1.45;
883
+ }
884
+ .config-alert code { background: rgba(0,0,0,.07); padding: 0.05rem 0.32rem; border-radius: 6px; }
885
+ .dark .config-alert { background: rgba(180,35,24,.13); border-color: rgba(180,35,24,.42); color: #f3b4b4; }
886
+ .dark .config-alert code { background: rgba(255,255,255,.1); }
887
+
888
+ /* ---------- Panels ---------- */
889
+ .panel-card {
890
+ border: 1px solid var(--border-color-primary);
891
+ border-radius: 20px; padding: 1.05rem 1.1rem 1.15rem;
892
+ background: var(--block-background-fill);
893
+ }
894
+ .panel-head {
895
+ display: flex; align-items: center; justify-content: space-between;
896
+ gap: 0.6rem; margin-bottom: 0.7rem; min-height: 34px;
897
+ }
898
+ .panel-title { font-weight: 700; font-size: 1.05rem; display: flex; align-items: center; gap: 0.45rem; margin: 0; }
899
+ .workbench-status { display: flex; justify-content: flex-end; }
900
+
901
+ /* ---------- Status badge ---------- */
902
+ .status-badge {
903
+ display: inline-flex; align-items: center; gap: 0.5rem;
904
+ font-size: 0.85rem; font-weight: 600; padding: 0.4rem 0.8rem;
905
+ border-radius: 999px; border: 1px solid var(--border-color-primary);
906
+ background: var(--block-background-fill); white-space: nowrap;
907
+ }
908
+ .status-dot { width: 9px; height: 9px; border-radius: 50%; background: #94a3b8; flex: none; }
909
+ .status-info .status-dot { background: #3b82f6; }
910
+ .status-success .status-dot { background: #22c55e; }
911
+ .status-error .status-dot { background: #ef4444; }
912
+ .status-error { border-color: rgba(239,68,68,.45); }
913
+ .status-success { border-color: rgba(34,197,94,.45); }
914
+ .status-working { border-color: rgba(99,102,241,.5); color: var(--body-text-color); }
915
+ .status-spinner {
916
+ width: 12px; height: 12px; border-radius: 50%; flex: none;
917
+ border: 2px solid rgba(99,102,241,.3); border-top-color: #6366f1;
918
+ animation: spin 0.7s linear infinite;
919
+ }
920
+ @keyframes spin { to { transform: rotate(360deg); } }
921
+
922
+ /* ---------- Examples ---------- */
923
+ .examples-label { font-size: 0.78rem; font-weight: 600; color: var(--body-text-color-subdued);
924
+ text-transform: uppercase; letter-spacing: 0.05em; margin: 0.45rem 0 0.15rem; }
925
+ .examples-wrap { flex-wrap: wrap !important; gap: 0.5rem !important; }
926
+ button.example-chip {
927
+ flex: 0 0 auto !important;
928
+ border-radius: 999px !important;
929
+ font-size: 0.83rem !important; font-weight: 600 !important;
930
+ padding: 0.45rem 0.95rem !important;
931
+ white-space: nowrap !important;
932
+ width: auto !important; min-width: 0 !important;
933
+ background: var(--block-background-fill) !important;
934
+ border: 1px solid var(--border-color-primary) !important;
935
+ transition: transform .12s ease, box-shadow .12s ease, border-color .12s ease;
936
+ }
937
+ button.example-chip:hover {
938
+ transform: translateY(-1px);
939
+ border-color: #8b5cf6 !important;
940
+ box-shadow: 0 8px 18px -12px rgba(124,58,237,.75);
941
+ }
942
+
943
+ /* ---------- Inputs ---------- */
944
+ .send-btn button { font-weight: 700 !important; }
945
+ .input-hint { font-size: 0.78rem; color: var(--body-text-color-subdued); margin: 0.2rem 0 0; }
946
+
947
+ /* ---------- Target toggle ---------- */
948
+ .target-toggle { background: transparent !important; border: none !important; box-shadow: none !important; padding: 0 !important; }
949
+
950
+ /* ---------- Console ---------- */
951
+ .console-box textarea {
952
+ font-family: var(--font-mono) !important;
953
+ font-size: 0.82rem !important; line-height: 1.5 !important;
954
+ }
955
+ .stderr-box textarea { color: #b42318 !important; }
956
+ .dark .stderr-box textarea { color: #fca5a5 !important; }
957
+
958
+ /* ---------- Preview ---------- */
959
+ .preview-placeholder {
960
+ min-height: 560px;
961
+ border: 1.5px dashed var(--border-color-primary);
962
+ border-radius: 16px;
963
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
964
+ gap: 0.45rem; color: var(--body-text-color-subdued); text-align: center; padding: 2rem;
965
+ }
966
+ .preview-placeholder .pp-emoji { font-size: 2.3rem; opacity: 0.85; }
967
+ .preview-placeholder strong { font-size: 1.02rem; color: var(--body-text-color); }
968
+ .web-frame { box-shadow: 0 8px 30px -18px rgba(16,24,40,.5); }
969
+ .web-frame:fullscreen { border-radius: 0 !important; box-shadow: none !important; min-height: 100vh !important; background: #fff; }
970
+ .preview-toolbar { justify-content: flex-end !important; margin-bottom: 0.5rem; }
971
+ .fullscreen-btn button {
972
+ border-radius: 999px !important;
973
+ font-weight: 600 !important;
974
+ white-space: nowrap !important;
975
+ }
976
+
977
+ /* ---------- Footer note ---------- */
978
+ .safety-note {
979
+ font-size: 0.78rem; color: var(--body-text-color-subdued);
980
+ margin: 0.7rem 0 0; line-height: 1.45; display: flex; align-items: center; gap: 0.4rem;
981
+ }
982
+
983
+ /* ---------- Responsive ---------- */
984
+ @media (max-width: 1024px) {
985
+ .hero h1 { font-size: 1.6rem; }
986
+ }
987
+ """
988
+
989
+
990
+ def _configuration_banner_html() -> str:
991
+ messages: list[str] = []
992
+ if not API_KEY_CONFIGURED:
993
+ messages.append("Set <code>COHERE_API_KEY</code> as a Hugging Face Space secret.")
994
+ if not messages:
995
+ return ""
996
+ items = " ".join(messages)
997
+ return (
998
+ '<div class="config-alert"><strong>⚙️ Configuration required.</strong> '
999
+ f"{items} Generation stays disabled until this is done.</div>"
1000
+ )
1001
+
1002
+
1003
+ def _hero_html() -> str:
1004
+ model_pill = (
1005
+ f'<a class="brand-pill" href="{html.escape(MODEL_URL, quote=True)}" '
1006
+ 'target="_blank" rel="noopener noreferrer" '
1007
+ f'title="View {html.escape(MODEL_ID)} on Hugging Face">'
1008
+ f'🧠 {html.escape(MODEL_ID)} <span class="pill-ext">↗</span></a>'
1009
+ )
1010
+ opencode_url = html.escape(OPENCODE_URL, quote=True)
1011
+ return f"""
1012
+ <section class="hero">
1013
+ <div class="hero-row">
1014
+ <div class="hero-mark">💻</div>
1015
+ <div>
1016
+ <h1>{html.escape(APP_TITLE)}</h1>
1017
+ <p class="subtitle">Chat with a coding model. Run Python or Web code.</p>
1018
+ </div>
1019
+ <div class="hero-spacer"></div>
1020
+ <div class="hero-pills">
1021
+ {model_pill}
1022
+ </div>
1023
+ </div>
1024
+ <p class="hero-tip">⌨️ <span><strong>North-Mini-Code-1.0</strong> is built for agentic coding and works best
1025
+ in your terminal with <a href="{opencode_url}" target="_blank" rel="noopener noreferrer">OpenCode ↗</a>.
1026
+ This Space is a browser playground for trying the model.</span></p>
1027
+ </section>
1028
+ {_configuration_banner_html()}
1029
+ """
1030
+
1031
+
1032
+ def build_demo() -> gr.Blocks:
1033
+ with gr.Blocks(title=APP_TITLE, fill_height=True) as demo:
1034
+ gr.HTML(_hero_html())
1035
+
1036
+ execution_state = gr.State({})
1037
+
1038
+ with gr.Row(equal_height=False):
1039
+ # ----- Left: conversation -----
1040
+ with gr.Column(scale=6, min_width=420, elem_classes=["panel-card"]):
1041
+ with gr.Row(elem_classes=["panel-head"]):
1042
+ gr.HTML('<div class="panel-title">💬 Conversation</div>')
1043
+ new_conversation_btn = gr.Button(
1044
+ "+ New chat", variant="secondary", size="sm", scale=0, min_width=120
1045
+ )
1046
+
1047
+ chatbot = gr.Chatbot(
1048
+ label=None,
1049
+ show_label=False,
1050
+ height=440,
1051
+ layout="bubble",
1052
+ reasoning_tags=[("<think>", "</think>")],
1053
+ placeholder=(
1054
+ "### 👋 Ask me to write code\n"
1055
+ "I can build runnable **Python** scripts and self-contained **web** demos.\n\n"
1056
+ "Pick a starter below, or describe what you want to build."
1057
+ ),
1058
+ )
1059
+
1060
+ gr.HTML('<p class="examples-label">Run generated code as</p>')
1061
+ target_language = gr.Radio(
1062
+ choices=["Python", "Web"],
1063
+ value="Python",
1064
+ show_label=False,
1065
+ container=False,
1066
+ interactive=True,
1067
+ elem_classes=["target-toggle"],
1068
+ )
1069
+
1070
+ prompt = gr.Textbox(
1071
+ label=None,
1072
+ show_label=False,
1073
+ placeholder="Ask a coding question, request code, or iterate on the previous result…",
1074
+ lines=3,
1075
+ max_lines=10,
1076
+ submit_btn=False,
1077
+ autofocus=True,
1078
+ )
1079
+ with gr.Row():
1080
+ submit_btn = gr.Button("Send ➤", variant="primary", scale=4, elem_classes=["send-btn"])
1081
+ stop_btn = gr.Button("Stop", variant="stop", scale=1, min_width=90)
1082
+ gr.HTML('<p class="input-hint">Press <strong>Shift+Enter</strong> to send · <strong>Enter</strong> for a new line</p>')
1083
+
1084
+ gr.HTML('<p class="examples-label">Try a starter</p>')
1085
+ example_buttons: list[tuple[gr.Button, str, str]] = []
1086
+ with gr.Row(elem_classes=["examples-wrap"]):
1087
+ for label, ex_prompt, ex_target in EXAMPLE_PROMPTS:
1088
+ btn = gr.Button(label, size="sm", scale=0, elem_classes=["example-chip"])
1089
+ example_buttons.append((btn, ex_prompt, ex_target))
1090
+
1091
+ # ----- Right: workbench -----
1092
+ with gr.Column(scale=6, min_width=420, elem_classes=["panel-card"]):
1093
+ with gr.Row(elem_classes=["panel-head"]):
1094
+ gr.HTML('<div class="panel-title">🛠️ Workbench</div>')
1095
+ status = gr.HTML(_status_badge("Idle.", "idle"), elem_classes=["workbench-status"])
1096
+
1097
+ with gr.Tabs() as result_tabs:
1098
+ with gr.Tab("Preview", id=TAB_PREVIEW):
1099
+ preview_placeholder = gr.HTML(_preview_placeholder())
1100
+ py_image = gr.Image(
1101
+ label="Matplotlib figure",
1102
+ type="filepath",
1103
+ visible=False,
1104
+ interactive=False,
1105
+ show_label=False,
1106
+ )
1107
+ with gr.Row(elem_classes=["preview-toolbar"], visible=False) as preview_toolbar:
1108
+ fullscreen_btn = gr.Button(
1109
+ "⤢ Fullscreen",
1110
+ size="sm",
1111
+ scale=0,
1112
+ min_width=120,
1113
+ elem_classes=["fullscreen-btn"],
1114
+ )
1115
+ web_preview = gr.HTML(value="", visible=False, min_height=680)
1116
+
1117
+ with gr.Tab("Console", id=TAB_CONSOLE):
1118
+ py_stdout = gr.Textbox(
1119
+ label="Program output (stdout)",
1120
+ lines=11,
1121
+ interactive=False,
1122
+ elem_classes=["console-box"],
1123
+ placeholder="Program output will appear here after a Python run.",
1124
+ )
1125
+ py_stderr = gr.Textbox(
1126
+ label="Errors / stderr",
1127
+ lines=7,
1128
+ interactive=False,
1129
+ elem_classes=["console-box", "stderr-box"],
1130
+ placeholder="Tracebacks and error messages will appear here.",
1131
+ )
1132
+
1133
+ with gr.Tab("Code", id=TAB_CODE):
1134
+ code_view = gr.Code(
1135
+ label="Generated source",
1136
+ language="python",
1137
+ lines=20,
1138
+ interactive=False,
1139
+ wrap_lines=True,
1140
+ )
1141
+ download_btn = gr.DownloadButton(
1142
+ "⬇ Download snippet", size="sm", visible=False
1143
+ )
1144
+
1145
+ outputs = [
1146
+ chatbot,
1147
+ prompt,
1148
+ status,
1149
+ result_tabs,
1150
+ preview_placeholder,
1151
+ py_image,
1152
+ web_preview,
1153
+ preview_toolbar,
1154
+ py_stdout,
1155
+ py_stderr,
1156
+ code_view,
1157
+ download_btn,
1158
+ execution_state,
1159
+ ]
1160
+
1161
+ new_conversation_btn.click(
1162
+ fn=start_new_conversation,
1163
+ inputs=[],
1164
+ outputs=outputs,
1165
+ show_progress="hidden",
1166
+ )
1167
+
1168
+ submit_event = submit_btn.click(
1169
+ fn=handle_submit,
1170
+ inputs=[prompt, target_language, chatbot, execution_state],
1171
+ outputs=outputs,
1172
+ show_progress="minimal",
1173
+ )
1174
+ prompt_event = prompt.submit(
1175
+ fn=handle_submit,
1176
+ inputs=[prompt, target_language, chatbot, execution_state],
1177
+ outputs=outputs,
1178
+ show_progress="minimal",
1179
+ )
1180
+
1181
+ cancellable_events = [submit_event, prompt_event]
1182
+ for btn, ex_prompt, ex_target in example_buttons:
1183
+ set_target = btn.click(
1184
+ fn=lambda t=ex_target: gr.update(value=t),
1185
+ inputs=[],
1186
+ outputs=[target_language],
1187
+ show_progress="hidden",
1188
+ )
1189
+ run_event = set_target.then(
1190
+ fn=_make_example_runner(ex_prompt, ex_target),
1191
+ inputs=[chatbot, execution_state],
1192
+ outputs=outputs,
1193
+ show_progress="minimal",
1194
+ )
1195
+ cancellable_events.append(run_event)
1196
+
1197
+ stop_btn.click(
1198
+ fn=_cancelled_status,
1199
+ inputs=[],
1200
+ outputs=[status],
1201
+ cancels=cancellable_events,
1202
+ show_progress="hidden",
1203
+ )
1204
+
1205
+ # Expand the sandboxed web preview to fill the screen so users can actually
1206
+ # interact with apps larger than the side panel. Runs purely client-side and
1207
+ # keeps the iframe's sandbox attributes intact.
1208
+ fullscreen_btn.click(
1209
+ fn=None,
1210
+ inputs=None,
1211
+ outputs=None,
1212
+ js=(
1213
+ "() => { const f = document.querySelector('.web-frame'); "
1214
+ "if (f) { const r = f.requestFullscreen || f.webkitRequestFullscreen; "
1215
+ "if (r) { try { Promise.resolve(r.call(f)).catch(() => {}); } catch (e) {} } } }"
1216
+ ),
1217
+ )
1218
+
1219
+ return demo
1220
+
1221
+
1222
+ demo = build_demo()
1223
+ demo.queue(default_concurrency_limit=2)
1224
+
1225
+
1226
+ if __name__ == "__main__":
1227
+ demo.launch(theme=APP_THEME, css=CUSTOM_CSS)
pyproject.toml ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ [project]
2
+ name = "coding-model-space"
3
+ version = "0.1.0"
4
+ description = "Gradio Space that streams code from Cohere and runs generated Python or Web output."
5
+ requires-python = ">=3.13"
6
+ dependencies = [
7
+ "cohere>=5.16.1",
8
+ "gradio==6.14.0",
9
+ "matplotlib>=3.8",
10
+ ]
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ cohere>=5.16.1
2
+ gradio==6.14.0
3
+ matplotlib>=3.8
uv.lock ADDED
The diff for this file is too large to render. See raw diff