3VVM commited on
Commit
10f26af
·
verified ·
1 Parent(s): bfcf2b0

Upload 6 files

Browse files
Files changed (6) hide show
  1. .gitignore +2 -92
  2. README.md +11 -155
  3. agent.py +577 -0
  4. app.py +321 -0
  5. index.html +2093 -0
  6. requirements.txt +4 -0
.gitignore CHANGED
@@ -1,94 +1,4 @@
1
- # Byte-compiled / optimized / DLL files
2
  __pycache__/
3
- *.py[cod]
4
- *$py.class
5
-
6
- # C extensions
7
- *.so
8
-
9
- # Distribution / packaging
10
- .Python
11
- build/
12
- develop-eggs/
13
- dist/
14
- downloads/
15
- eggs/
16
- .eggs/
17
- # Ignore Python lib directories but NOT frontend/src/lib
18
- /lib/
19
- /lib64/
20
- venv/lib/
21
- venv/lib64/
22
- parts/
23
- sdist/
24
- var/
25
- *.egg-info/
26
- .installed.cfg
27
- *.egg
28
- MANIFEST
29
-
30
- # Virtual environments
31
- venv/
32
- env/
33
- ENV/
34
- .venv/
35
-
36
- # PyInstaller
37
- *.manifest
38
- *.spec
39
-
40
- # Installer logs
41
- pip-log.txt
42
- pip-delete-this-directory.txt
43
-
44
- # Unit test / coverage reports
45
- htmlcov/
46
- .tox/
47
- .nox/
48
- .coverage
49
- .coverage.*
50
- .cache
51
- nosetests.xml
52
- coverage.xml
53
- *.cover
54
- .hypothesis/
55
- .pytest_cache/
56
-
57
- # Jupyter Notebook
58
- .ipynb_checkpoints
59
-
60
- # pyenv
61
- .python-version
62
-
63
- # mypy
64
- .mypy_cache/
65
- .dmypy.json
66
-
67
- # Pyre type checker
68
- .pyre/
69
-
70
- # Gradio cache
71
- log/
72
- logs/
73
-
74
- # Documentation cache files (backend)
75
- .backend_gradio_docs_cache.txt
76
- .backend_gradio_docs_last_update.txt
77
- .gradio_docs_cache.txt
78
- .gradio_docs_last_update.txt
79
- .comfyui_docs_cache.txt
80
- .comfyui_docs_last_update.txt
81
- .fastrtc_docs_cache.txt
82
- .fastrtc_docs_last_update.txt
83
-
84
- # System files
85
  .DS_Store
86
- Thumbs.db
87
-
88
- # Lock files
89
- uv.lock
90
- poetry.lock
91
- Pipfile.lock
92
-
93
- # VSCode
94
- .vscode/
 
 
1
  __pycache__/
2
+ *.pyc
3
+ workspace_store/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  .DS_Store
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,162 +1,18 @@
1
  ---
2
- title: AnyCoder
3
- emoji: 🏆
4
- colorFrom: blue
5
- colorTo: purple
6
- sdk: docker
7
- app_port: 7860
 
 
8
  pinned: false
9
- disable_embedding: false
10
  hf_oauth: true
11
  hf_oauth_expiration_minutes: 43200
12
  hf_oauth_scopes:
13
- - manage-repos
14
- - write-discussions
15
- models:
16
- - MiniMaxAI/MiniMax-M2.5
17
- - zai-org/GLM-5
18
- - Qwen/Qwen3-Coder-Next
19
- - moonshotai/Kimi-K2.5
20
- - zai-org/GLM-4.7-Flash
21
- - zai-org/GLM-4.7
22
- - MiniMaxAI/MiniMax-M2.1
23
- - zai-org/GLM-4.6
24
- - zai-org/GLM-4.6V
25
- - deepseek-ai/DeepSeek-V3
26
- - deepseek-ai/DeepSeek-R1
27
- - MiniMaxAI/MiniMax-M2
28
- - moonshotai/Kimi-K2-Thinking
29
  ---
30
 
31
-
32
- # AnyCoder - AI Code Generator with React Frontend
33
-
34
- AnyCoder is a full-stack AI-powered code generator with a modern React/TypeScript frontend and FastAPI backend. Generate applications by describing them in plain English, with support for multiple AI models and one-click deployment to Hugging Face Spaces.
35
-
36
- ## 🎨 Features
37
-
38
- - **Modern React UI**: Apple-inspired design with VS Code layout
39
- - **Real-time Streaming**: Server-Sent Events for live code generation
40
- - **Multi-Model Support**: MiniMax M2, DeepSeek V3, and more via HuggingFace InferenceClient
41
- - **Multiple Languages**: HTML, Gradio, Streamlit, React, Transformers.js, ComfyUI
42
- - **Authentication**: HuggingFace OAuth + Dev mode for local testing
43
- - **One-Click Deployment**: Deploy generated apps directly to HF Spaces
44
-
45
- ## 🏗️ Architecture
46
-
47
- ```
48
- anycoder/
49
- ├── backend_api.py # FastAPI backend with streaming
50
- ├── frontend/ # Next.js React frontend
51
- │ ├── src/
52
- │ │ ├── app/ # Pages (page.tsx, layout.tsx, globals.css)
53
- │ │ ├── components/ # React components
54
- │ │ ├── lib/ # API client, auth utilities
55
- │ │ └── types/ # TypeScript types
56
- │ └── package.json
57
- ├── requirements.txt # Python dependencies
58
- ├── Dockerfile # Docker Space configuration
59
- └── start_fullstack.sh # Local development script
60
- ```
61
-
62
- ## 🚀 Quick Start
63
-
64
- ### Local Development
65
-
66
- 1. **Backend**:
67
- ```bash
68
- export HF_TOKEN="your_huggingface_token"
69
- export GEMINI_API_KEY="your_gemini_api_key"
70
- python backend_api.py
71
- ```
72
-
73
- 2. **Frontend** (new terminal):
74
- ```bash
75
- cd frontend
76
- npm install
77
- npm run dev
78
- ```
79
-
80
- 3. Open `http://localhost:3000`
81
-
82
- ### Using start script:
83
- ```bash
84
- export HF_TOKEN="your_token"
85
- export GEMINI_API_KEY="your_gemini_api_key"
86
- ./start_fullstack.sh
87
- ```
88
-
89
- ## 🐳 Docker Space Deployment
90
-
91
- This app runs as a Docker Space on HuggingFace. The Dockerfile:
92
- - Builds the Next.js frontend
93
- - Runs FastAPI backend on port 7860
94
- - Uses proper user permissions (UID 1000)
95
- - Handles environment variables securely
96
-
97
- ## 🔑 Authentication
98
-
99
- - **Dev Mode** (localhost): Mock login for testing
100
- - **Production**: HuggingFace OAuth with manage-repos scope
101
-
102
- ## 📝 Supported Languages
103
-
104
- - `html` - Static HTML pages
105
- - `gradio` - Python Gradio apps
106
- - `streamlit` - Python Streamlit apps
107
- - `react` - React/Next.js apps
108
- - `transformers.js` - Browser ML apps
109
- - `comfyui` - ComfyUI workflows
110
-
111
- ## 🤖 Available Models
112
-
113
- The following models are currently supported and used by AnyCoder:
114
-
115
- models:
116
- - MiniMaxAI/MiniMax-M2.5
117
- - zai-org/GLM-5
118
- - Qwen/Qwen3-Coder-Next
119
- - moonshotai/Kimi-K2.5
120
- - zai-org/GLM-4.7-Flash
121
- - zai-org/GLM-4.7
122
- - MiniMaxAI/MiniMax-M2.1
123
- - zai-org/GLM-4.6
124
- - zai-org/GLM-4.6V
125
- - deepseek-ai/DeepSeek-V3
126
- - deepseek-ai/DeepSeek-R1
127
- - MiniMaxAI/MiniMax-M2
128
- - moonshotai/Kimi-K2-Thinking
129
-
130
- ## 🎯 Usage
131
-
132
- 1. Sign in with HuggingFace (or use Dev Login locally)
133
- 2. Select a language and AI model
134
- 3. Describe your app in the chat
135
- 4. Watch code generate in real-time
136
- 5. Click **🚀 Deploy** to publish to HF Spaces
137
-
138
- ## 🛠️ Environment Variables
139
-
140
- - `HF_TOKEN` - HuggingFace API token (required)
141
- - `GEMINI_API_KEY` - Google Gemini API key (required for Gemini 3 Pro Preview)
142
- - `POE_API_KEY` - Poe API key (optional, for GPT-5 and Claude models)
143
- - `DASHSCOPE_API_KEY` - DashScope API key (optional, for Qwen models)
144
- - `OPENROUTER_API_KEY` - OpenRouter API key (optional, for Sherlock models)
145
- - `MISTRAL_API_KEY` - Mistral API key (optional, for Mistral models)
146
-
147
- ## 📦 Tech Stack
148
-
149
- **Frontend:**
150
- - Next.js 14
151
- - TypeScript
152
- - Tailwind CSS
153
- - Monaco Editor
154
-
155
- **Backend:**
156
- - FastAPI
157
- - HuggingFace Hub
158
- - Server-Sent Events (SSE)
159
-
160
- ## 📄 License
161
-
162
- MIT
 
1
  ---
2
+ title: GLM 5.2
3
+ emoji: 🐠
4
+ colorFrom: yellow
5
+ colorTo: red
6
+ sdk: gradio
7
+ sdk_version: 6.18.0
8
+ python_version: '3.13'
9
+ app_file: app.py
10
  pinned: false
11
+
12
  hf_oauth: true
13
  hf_oauth_expiration_minutes: 43200
14
  hf_oauth_scopes:
15
+ - inference-api
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  ---
17
 
18
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
agent.py ADDED
@@ -0,0 +1,577 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ agent.py -- Tool-using agent core for the GLM-5.2 personal workspace Space.
3
+
4
+ This module is deliberately self-contained and network-free (the only
5
+ network call in the whole app -- the actual chat completion -- is made by
6
+ app.py using the caller's own OpenAI-compatible client). Everything here is
7
+ pure local logic:
8
+
9
+ - a system prompt that teaches the model a strict, easy-to-parse
10
+ tool-call protocol
11
+ - a small set of real tools: python execution, shell execution, file
12
+ read/write/list/delete, zip packaging, pip install
13
+ - a per-user sandboxed workspace directory with path-traversal protection
14
+ - simple JSON-file backed conversation memory (persists to /data if the
15
+ Space has persistent storage attached, otherwise to a local folder
16
+ that lives only as long as the current container)
17
+ - a parser that pulls a tool call out of the model's raw reply text
18
+
19
+ app.py drives the actual multi-step loop; this file only exposes the
20
+ building blocks so that loop stays short and readable.
21
+ """
22
+
23
+ import os
24
+ import re
25
+ import sys
26
+ import json
27
+ import uuid
28
+ import shutil
29
+ import zipfile
30
+ import subprocess
31
+ from pathlib import Path
32
+ from typing import Optional, Dict, Any, List, Tuple
33
+
34
+
35
+ # --------------------------------------------------------------------------
36
+ # Tunables -- safe to adjust these constants as your needs change
37
+ # --------------------------------------------------------------------------
38
+
39
+ MAX_AGENT_STEPS = 12 # hard cap on tool round-trips per user message
40
+ MAX_TOOL_OUTPUT_CHARS = 8000 # truncate stdout/stderr fed back to the model
41
+ MAX_READ_FILE_CHARS = 20000 # truncate read_file output
42
+ MAX_FILE_WRITE_BYTES = 8 * 1024 * 1024 # 8MB per write_file call
43
+ MAX_UPLOAD_BYTES = 32 * 1024 * 1024 # 32MB per user upload
44
+ MAX_HISTORY_ITEMS_KEPT_ON_DISK = 400 # trim the on-disk transcript beyond this
45
+ MAX_HISTORY_ITEMS_SENT_TO_MODEL = 40 # trim what's replayed into the next API call
46
+ DEFAULT_TOOL_TIMEOUT = 60
47
+ MAX_TOOL_TIMEOUT = 240
48
+ ULIMIT_V_KB = 3_000_000 # ~2.9GB address-space cap per tool execution
49
+
50
+
51
+ # --------------------------------------------------------------------------
52
+ # Storage roots
53
+ # --------------------------------------------------------------------------
54
+
55
+ def _pick_storage_root() -> Path:
56
+ """Prefer Hugging Face Spaces persistent storage (/data) when it exists
57
+ and is writable, otherwise fall back to a folder next to the app that
58
+ only lives as long as the current container (reset on rebuild/restart).
59
+ """
60
+ data_dir = Path("/data")
61
+ try:
62
+ if data_dir.is_dir() and os.access(str(data_dir), os.W_OK):
63
+ root = data_dir / "glm_workspace"
64
+ root.mkdir(parents=True, exist_ok=True)
65
+ return root
66
+ except OSError:
67
+ pass
68
+ root = Path(__file__).resolve().parent / "workspace_store"
69
+ root.mkdir(parents=True, exist_ok=True)
70
+ return root
71
+
72
+
73
+ STORAGE_ROOT = _pick_storage_root()
74
+ IS_PERSISTENT = str(STORAGE_ROOT).startswith("/data")
75
+
76
+
77
+ def _slugify(raw: str) -> str:
78
+ raw = (raw or "").strip().lower()
79
+ slug = re.sub(r"[^a-z0-9_-]+", "-", raw).strip("-")
80
+ return slug or "user"
81
+
82
+
83
+ def user_key_from_userinfo(user_info: Optional[dict]) -> str:
84
+ """Stable, filesystem-safe key identifying this person's private
85
+ workspace, derived from their HF OAuth profile."""
86
+ if not user_info:
87
+ return "user"
88
+ raw = (
89
+ user_info.get("preferred_username")
90
+ or user_info.get("sub")
91
+ or user_info.get("name")
92
+ or "user"
93
+ )
94
+ return _slugify(str(raw))
95
+
96
+
97
+ def get_user_dir(user_key: str) -> Path:
98
+ d = STORAGE_ROOT / _slugify(user_key)
99
+ (d / "files").mkdir(parents=True, exist_ok=True)
100
+ return d
101
+
102
+
103
+ def get_files_dir(user_key: str) -> Path:
104
+ return get_user_dir(user_key) / "files"
105
+
106
+
107
+ def get_history_path(user_key: str) -> Path:
108
+ return get_user_dir(user_key) / "chat_history.json"
109
+
110
+
111
+ # --------------------------------------------------------------------------
112
+ # History persistence
113
+ # --------------------------------------------------------------------------
114
+
115
+ def load_history(user_key: str) -> List[Dict[str, Any]]:
116
+ p = get_history_path(user_key)
117
+ if not p.exists():
118
+ return []
119
+ try:
120
+ data = json.loads(p.read_text(encoding="utf-8"))
121
+ return data if isinstance(data, list) else []
122
+ except (json.JSONDecodeError, OSError):
123
+ return []
124
+
125
+
126
+ def save_history(user_key: str, history: List[Dict[str, Any]]) -> None:
127
+ trimmed = history[-MAX_HISTORY_ITEMS_KEPT_ON_DISK:]
128
+ p = get_history_path(user_key)
129
+ try:
130
+ p.write_text(json.dumps(trimmed, ensure_ascii=False), encoding="utf-8")
131
+ except OSError:
132
+ pass
133
+
134
+
135
+ def clear_history(user_key: str) -> None:
136
+ p = get_history_path(user_key)
137
+ try:
138
+ if p.exists():
139
+ p.unlink()
140
+ except OSError:
141
+ pass
142
+
143
+
144
+ def history_to_api_messages(history: List[Dict[str, Any]]) -> List[Dict[str, str]]:
145
+ """Collapse stored history into the plain role/content pairs the chat
146
+ completion API expects, trimmed to a recent window so context doesn't
147
+ grow without bound."""
148
+ recent = history[-MAX_HISTORY_ITEMS_SENT_TO_MODEL:]
149
+ return [{"role": item["role"], "content": item["content"]} for item in recent]
150
+
151
+
152
+ # --------------------------------------------------------------------------
153
+ # Path safety
154
+ # --------------------------------------------------------------------------
155
+
156
+ class UnsafePathError(ValueError):
157
+ pass
158
+
159
+
160
+ def safe_join(base: Path, rel_path: str) -> Path:
161
+ """Resolve rel_path against base, refusing anything that would escape
162
+ the workspace. A leading "/" is treated as "root of the workspace"
163
+ (stripped, not rejected) since every tool path is meant to be
164
+ workspace-relative anyway; ".." is still blocked below."""
165
+ rel_path = (rel_path or "").strip().lstrip("/")
166
+ if not rel_path or rel_path == ".":
167
+ return base.resolve()
168
+ candidate = (base / rel_path).resolve()
169
+ base_resolved = base.resolve()
170
+ if candidate != base_resolved and base_resolved not in candidate.parents:
171
+ raise UnsafePathError(f"Path '{rel_path}' escapes the workspace")
172
+ return candidate
173
+
174
+
175
+ def safe_upload_name(filename: str, target_dir: Path) -> str:
176
+ base = Path(filename or "upload.bin").name
177
+ stem = Path(base).stem
178
+ suffix = re.sub(r"[^A-Za-z0-9.]+", "", Path(base).suffix)[:12]
179
+ stem_slug = re.sub(r"[^A-Za-z0-9_.-]+", "_", stem).strip("_") or "upload"
180
+ candidate = f"{stem_slug}{suffix}"
181
+ if not (target_dir / candidate).exists():
182
+ return candidate
183
+ return f"{stem_slug}_{uuid.uuid4().hex[:6]}{suffix}"
184
+
185
+
186
+ def truncate(text: str, limit: int) -> str:
187
+ if not text:
188
+ return text or ""
189
+ if len(text) <= limit:
190
+ return text
191
+ cut = len(text) - limit
192
+ return text[:limit] + f"\n...[truncated {cut} more characters]..."
193
+
194
+
195
+ # --------------------------------------------------------------------------
196
+ # Sandboxed subprocess execution
197
+ # --------------------------------------------------------------------------
198
+
199
+ _SECRET_NAME_RE = re.compile(r"(TOKEN|SECRET|KEY|PASSWORD|CREDENTIAL)", re.IGNORECASE)
200
+
201
+
202
+ def _sanitized_env() -> Dict[str, str]:
203
+ """Strip anything that looks like a credential so a tool call can never
204
+ accidentally echo the Space's own secrets (e.g. HF_TOKEN) back into the
205
+ conversation, where they'd be sent to the inference provider as plain
206
+ text."""
207
+ return {k: v for k, v in os.environ.items() if not _SECRET_NAME_RE.search(k)}
208
+
209
+
210
+ def _ulimit_prefix(cpu_cap: int) -> str:
211
+ # Applied via the shell's own `ulimit` builtin *after* bash has already
212
+ # exec'd (not via subprocess's preexec_fn, which runs arbitrary Python
213
+ # between fork() and exec() and can deadlock in a multi-threaded async
214
+ # server). Limits are inherited across the following exec.
215
+ return f"ulimit -v {ULIMIT_V_KB} 2>/dev/null; ulimit -t {cpu_cap} 2>/dev/null; "
216
+
217
+
218
+ def _run_subprocess(cmd: List[str], cwd: Path, timeout: int) -> Dict[str, Any]:
219
+ try:
220
+ proc = subprocess.run(
221
+ cmd,
222
+ cwd=str(cwd),
223
+ capture_output=True,
224
+ text=True,
225
+ timeout=timeout,
226
+ env=_sanitized_env(),
227
+ )
228
+ return {
229
+ "success": proc.returncode == 0,
230
+ "exit_code": proc.returncode,
231
+ "stdout": truncate(proc.stdout or "", MAX_TOOL_OUTPUT_CHARS),
232
+ "stderr": truncate(proc.stderr or "", MAX_TOOL_OUTPUT_CHARS),
233
+ }
234
+ except subprocess.TimeoutExpired:
235
+ return {
236
+ "success": False, "exit_code": None, "stdout": "",
237
+ "stderr": f"Timed out after {timeout}s.",
238
+ }
239
+ except FileNotFoundError as e:
240
+ return {"success": False, "exit_code": None, "stdout": "", "stderr": str(e)}
241
+
242
+
243
+ def _run_argv(argv: List[str], cwd: Path, timeout: int) -> Dict[str, Any]:
244
+ cpu_cap = max(int(timeout) + 10, 10)
245
+ bash_cmd = ["/bin/bash", "-c", _ulimit_prefix(cpu_cap) + 'exec "$@"', "--", *argv]
246
+ return _run_subprocess(bash_cmd, cwd, timeout)
247
+
248
+
249
+ def _run_shell_str(command: str, cwd: Path, timeout: int) -> Dict[str, Any]:
250
+ cpu_cap = max(int(timeout) + 10, 10)
251
+ bash_cmd = ["/bin/bash", "-lc", _ulimit_prefix(cpu_cap) + command]
252
+ return _run_subprocess(bash_cmd, cwd, timeout)
253
+
254
+
255
+ _SHELL_DENYLIST = [
256
+ r"rm\s+-rf\s+/(?!\S)", # rm -rf /
257
+ r"rm\s+-rf\s+/\*", # rm -rf /*
258
+ r":\(\)\s*\{\s*:\|\s*:&\s*\}\s*;\s*:", # classic fork bomb
259
+ r"\bmkfs\b",
260
+ r"\bdd\s+.*of=/dev/",
261
+ r"\bshutdown\b",
262
+ r"\breboot\b",
263
+ r">\s*/dev/sd[a-z]",
264
+ ]
265
+ _SHELL_DENYLIST_RE = re.compile("|".join(_SHELL_DENYLIST), re.IGNORECASE)
266
+
267
+
268
+ def _clean_timeout(args: dict) -> int:
269
+ try:
270
+ t = int(args.get("timeout") or DEFAULT_TOOL_TIMEOUT)
271
+ except (TypeError, ValueError):
272
+ t = DEFAULT_TOOL_TIMEOUT
273
+ return max(1, min(t, MAX_TOOL_TIMEOUT))
274
+
275
+
276
+ # --------------------------------------------------------------------------
277
+ # Tool implementations -- each takes (args, files_dir) and returns a dict
278
+ # with at least {"success": bool, "stdout": str, "stderr": str}
279
+ # --------------------------------------------------------------------------
280
+
281
+ def tool_run_python(args: dict, files_dir: Path) -> Dict[str, Any]:
282
+ code = args.get("code")
283
+ if not code or not isinstance(code, str):
284
+ return {"success": False, "stdout": "", "stderr": "Missing 'code' string."}
285
+ timeout = _clean_timeout(args)
286
+ script_path = files_dir / f".tmp_{uuid.uuid4().hex}.py"
287
+ try:
288
+ script_path.write_text(code, encoding="utf-8")
289
+ return _run_argv([sys.executable, "-u", script_path.name], files_dir, timeout)
290
+ finally:
291
+ try:
292
+ script_path.unlink(missing_ok=True)
293
+ except OSError:
294
+ pass
295
+
296
+
297
+ def tool_run_shell(args: dict, files_dir: Path) -> Dict[str, Any]:
298
+ command = args.get("command")
299
+ if not command or not isinstance(command, str):
300
+ return {"success": False, "stdout": "", "stderr": "Missing 'command' string."}
301
+ if _SHELL_DENYLIST_RE.search(command):
302
+ return {
303
+ "success": False, "stdout": "",
304
+ "stderr": "Blocked: this command matches a destructive pattern disabled in this workspace.",
305
+ }
306
+ timeout = _clean_timeout(args)
307
+ return _run_shell_str(command, files_dir, timeout)
308
+
309
+
310
+ def tool_write_file(args: dict, files_dir: Path) -> Dict[str, Any]:
311
+ path = args.get("path")
312
+ content = args.get("content", "")
313
+ if not path or not isinstance(path, str):
314
+ return {"success": False, "stdout": "", "stderr": "Missing 'path'."}
315
+ if isinstance(content, (dict, list)):
316
+ content = json.dumps(content, ensure_ascii=False, indent=2)
317
+ content = "" if content is None else str(content)
318
+ if len(content.encode("utf-8", errors="ignore")) > MAX_FILE_WRITE_BYTES:
319
+ return {"success": False, "stdout": "", "stderr": f"File too large (max {MAX_FILE_WRITE_BYTES} bytes)."}
320
+ try:
321
+ target = safe_join(files_dir, path)
322
+ except UnsafePathError as e:
323
+ return {"success": False, "stdout": "", "stderr": str(e)}
324
+ target.parent.mkdir(parents=True, exist_ok=True)
325
+ target.write_text(content, encoding="utf-8")
326
+ return {"success": True, "stdout": f"Wrote {len(content)} chars to {path}", "stderr": "", "file": path}
327
+
328
+
329
+ def tool_read_file(args: dict, files_dir: Path) -> Dict[str, Any]:
330
+ path = args.get("path")
331
+ if not path or not isinstance(path, str):
332
+ return {"success": False, "stdout": "", "stderr": "Missing 'path'."}
333
+ try:
334
+ target = safe_join(files_dir, path)
335
+ except UnsafePathError as e:
336
+ return {"success": False, "stdout": "", "stderr": str(e)}
337
+ if not target.exists() or not target.is_file():
338
+ return {"success": False, "stdout": "", "stderr": f"No such file: {path}"}
339
+ try:
340
+ text = target.read_text(encoding="utf-8", errors="replace")
341
+ except OSError as e:
342
+ return {"success": False, "stdout": "", "stderr": str(e)}
343
+ return {"success": True, "stdout": truncate(text, MAX_READ_FILE_CHARS), "stderr": ""}
344
+
345
+
346
+ def tool_list_files(args: dict, files_dir: Path) -> Dict[str, Any]:
347
+ path = args.get("path", ".") or "."
348
+ try:
349
+ target = safe_join(files_dir, path)
350
+ except UnsafePathError as e:
351
+ return {"success": False, "stdout": "", "stderr": str(e)}
352
+ if not target.exists() or not target.is_dir():
353
+ return {"success": False, "stdout": "", "stderr": f"No such directory: {path}"}
354
+ entries = []
355
+ for p in sorted(target.rglob("*")):
356
+ if p.name.startswith(".tmp_") or p.name == "chat_history.json":
357
+ continue
358
+ rel = p.relative_to(files_dir)
359
+ kind = "dir" if p.is_dir() else "file"
360
+ size = str(p.stat().st_size) if p.is_file() else ""
361
+ entries.append(f"{kind:4} {size:>10} {rel}")
362
+ if len(entries) >= 500:
363
+ entries.append("... [truncated: more than 500 entries]")
364
+ break
365
+ return {"success": True, "stdout": "\n".join(entries) or "(empty)", "stderr": ""}
366
+
367
+
368
+ def tool_delete_file(args: dict, files_dir: Path) -> Dict[str, Any]:
369
+ path = args.get("path")
370
+ if not path or not isinstance(path, str):
371
+ return {"success": False, "stdout": "", "stderr": "Missing 'path'."}
372
+ try:
373
+ target = safe_join(files_dir, path)
374
+ except UnsafePathError as e:
375
+ return {"success": False, "stdout": "", "stderr": str(e)}
376
+ if target == files_dir.resolve():
377
+ return {"success": False, "stdout": "", "stderr": "Refusing to delete the whole workspace."}
378
+ if not target.exists():
379
+ return {"success": False, "stdout": "", "stderr": f"No such path: {path}"}
380
+ try:
381
+ if target.is_dir():
382
+ shutil.rmtree(target)
383
+ else:
384
+ target.unlink()
385
+ except OSError as e:
386
+ return {"success": False, "stdout": "", "stderr": str(e)}
387
+ return {"success": True, "stdout": f"Deleted {path}", "stderr": ""}
388
+
389
+
390
+ def tool_make_zip(args: dict, files_dir: Path) -> Dict[str, Any]:
391
+ output = (args.get("output") or f"archive_{uuid.uuid4().hex[:8]}.zip").strip()
392
+ if not output.lower().endswith(".zip"):
393
+ output += ".zip"
394
+ try:
395
+ out_target = safe_join(files_dir, output)
396
+ except UnsafePathError as e:
397
+ return {"success": False, "stdout": "", "stderr": str(e)}
398
+
399
+ paths = args.get("paths") or None
400
+ try:
401
+ if paths:
402
+ resolved = [safe_join(files_dir, p) for p in paths]
403
+ else:
404
+ resolved = [
405
+ p for p in files_dir.iterdir()
406
+ if p.name != Path(output).name
407
+ and not p.name.startswith(".tmp_")
408
+ and p.name != "chat_history.json"
409
+ ]
410
+ except UnsafePathError as e:
411
+ return {"success": False, "stdout": "", "stderr": str(e)}
412
+
413
+ out_target.parent.mkdir(parents=True, exist_ok=True)
414
+ count = 0
415
+ with zipfile.ZipFile(out_target, "w", zipfile.ZIP_DEFLATED) as zf:
416
+ for item in resolved:
417
+ if not item.exists():
418
+ continue
419
+ if item.is_file():
420
+ zf.write(item, item.relative_to(files_dir))
421
+ count += 1
422
+ else:
423
+ for sub in item.rglob("*"):
424
+ if sub.is_file():
425
+ zf.write(sub, sub.relative_to(files_dir))
426
+ count += 1
427
+ if count == 0:
428
+ out_target.unlink(missing_ok=True)
429
+ return {"success": False, "stdout": "", "stderr": "Nothing to zip (no matching files found)."}
430
+ size = out_target.stat().st_size
431
+ return {"success": True, "stdout": f"Created {output} ({count} files, {size} bytes)", "stderr": "", "file": output}
432
+
433
+
434
+ def tool_pip_install(args: dict, files_dir: Path) -> Dict[str, Any]:
435
+ package = args.get("package")
436
+ if not package or not isinstance(package, str):
437
+ return {"success": False, "stdout": "", "stderr": "Missing 'package'."}
438
+ if re.search(r"[;&|`$\n]", package):
439
+ return {"success": False, "stdout": "", "stderr": "Invalid characters in package spec."}
440
+ return _run_argv(
441
+ [sys.executable, "-m", "pip", "install", "--quiet",
442
+ "--disable-pip-version-check", "--break-system-packages", package],
443
+ files_dir, 180,
444
+ )
445
+
446
+
447
+ TOOLS = {
448
+ "run_python": tool_run_python,
449
+ "run_shell": tool_run_shell,
450
+ "write_file": tool_write_file,
451
+ "read_file": tool_read_file,
452
+ "list_files": tool_list_files,
453
+ "delete_file": tool_delete_file,
454
+ "make_zip": tool_make_zip,
455
+ "pip_install": tool_pip_install,
456
+ }
457
+
458
+
459
+ def execute_tool(name: str, args: dict, files_dir: Path) -> Dict[str, Any]:
460
+ fn = TOOLS.get(name)
461
+ if not fn:
462
+ return {"success": False, "stdout": "", "stderr": f"Unknown tool '{name}'. Available: {', '.join(TOOLS)}"}
463
+ if not isinstance(args, dict):
464
+ return {"success": False, "stdout": "", "stderr": "Tool 'args' must be a JSON object."}
465
+ try:
466
+ return fn(args, files_dir)
467
+ except Exception as e: # noqa: BLE001 -- surface tool bugs to the model instead of crashing the turn
468
+ return {"success": False, "stdout": "", "stderr": f"Tool crashed: {e}"}
469
+
470
+
471
+ # --------------------------------------------------------------------------
472
+ # Tool-call parsing
473
+ # --------------------------------------------------------------------------
474
+
475
+ _TAGGED_BLOCK_RE = re.compile(r"```tool_call\s*\n(.*?)```", re.DOTALL | re.IGNORECASE)
476
+ _ANY_BLOCK_RE = re.compile(r"```[a-zA-Z_-]*\s*\n(.*?)```", re.DOTALL)
477
+
478
+
479
+ def find_tool_call(text: str) -> Tuple[Optional[dict], Optional[str], str]:
480
+ """Look for a tool call in the model's raw turn text.
481
+
482
+ Returns (call, parse_error, display_text):
483
+ - call: {"tool": str, "args": dict} if a valid call was found, else None
484
+ - parse_error: a message to feed back to the model if a tool_call-shaped
485
+ block was found but was malformed, else None
486
+ - display_text: the turn text with the raw tool-call block removed, i.e.
487
+ whatever visible reasoning the model wrote for this turn
488
+ """
489
+ m = _TAGGED_BLOCK_RE.search(text)
490
+ if m:
491
+ raw = m.group(1).strip()
492
+ display = (text[: m.start()] + text[m.end():]).strip()
493
+ try:
494
+ data = json.loads(raw)
495
+ except json.JSONDecodeError as e:
496
+ return (
497
+ None,
498
+ f"Your tool_call block wasn't valid JSON ({e}). Send a single "
499
+ f'```tool_call block with valid JSON: {{"tool": "...", "args": {{...}}}}.',
500
+ display,
501
+ )
502
+ if not isinstance(data, dict) or "tool" not in data:
503
+ return None, "Your tool_call JSON must be an object with a 'tool' key and an 'args' object.", display
504
+ return {"tool": data.get("tool"), "args": data.get("args") or {}}, None, display
505
+
506
+ # Fallback: tolerate a mis-tagged fenced block (e.g. ```json) as long as
507
+ # it parses into the same shape -- models sometimes drift on the tag.
508
+ for bm in _ANY_BLOCK_RE.finditer(text):
509
+ raw = bm.group(1).strip()
510
+ try:
511
+ data = json.loads(raw)
512
+ except json.JSONDecodeError:
513
+ continue
514
+ if isinstance(data, dict) and data.get("tool") in TOOLS:
515
+ display = (text[: bm.start()] + text[bm.end():]).strip()
516
+ return {"tool": data.get("tool"), "args": data.get("args") or {}}, None, display
517
+
518
+ return None, None, text.strip()
519
+
520
+
521
+ # --------------------------------------------------------------------------
522
+ # System prompt
523
+ # --------------------------------------------------------------------------
524
+
525
+ BASE_SYSTEM_PROMPT = """You are a personal autonomous coding assistant running inside the user's own private, isolated Linux workspace (a Hugging Face Space container only this user can reach). You reason, write code, run it for real, read the results, fix bugs, and hand back finished files -- the same kind of workflow Claude Code or ChatGPT's code interpreter use.
526
+
527
+ WORKSPACE
528
+ All tool paths are relative to your private working directory. Anything you write there can be downloaded by the user directly from this chat. Nothing you do here is visible to anyone else.
529
+
530
+ TOOL PROTOCOL
531
+ To take an action, reply with ONLY one fenced block, exactly like this, and nothing else in that message:
532
+
533
+ ```tool_call
534
+ {"tool": "<name>", "args": {...}}
535
+ ```
536
+
537
+ Wait for the result before deciding what to do next. Only ONE tool call per message -- never bundle several. If you ever want to show what a tool call looks like as an example without actually running it, do not use a real ```tool_call fence for that -- describe it in words instead, since a real fenced block will actually execute.
538
+
539
+ AVAILABLE TOOLS
540
+ - run_python -- {"code": "...", "timeout": optional seconds (default 60)}. Runs Python 3 in your workspace.
541
+ - run_shell -- {"command": "...", "timeout": optional seconds (default 60)}. Runs a bash command in your workspace.
542
+ - write_file -- {"path": "relative/path.ext", "content": "..."}. Creates or overwrites a file.
543
+ - read_file -- {"path": "relative/path.ext"}.
544
+ - list_files -- {"path": optional relative dir, default "."}.
545
+ - delete_file -- {"path": "relative/path.ext"}.
546
+ - make_zip -- {"output": "name.zip", "paths": optional list of relative paths -- omit to zip everything}.
547
+ - pip_install -- {"package": "name"}. Installs a Python package for use in run_python.
548
+
549
+ WORKING STYLE
550
+ 1. Before telling the user a piece of code or a file is finished, actually run it with run_python or run_shell and check the output. If it errors, read the error, fix it, and run it again. Keep going until it genuinely works, or you've made a real effort and can explain what's blocking it.
551
+ 2. When the deliverable is more than a one-liner, or is a set of files, package it with make_zip and name the file in your final answer so the user can download it.
552
+ 3. When there is nothing left to run, just answer normally in plain text/Markdown with NO tool_call block. That ends your turn and is what the user sees as your reply.
553
+ 4. You have a limited number of tool calls for this message, so work efficiently rather than exploring aimlessly.
554
+ 5. Never try to damage or escape the container; stay inside your own workspace, and treat destructive system-wide commands as off-limits.
555
+ 6. For ordinary conversation that needs no code or files, just answer directly -- you don't have to use a tool just because you can.
556
+ """
557
+
558
+
559
+ def build_system_prompt(user_extra: Optional[str]) -> str:
560
+ if user_extra and user_extra.strip():
561
+ return BASE_SYSTEM_PROMPT + "\n\nADDITIONAL INSTRUCTIONS FROM THE USER:\n" + user_extra.strip()
562
+ return BASE_SYSTEM_PROMPT
563
+
564
+
565
+ def format_tool_result_message(tool: str, result: Dict[str, Any]) -> str:
566
+ lines = [f"[TOOL RESULT: {tool}]", f"success: {result.get('success')}"]
567
+ if result.get("exit_code") is not None:
568
+ lines.append(f"exit_code: {result['exit_code']}")
569
+ stdout = result.get("stdout") or ""
570
+ stderr = result.get("stderr") or ""
571
+ if stdout:
572
+ lines.append("stdout:\n" + stdout)
573
+ if stderr:
574
+ lines.append("stderr:\n" + stderr)
575
+ if not stdout and not stderr:
576
+ lines.append("(no output)")
577
+ return "\n".join(lines)
app.py ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import asyncio
4
+ import mimetypes
5
+ from typing import Optional
6
+
7
+ from fastapi import Request, UploadFile, File
8
+ from fastapi.responses import HTMLResponse, StreamingResponse, JSONResponse, FileResponse
9
+ from gradio import Server
10
+ from gradio.oauth import attach_oauth, _get_valid_oauth_info_from_session
11
+ from openai import AsyncOpenAI
12
+ from pydantic import BaseModel
13
+
14
+ import agent
15
+
16
+ # Initialize gradio.Server (which is a subclass of FastAPI)
17
+ app = Server()
18
+
19
+ # Delegate Hugging Face OAuth to Gradio's battle-tested implementation.
20
+ # attach_oauth(app) registers /login/huggingface, /login/callback, /logout, AND
21
+ # the SessionMiddleware. After it runs, the user's token + profile are stored
22
+ # in request.session['oauth_info'] under the keys authlib returns
23
+ # (access_token, expires_at, userinfo, ...).
24
+ attach_oauth(app)
25
+
26
+ MODEL_NAME = "zai-org/GLM-5.2:fireworks-ai"
27
+ MAX_TOKENS_PER_TURN = 4096
28
+
29
+
30
+ class ChatRequest(BaseModel):
31
+ message: str
32
+ temperature: float = 0.7
33
+ system_prompt: Optional[str] = None
34
+
35
+
36
+ def _session_info(request: Request) -> dict:
37
+ """Resolve everything downstream code needs from the current session in
38
+ one place: who's asking, which token pays for it, and which private
39
+ workspace folder is theirs."""
40
+ oauth_info = _get_valid_oauth_info_from_session(request.session) or {}
41
+ user_info = oauth_info.get("userinfo") or {}
42
+ user_token = (oauth_info.get("access_token") or "").strip()
43
+ host_token = os.environ.get("HF_TOKEN", "").strip()
44
+ api_key = (user_token or host_token).strip()
45
+ billed_to = "user" if user_token else ("host" if host_token else "none")
46
+ # Everyone signed in via OAuth gets their own workspace; if the Space is
47
+ # just running on the owner's own HF_TOKEN secret with nobody logged in,
48
+ # treat that as a single personal workspace.
49
+ user_key = agent.user_key_from_userinfo(user_info) if user_token else "_host"
50
+ return {
51
+ "user_info": user_info,
52
+ "api_key": api_key,
53
+ "billed_to": billed_to,
54
+ "user_key": user_key,
55
+ }
56
+
57
+
58
+ def _sse(payload: dict) -> str:
59
+ return "data: " + json.dumps(payload, ensure_ascii=False) + "\n\n"
60
+
61
+
62
+ @app.get("/me")
63
+ async def me(request: Request):
64
+ """Expose the current user's profile (or null) to the frontend."""
65
+ oauth_info = _get_valid_oauth_info_from_session(request.session) or {}
66
+ user_info = oauth_info.get("userinfo") or {}
67
+ if not oauth_info or not user_info:
68
+ return JSONResponse({"user": None})
69
+ return JSONResponse({"user": user_info})
70
+
71
+
72
+ @app.get("/api/history")
73
+ async def get_history(request: Request):
74
+ info = _session_info(request)
75
+ if not info["api_key"]:
76
+ return JSONResponse({"history": [], "persistent": agent.IS_PERSISTENT})
77
+ history = agent.load_history(info["user_key"])
78
+ display_items = [h["display"] for h in history if h.get("display")]
79
+ return JSONResponse({"history": display_items, "persistent": agent.IS_PERSISTENT})
80
+
81
+
82
+ @app.post("/api/history/clear")
83
+ async def clear_history_endpoint(request: Request):
84
+ info = _session_info(request)
85
+ if info["api_key"]:
86
+ agent.clear_history(info["user_key"])
87
+ return JSONResponse({"ok": True})
88
+
89
+
90
+ @app.get("/api/workspace")
91
+ async def workspace_listing(request: Request):
92
+ info = _session_info(request)
93
+ if not info["api_key"]:
94
+ return JSONResponse({"files": [], "persistent": agent.IS_PERSISTENT})
95
+ files_dir = agent.get_files_dir(info["user_key"])
96
+ items = []
97
+ for p in sorted(files_dir.rglob("*")):
98
+ if p.is_file() and not p.name.startswith(".tmp_") and p.name != "chat_history.json":
99
+ items.append({"name": str(p.relative_to(files_dir)), "size": p.stat().st_size})
100
+ return JSONResponse({"files": items, "persistent": agent.IS_PERSISTENT})
101
+
102
+
103
+ @app.get("/api/files/{filename:path}")
104
+ async def download_file(request: Request, filename: str):
105
+ info = _session_info(request)
106
+ if not info["api_key"]:
107
+ return JSONResponse({"error": "Not signed in."}, status_code=401)
108
+ files_dir = agent.get_files_dir(info["user_key"])
109
+ try:
110
+ target = agent.safe_join(files_dir, filename)
111
+ except agent.UnsafePathError:
112
+ return JSONResponse({"error": "Invalid path."}, status_code=400)
113
+ if not target.exists() or not target.is_file():
114
+ return JSONResponse({"error": "Not found."}, status_code=404)
115
+ media_type, _ = mimetypes.guess_type(str(target))
116
+ return FileResponse(str(target), media_type=media_type or "application/octet-stream", filename=target.name)
117
+
118
+
119
+ @app.post("/api/upload")
120
+ async def upload_file(request: Request, file: UploadFile = File(...)):
121
+ info = _session_info(request)
122
+ if not info["api_key"]:
123
+ return JSONResponse({"error": "Not signed in."}, status_code=401)
124
+ files_dir = agent.get_files_dir(info["user_key"])
125
+ contents = await file.read()
126
+ if len(contents) > agent.MAX_UPLOAD_BYTES:
127
+ return JSONResponse({"error": f"File too large (max {agent.MAX_UPLOAD_BYTES} bytes)."}, status_code=400)
128
+ safe_name = agent.safe_upload_name(file.filename or "upload.bin", files_dir)
129
+ (files_dir / safe_name).write_bytes(contents)
130
+ return JSONResponse({"name": safe_name, "size": len(contents)})
131
+
132
+
133
+ @app.post("/api/chat")
134
+ async def chat_endpoint(request: Request, payload: ChatRequest):
135
+ info = _session_info(request)
136
+ api_key = info["api_key"]
137
+ billed_to = info["billed_to"]
138
+ user_key = info["user_key"]
139
+
140
+ headers = {
141
+ "Cache-Control": "no-cache",
142
+ "Connection": "keep-alive",
143
+ "X-Accel-Buffering": "no",
144
+ }
145
+
146
+ if not api_key:
147
+ async def _need_login():
148
+ yield _sse({
149
+ "type": "error",
150
+ "message": "Not signed in. Click the Hugging Face button in the sidebar to sign in "
151
+ "and run inference billed to your own HF account.",
152
+ })
153
+ yield _sse({"type": "done", "billedTo": "none"})
154
+ return StreamingResponse(_need_login(), media_type="text/event-stream", headers=headers)
155
+
156
+ user_message = (payload.message or "").strip()
157
+ if not user_message:
158
+ async def _empty():
159
+ yield _sse({"type": "error", "message": "Empty message."})
160
+ yield _sse({"type": "done", "billedTo": billed_to})
161
+ return StreamingResponse(_empty(), media_type="text/event-stream", headers=headers)
162
+
163
+ client = AsyncOpenAI(base_url="https://router.huggingface.co/v1", api_key=api_key)
164
+ files_dir = agent.get_files_dir(user_key)
165
+ system_prompt = agent.build_system_prompt(payload.system_prompt)
166
+
167
+ history = agent.load_history(user_key)
168
+ history.append({
169
+ "role": "user",
170
+ "content": user_message,
171
+ "display": {"type": "user", "text": user_message},
172
+ })
173
+ agent.save_history(user_key, history)
174
+
175
+ async def event_generator():
176
+ try:
177
+ finished = False
178
+
179
+ for _step in range(agent.MAX_AGENT_STEPS):
180
+ yield _sse({"type": "turn_start"})
181
+ api_messages = [{"role": "system", "content": system_prompt}] + agent.history_to_api_messages(history)
182
+ turn_text = ""
183
+ try:
184
+ stream = await client.chat.completions.create(
185
+ model=MODEL_NAME,
186
+ messages=api_messages,
187
+ temperature=payload.temperature,
188
+ max_tokens=MAX_TOKENS_PER_TURN,
189
+ stream=True,
190
+ )
191
+ async for chunk in stream:
192
+ if chunk.choices:
193
+ delta = chunk.choices[0].delta.content
194
+ if delta:
195
+ turn_text += delta
196
+ yield _sse({"type": "token", "content": delta})
197
+ except Exception as e:
198
+ yield _sse({"type": "error", "message": f"Model call failed: {e}"})
199
+ yield _sse({"type": "done", "billedTo": billed_to})
200
+ return
201
+
202
+ call, parse_error, display_text = agent.find_tool_call(turn_text)
203
+
204
+ history.append({
205
+ "role": "assistant",
206
+ "content": turn_text,
207
+ "display": {"type": "assistant_text", "text": display_text, "tool_call": call},
208
+ })
209
+ agent.save_history(user_key, history)
210
+
211
+ if parse_error:
212
+ yield _sse({"type": "tool_result", "tool": None, "success": False,
213
+ "stdout": "", "stderr": parse_error, "exit_code": None, "file": None,
214
+ "text": display_text})
215
+ history.append({
216
+ "role": "user",
217
+ "content": f"[SYSTEM]: {parse_error}",
218
+ "display": {"type": "tool_result", "tool": None, "success": False,
219
+ "stdout": "", "stderr": parse_error, "exit_code": None, "file": None},
220
+ })
221
+ agent.save_history(user_key, history)
222
+ continue
223
+
224
+ if not call:
225
+ yield _sse({"type": "done", "billedTo": billed_to})
226
+ finished = True
227
+ break
228
+
229
+ tool_name = call.get("tool")
230
+ tool_args = call.get("args") or {}
231
+ yield _sse({"type": "tool_call", "tool": tool_name, "args": tool_args, "text": display_text})
232
+
233
+ result = await asyncio.to_thread(agent.execute_tool, tool_name, tool_args, files_dir)
234
+ yield _sse({
235
+ "type": "tool_result",
236
+ "tool": tool_name,
237
+ "success": bool(result.get("success")),
238
+ "stdout": result.get("stdout") or "",
239
+ "stderr": result.get("stderr") or "",
240
+ "exit_code": result.get("exit_code"),
241
+ "file": result.get("file") if result.get("success") else None,
242
+ })
243
+
244
+ produced_file = result.get("file") if result.get("success") else None
245
+ if produced_file:
246
+ try:
247
+ size = (files_dir / produced_file).stat().st_size
248
+ except OSError:
249
+ size = None
250
+ yield _sse({"type": "file", "name": produced_file, "url": f"/api/files/{produced_file}", "size": size})
251
+
252
+ history.append({
253
+ "role": "user",
254
+ "content": agent.format_tool_result_message(tool_name, result),
255
+ "display": {
256
+ "type": "tool_result",
257
+ "tool": tool_name,
258
+ "success": bool(result.get("success")),
259
+ "stdout": result.get("stdout") or "",
260
+ "stderr": result.get("stderr") or "",
261
+ "exit_code": result.get("exit_code"),
262
+ "file": produced_file,
263
+ },
264
+ })
265
+ agent.save_history(user_key, history)
266
+
267
+ if not finished:
268
+ # Ran out of steps: force exactly one more plain-text round
269
+ # with no tool parsing, then stop no matter what.
270
+ api_messages = [{"role": "system", "content": system_prompt}] + agent.history_to_api_messages(history)
271
+ api_messages.append({
272
+ "role": "user",
273
+ "content": "[SYSTEM]: You're out of tool calls for this message. "
274
+ "Give your best final answer now in plain text -- no tool_call block.",
275
+ })
276
+ yield _sse({"type": "turn_start"})
277
+ turn_text = ""
278
+ try:
279
+ stream = await client.chat.completions.create(
280
+ model=MODEL_NAME,
281
+ messages=api_messages,
282
+ temperature=payload.temperature,
283
+ max_tokens=MAX_TOKENS_PER_TURN,
284
+ stream=True,
285
+ )
286
+ async for chunk in stream:
287
+ if chunk.choices:
288
+ delta = chunk.choices[0].delta.content
289
+ if delta:
290
+ turn_text += delta
291
+ yield _sse({"type": "token", "content": delta})
292
+ except Exception as e:
293
+ yield _sse({"type": "error", "message": f"Model call failed: {e}"})
294
+
295
+ history.append({
296
+ "role": "assistant",
297
+ "content": turn_text,
298
+ "display": {"type": "assistant_text", "text": turn_text.strip(), "tool_call": None},
299
+ })
300
+ agent.save_history(user_key, history)
301
+ yield _sse({"type": "done", "billedTo": billed_to})
302
+
303
+ except Exception as e:
304
+ yield _sse({"type": "error", "message": str(e)})
305
+ yield _sse({"type": "done", "billedTo": billed_to})
306
+
307
+ return StreamingResponse(event_generator(), media_type="text/event-stream", headers=headers)
308
+
309
+
310
+ @app.get("/", response_class=HTMLResponse)
311
+ async def homepage():
312
+ html_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "index.html")
313
+ if not os.path.exists(html_path):
314
+ return HTMLResponse("<h3>index.html not found. Please ensure it is created in the workspace directory.</h3>", status_code=404)
315
+ with open(html_path, "r", encoding="utf-8") as f:
316
+ return HTMLResponse(f.read())
317
+
318
+
319
+ if __name__ == "__main__":
320
+ # Launch Gradio Server (which binds to the FastAPI app underneath)
321
+ app.launch(show_error=True)
index.html ADDED
@@ -0,0 +1,2093 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>GLM 5.2</title>
7
+
8
+ <!-- Google Fonts -->
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;500;600&family=Inter:wght@300;400;500;600;700&family=Playfair+Display:ital,wght@0,400..900;1,400..900&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet">
12
+
13
+ <!-- Markdown Parser -->
14
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
15
+
16
+ <style>
17
+ :root {
18
+ /* Clean Minimalist Light Mode (Screenshot Aesthetics) */
19
+ --bg-color: #ffffff;
20
+ --sidebar-bg: #ffffff;
21
+ --sidebar-border: rgba(0, 0, 0, 0.06);
22
+ --sidebar-icon-color: #71717a;
23
+ --sidebar-icon-hover: #18181b;
24
+ --sidebar-active-bg: #f4f4f5;
25
+
26
+ --text-color: #09090b;
27
+ --text-muted: #71717a;
28
+ --text-terminal: #27272a;
29
+
30
+ --input-bg: #ffffff;
31
+ --input-border: rgba(0, 0, 0, 0.08);
32
+ --input-shadow: 0 4px 20px rgba(0, 0, 0, 0.03), 0 10px 40px -10px rgba(0, 0, 0, 0.05);
33
+
34
+ --btn-active-bg: #18181b;
35
+ --btn-active-color: #ffffff;
36
+ --btn-disabled-bg: #f4f4f5;
37
+ --btn-disabled-color: #a1a1aa;
38
+
39
+ --font-serif: 'Playfair Display', serif;
40
+ --font-sans: 'Outfit', 'Inter', sans-serif;
41
+ --font-mono: 'Fira Code', monospace;
42
+
43
+ --msg-user-bg: transparent;
44
+ --msg-assistant-bg: transparent;
45
+
46
+ --code-bg: #18181b;
47
+ --code-color: #f4f4f5;
48
+ --accent-color: #18181b;
49
+ --accent-bg: #f4f4f5;
50
+ --watermark-color: rgba(0, 0, 0, 0.02);
51
+ --capsule-bg: #f4f4f5;
52
+ --capsule-text: #27272a;
53
+ --capsule-border: rgba(0, 0, 0, 0.04);
54
+
55
+ --cursor-color: #18181b;
56
+ }
57
+
58
+ /* Dark Modern Mode */
59
+ .dark-mode {
60
+ --bg-color: #09090b;
61
+ --sidebar-bg: #09090b;
62
+ --sidebar-border: rgba(255, 255, 255, 0.06);
63
+ --sidebar-icon-color: #a1a1aa;
64
+ --sidebar-icon-hover: #ffffff;
65
+ --sidebar-active-bg: #27272a;
66
+
67
+ --text-color: #f4f4f5;
68
+ --text-muted: #a1a1aa;
69
+ --text-terminal: #e4e4e7;
70
+
71
+ --input-bg: #09090b;
72
+ --input-border: rgba(255, 255, 255, 0.08);
73
+ --input-shadow: 0 4px 25px rgba(0, 0, 0, 0.4);
74
+
75
+ --btn-active-bg: #ffffff;
76
+ --btn-active-color: #09090b;
77
+ --btn-disabled-bg: #27272a;
78
+ --btn-disabled-color: #71717a;
79
+
80
+ --code-bg: #27272a;
81
+ --code-color: #f4f4f5;
82
+ --accent-color: #ffffff;
83
+ --accent-bg: #27272a;
84
+ --watermark-color: rgba(255, 255, 255, 0.015);
85
+ --capsule-bg: #27272a;
86
+ --capsule-text: #e4e4e7;
87
+ --capsule-border: rgba(255, 255, 255, 0.04);
88
+
89
+ --cursor-color: #ffffff;
90
+ }
91
+
92
+ /* Classic Retro Hacker Terminal Theme */
93
+ .classic-terminal-mode {
94
+ --bg-color: #050806;
95
+ --sidebar-bg: #020403;
96
+ --sidebar-border: rgba(51, 255, 51, 0.15);
97
+ --sidebar-icon-color: rgba(51, 255, 51, 0.6);
98
+ --sidebar-icon-hover: #33ff33;
99
+ --sidebar-active-bg: rgba(51, 255, 51, 0.1);
100
+
101
+ --text-color: #33ff33;
102
+ --text-muted: rgba(51, 255, 51, 0.6);
103
+ --text-terminal: #33ff33;
104
+
105
+ --input-bg: #020403;
106
+ --input-border: rgba(51, 255, 51, 0.3);
107
+ --input-shadow: 0 0 15px rgba(51, 255, 51, 0.1);
108
+
109
+ --btn-active-bg: #33ff33;
110
+ --btn-active-color: #020403;
111
+ --btn-disabled-bg: rgba(51, 255, 51, 0.05);
112
+ --btn-disabled-color: rgba(51, 255, 51, 0.3);
113
+
114
+ --font-serif: 'Fira Code', monospace;
115
+ --font-sans: 'Fira Code', monospace;
116
+ --font-mono: 'Fira Code', monospace;
117
+
118
+ --code-bg: #020403;
119
+ --code-color: #33ff33;
120
+ --accent-color: #33ff33;
121
+ --accent-bg: rgba(51, 255, 51, 0.08);
122
+ --watermark-color: rgba(51, 255, 51, 0.02);
123
+ --capsule-bg: rgba(51, 255, 51, 0.05);
124
+ --capsule-text: #33ff33;
125
+ --capsule-border: rgba(51, 255, 51, 0.2);
126
+
127
+ --cursor-color: #33ff33;
128
+ }
129
+
130
+ * {
131
+ box-sizing: border-box;
132
+ margin: 0;
133
+ padding: 0;
134
+ transition: background-color 0.25s ease, border-color 0.25s ease, color 0.15s ease;
135
+ }
136
+
137
+ body, html {
138
+ height: 100%;
139
+ width: 100%;
140
+ font-family: var(--font-sans);
141
+ background-color: var(--bg-color);
142
+ color: var(--text-color);
143
+ overflow: hidden;
144
+ display: flex;
145
+ }
146
+
147
+ /* Sidebar Styling (Matches Screenshot Layout) */
148
+ .sidebar {
149
+ width: 72px;
150
+ height: 100%;
151
+ background-color: var(--sidebar-bg);
152
+ border-right: 1px solid var(--sidebar-border);
153
+ display: flex;
154
+ flex-direction: column;
155
+ align-items: center;
156
+ padding: 16px 0;
157
+ justify-content: space-between;
158
+ z-index: 10;
159
+ }
160
+
161
+ .sidebar-top {
162
+ display: flex;
163
+ flex-direction: column;
164
+ align-items: center;
165
+ gap: 16px;
166
+ width: 100%;
167
+ }
168
+
169
+ .sidebar-bottom {
170
+ display: flex;
171
+ flex-direction: column;
172
+ align-items: center;
173
+ gap: 16px;
174
+ width: 100%;
175
+ }
176
+
177
+ .sidebar-btn {
178
+ width: 44px;
179
+ height: 44px;
180
+ border-radius: 8px;
181
+ border: 1px solid transparent;
182
+ background: transparent;
183
+ color: var(--sidebar-icon-color);
184
+ display: flex;
185
+ align-items: center;
186
+ justify-content: center;
187
+ cursor: pointer;
188
+ position: relative;
189
+ }
190
+
191
+ .sidebar-btn:hover {
192
+ color: var(--sidebar-icon-hover);
193
+ background-color: var(--sidebar-active-bg);
194
+ }
195
+
196
+ .sidebar-btn.active {
197
+ background-color: var(--sidebar-active-bg);
198
+ color: var(--sidebar-icon-hover);
199
+ border-color: var(--sidebar-border);
200
+ }
201
+
202
+ /* Top Brand Button (Boxed Z Logo) */
203
+ .brand-btn {
204
+ width: 44px;
205
+ height: 44px;
206
+ border-radius: 8px;
207
+ background-color: var(--bg-color);
208
+ border: 1px solid var(--sidebar-border);
209
+ color: var(--text-color);
210
+ display: flex;
211
+ align-items: center;
212
+ justify-content: center;
213
+ cursor: pointer;
214
+ font-weight: 700;
215
+ font-family: var(--font-sans);
216
+ font-size: 18px;
217
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02);
218
+ }
219
+
220
+ .brand-btn:hover {
221
+ background-color: var(--sidebar-active-bg);
222
+ }
223
+
224
+ /* Tooltip */
225
+ .sidebar-btn::after {
226
+ content: attr(data-tooltip);
227
+ position: absolute;
228
+ left: 80px;
229
+ background: #18181b;
230
+ color: #ffffff;
231
+ padding: 4px 8px;
232
+ border-radius: 4px;
233
+ font-size: 11px;
234
+ white-space: nowrap;
235
+ opacity: 0;
236
+ pointer-events: none;
237
+ transition: opacity 0.2s ease, transform 0.2s ease;
238
+ transform: translateX(-10px);
239
+ z-index: 100;
240
+ font-family: var(--font-sans);
241
+ }
242
+
243
+ .sidebar-btn:hover::after {
244
+ opacity: 1;
245
+ transform: translateX(0);
246
+ }
247
+
248
+ /* Main Workspace */
249
+ .workspace {
250
+ flex: 1;
251
+ height: 100%;
252
+ display: flex;
253
+ flex-direction: column;
254
+ position: relative;
255
+ }
256
+
257
+ /* Watermark Background Z */
258
+ .watermark-container {
259
+ position: absolute;
260
+ top: 50%;
261
+ left: 50%;
262
+ transform: translate(-50%, -50%);
263
+ width: 450px;
264
+ height: 450px;
265
+ pointer-events: none;
266
+ z-index: 1;
267
+ display: flex;
268
+ align-items: center;
269
+ justify-content: center;
270
+ }
271
+
272
+ .watermark-svg {
273
+ width: 100%;
274
+ height: 100%;
275
+ stroke: var(--watermark-color);
276
+ fill: none;
277
+ stroke-width: 0.8;
278
+ stroke-linecap: round;
279
+ stroke-linejoin: round;
280
+ }
281
+
282
+ /* Welcome Center State */
283
+ .welcome-panel {
284
+ flex: 1;
285
+ display: flex;
286
+ flex-direction: column;
287
+ justify-content: center;
288
+ align-items: center;
289
+ text-align: center;
290
+ z-index: 2;
291
+ padding: 32px;
292
+ user-select: none;
293
+ }
294
+
295
+ .welcome-title {
296
+ font-family: var(--font-serif);
297
+ font-size: 48px;
298
+ font-weight: 400;
299
+ margin-bottom: 16px;
300
+ color: var(--text-color);
301
+ letter-spacing: -0.5px;
302
+ animation: fadeInUp 0.8s ease;
303
+ }
304
+
305
+ .welcome-subtitle {
306
+ font-family: var(--font-sans);
307
+ font-size: 16px;
308
+ color: var(--text-muted);
309
+ font-weight: 400;
310
+ letter-spacing: 0.1px;
311
+ animation: fadeInUp 0.8s ease 0.1s both;
312
+ }
313
+
314
+ /* Chat Terminal Console Output */
315
+ .chat-container {
316
+ flex: 1;
317
+ overflow-y: auto;
318
+ padding: 40px 10%;
319
+ display: none;
320
+ flex-direction: column;
321
+ gap: 24px;
322
+ z-index: 2;
323
+ scroll-behavior: smooth;
324
+ }
325
+
326
+ .chat-container.terminal-layout {
327
+ background-size: 20px 20px;
328
+ background-image:
329
+ linear-gradient(to right, rgba(0, 0, 0, 0.01) 1px, transparent 1px),
330
+ linear-gradient(to bottom, rgba(0, 0, 0, 0.01) 1px, transparent 1px);
331
+ }
332
+
333
+ .dark-mode .chat-container.terminal-layout {
334
+ background-image:
335
+ linear-gradient(to right, rgba(255, 255, 255, 0.005) 1px, transparent 1px),
336
+ linear-gradient(to bottom, rgba(255, 255, 255, 0.005) 1px, transparent 1px);
337
+ }
338
+
339
+ .classic-terminal-mode .chat-container.terminal-layout {
340
+ background-image:
341
+ linear-gradient(to right, rgba(51, 255, 51, 0.015) 1px, transparent 1px),
342
+ linear-gradient(to bottom, rgba(51, 255, 51, 0.015) 1px, transparent 1px);
343
+ }
344
+
345
+ .message-row {
346
+ display: flex;
347
+ flex-direction: column;
348
+ width: 100%;
349
+ animation: fadeIn 0.3s ease-out;
350
+ line-height: 1.6;
351
+ }
352
+
353
+ /* Terminal Prompt Headers */
354
+ .prompt-header {
355
+ display: flex;
356
+ align-items: center;
357
+ gap: 8px;
358
+ margin-bottom: 4px;
359
+ user-select: none;
360
+ }
361
+
362
+ .prompt-prefix-user {
363
+ color: #3b82f6; /* Modern Blue */
364
+ font-weight: 600;
365
+ }
366
+
367
+ .classic-terminal-mode .prompt-prefix-user {
368
+ color: #33ff33;
369
+ }
370
+
371
+ .prompt-prefix-system {
372
+ color: #10b981; /* Modern Green */
373
+ font-weight: 600;
374
+ }
375
+
376
+ .classic-terminal-mode .prompt-prefix-system {
377
+ color: #33ff33;
378
+ }
379
+
380
+ .prompt-command {
381
+ color: var(--text-muted);
382
+ font-size: 12px;
383
+ }
384
+
385
+ .prompt-text {
386
+ color: var(--text-color);
387
+ white-space: pre-wrap;
388
+ word-break: break-word;
389
+ padding-left: 4px;
390
+ }
391
+
392
+ .message-content {
393
+ padding-left: 16px;
394
+ border-left: 2px solid var(--sidebar-border);
395
+ color: var(--text-terminal);
396
+ margin-top: 4px;
397
+ word-break: break-word;
398
+ }
399
+
400
+ .message-content p {
401
+ margin-bottom: 12px;
402
+ }
403
+
404
+ .message-content p:last-child {
405
+ margin-bottom: 0;
406
+ }
407
+
408
+ /* Code Block Styling */
409
+ .message-content pre {
410
+ background-color: var(--code-bg);
411
+ color: var(--code-color);
412
+ padding: 14px;
413
+ border-radius: 8px;
414
+ overflow-x: auto;
415
+ margin: 12px 0;
416
+ border: 1px solid var(--sidebar-border);
417
+ position: relative;
418
+ }
419
+
420
+ .message-content code {
421
+ font-family: var(--font-mono);
422
+ font-size: 13px;
423
+ }
424
+
425
+ /* ===== Agent Tool Cards & File Chips ===== */
426
+ .tool-card {
427
+ background-color: var(--accent-bg);
428
+ border: 1px solid var(--sidebar-border);
429
+ border-radius: 10px;
430
+ margin: 10px 0;
431
+ overflow: hidden;
432
+ font-family: var(--font-mono);
433
+ font-size: 12.5px;
434
+ }
435
+
436
+ .tool-card-header {
437
+ display: flex;
438
+ align-items: center;
439
+ gap: 8px;
440
+ padding: 9px 12px;
441
+ color: var(--text-terminal);
442
+ border-bottom: 1px solid var(--sidebar-border);
443
+ }
444
+
445
+ .tool-card-header .tool-name {
446
+ font-weight: 600;
447
+ flex-shrink: 0;
448
+ }
449
+
450
+ .tool-card-header .tool-args-preview {
451
+ color: var(--text-muted);
452
+ overflow: hidden;
453
+ text-overflow: ellipsis;
454
+ white-space: nowrap;
455
+ flex: 1;
456
+ }
457
+
458
+ .tool-status {
459
+ flex-shrink: 0;
460
+ font-size: 10px;
461
+ font-weight: 600;
462
+ letter-spacing: 0.04em;
463
+ text-transform: uppercase;
464
+ padding: 2px 8px;
465
+ border-radius: 999px;
466
+ }
467
+
468
+ .tool-status.pending {
469
+ color: var(--text-muted);
470
+ border: 1px solid var(--sidebar-border);
471
+ }
472
+
473
+ .tool-status.success {
474
+ color: #0a7d34;
475
+ background-color: rgba(16, 185, 90, 0.12);
476
+ }
477
+
478
+ .tool-status.fail {
479
+ color: #c0392b;
480
+ background-color: rgba(220, 60, 40, 0.1);
481
+ }
482
+
483
+ .tool-card-body {
484
+ padding: 10px 12px;
485
+ }
486
+
487
+ .tool-card-output {
488
+ background-color: var(--code-bg);
489
+ color: var(--code-color);
490
+ padding: 10px 12px;
491
+ border-radius: 6px;
492
+ overflow-x: auto;
493
+ white-space: pre-wrap;
494
+ word-break: break-word;
495
+ max-height: 260px;
496
+ overflow-y: auto;
497
+ margin: 0;
498
+ }
499
+
500
+ .tool-card-output.stderr-block {
501
+ margin-top: 8px;
502
+ border-left: 3px solid #c0392b;
503
+ }
504
+
505
+ .tool-card-file-line {
506
+ margin-top: 8px;
507
+ color: var(--text-muted);
508
+ }
509
+
510
+ .file-chip {
511
+ display: inline-flex;
512
+ align-items: center;
513
+ gap: 8px;
514
+ padding: 10px 14px;
515
+ margin: 6px 0;
516
+ background-color: var(--accent-bg);
517
+ border: 1px solid var(--sidebar-border);
518
+ border-radius: 10px;
519
+ text-decoration: none;
520
+ color: var(--text-color);
521
+ font-family: var(--font-sans);
522
+ font-size: 13.5px;
523
+ transition: background-color 0.15s ease;
524
+ }
525
+
526
+ .file-chip:hover {
527
+ background-color: var(--sidebar-active-bg);
528
+ }
529
+
530
+ .file-chip svg {
531
+ flex-shrink: 0;
532
+ width: 16px;
533
+ height: 16px;
534
+ }
535
+
536
+ .file-chip .file-chip-meta {
537
+ color: var(--text-muted);
538
+ font-size: 11.5px;
539
+ }
540
+
541
+ .attach-btn {
542
+ background: none;
543
+ border: none;
544
+ cursor: pointer;
545
+ color: var(--sidebar-icon-color);
546
+ display: flex;
547
+ align-items: center;
548
+ justify-content: center;
549
+ padding: 6px;
550
+ border-radius: 8px;
551
+ transition: color 0.15s ease, background-color 0.15s ease;
552
+ }
553
+
554
+ .attach-btn:hover {
555
+ color: var(--sidebar-icon-hover);
556
+ background-color: var(--sidebar-active-bg);
557
+ }
558
+
559
+ .attach-btn svg {
560
+ width: 17px;
561
+ height: 17px;
562
+ }
563
+
564
+ .pending-upload-row {
565
+ display: none;
566
+ align-items: center;
567
+ gap: 8px;
568
+ padding: 6px 10px;
569
+ margin-bottom: 4px;
570
+ background-color: var(--accent-bg);
571
+ border-radius: 8px;
572
+ font-size: 12.5px;
573
+ color: var(--text-muted);
574
+ font-family: var(--font-mono);
575
+ }
576
+
577
+ .pending-upload-row.visible {
578
+ display: flex;
579
+ }
580
+
581
+ .pending-upload-row .remove-upload {
582
+ cursor: pointer;
583
+ color: var(--text-muted);
584
+ font-weight: 600;
585
+ margin-left: auto;
586
+ }
587
+
588
+ .pending-upload-row .remove-upload:hover {
589
+ color: var(--text-color);
590
+ }
591
+
592
+ /* Floating Message Input Panel (Matches GLM-5.2 Layout) */
593
+ .input-panel {
594
+ width: 100%;
595
+ max-width: 800px;
596
+ margin: 0 auto 32px auto;
597
+ padding: 0 24px;
598
+ z-index: 5;
599
+ }
600
+
601
+ .input-container {
602
+ background-color: var(--input-bg);
603
+ border: 1px solid var(--input-border);
604
+ border-radius: 16px;
605
+ padding: 16px 20px;
606
+ box-shadow: var(--input-shadow);
607
+ display: flex;
608
+ flex-direction: column;
609
+ gap: 12px;
610
+ }
611
+
612
+ .textarea-wrapper {
613
+ width: 100%;
614
+ }
615
+
616
+ .chat-input {
617
+ width: 100%;
618
+ border: none;
619
+ outline: none;
620
+ background: transparent;
621
+ color: var(--text-color);
622
+ font-family: var(--font-sans);
623
+ font-size: 16px;
624
+ resize: none;
625
+ max-height: 200px;
626
+ height: 24px;
627
+ line-height: 1.5;
628
+ }
629
+
630
+ .chat-input::placeholder {
631
+ color: var(--text-muted);
632
+ opacity: 0.8;
633
+ }
634
+
635
+ /* Input Controls Panel */
636
+ .input-controls {
637
+ display: flex;
638
+ align-items: center;
639
+ justify-content: space-between;
640
+ width: 100%;
641
+ }
642
+
643
+ .controls-left {
644
+ display: flex;
645
+ align-items: center;
646
+ gap: 12px;
647
+ }
648
+
649
+ .action-circle-btn {
650
+ width: 32px;
651
+ height: 32px;
652
+ border-radius: 50%;
653
+ border: 1px solid var(--input-border);
654
+ background: transparent;
655
+ color: var(--text-color);
656
+ display: flex;
657
+ align-items: center;
658
+ justify-content: center;
659
+ cursor: pointer;
660
+ }
661
+
662
+ .action-circle-btn:hover {
663
+ background-color: var(--sidebar-active-bg);
664
+ }
665
+
666
+ /* Capsule Agent Button (Screenshot style) */
667
+ .agent-capsule {
668
+ display: flex;
669
+ align-items: center;
670
+ gap: 6px;
671
+ background-color: var(--capsule-bg);
672
+ border: 1px solid var(--capsule-border);
673
+ color: var(--capsule-text);
674
+ padding: 6px 14px;
675
+ border-radius: 20px;
676
+ font-family: var(--font-sans);
677
+ font-size: 13px;
678
+ font-weight: 500;
679
+ cursor: pointer;
680
+ user-select: none;
681
+ border: 1px solid var(--sidebar-border);
682
+ }
683
+
684
+ .agent-capsule:hover {
685
+ background-color: var(--sidebar-active-bg);
686
+ filter: brightness(0.95);
687
+ }
688
+
689
+ .agent-capsule svg {
690
+ color: var(--text-muted);
691
+ }
692
+
693
+ /* Send Circular Button (Screenshot style) */
694
+ .send-btn {
695
+ width: 32px;
696
+ height: 32px;
697
+ border-radius: 50%;
698
+ border: none;
699
+ background-color: var(--btn-disabled-bg);
700
+ color: var(--btn-disabled-color);
701
+ display: flex;
702
+ align-items: center;
703
+ justify-content: center;
704
+ cursor: pointer;
705
+ transition: all 0.2s ease;
706
+ }
707
+
708
+ .send-btn.active {
709
+ background-color: var(--btn-active-bg);
710
+ color: var(--btn-active-color);
711
+ }
712
+
713
+ .send-btn svg {
714
+ width: 16px;
715
+ height: 16px;
716
+ }
717
+
718
+ /* Blinking Terminal Cursor */
719
+ .terminal-cursor {
720
+ display: inline-block;
721
+ width: 8px;
722
+ height: 15px;
723
+ background-color: var(--cursor-color);
724
+ margin-left: 4px;
725
+ animation: blink 0.8s infinite;
726
+ vertical-align: middle;
727
+ }
728
+
729
+ /* Settings Drawer overlay */
730
+ .settings-drawer {
731
+ position: absolute;
732
+ left: 73px;
733
+ top: 0;
734
+ bottom: 0;
735
+ width: 320px;
736
+ background-color: var(--sidebar-bg);
737
+ border-right: 1px solid var(--sidebar-border);
738
+ z-index: 9;
739
+ box-shadow: 10px 0 30px rgba(0,0,0,0.02);
740
+ padding: 24px;
741
+ display: flex;
742
+ flex-direction: column;
743
+ gap: 20px;
744
+ transform: translateX(-105%);
745
+ transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
746
+ }
747
+
748
+ .settings-drawer.open {
749
+ transform: translateX(0);
750
+ }
751
+
752
+ .settings-header {
753
+ display: flex;
754
+ justify-content: space-between;
755
+ align-items: center;
756
+ margin-bottom: 8px;
757
+ }
758
+
759
+ .settings-title {
760
+ font-size: 18px;
761
+ font-weight: 600;
762
+ }
763
+
764
+ .close-drawer-btn {
765
+ background: transparent;
766
+ border: none;
767
+ color: var(--text-muted);
768
+ cursor: pointer;
769
+ }
770
+
771
+ .close-drawer-btn:hover {
772
+ color: var(--text-color);
773
+ }
774
+
775
+ .settings-group {
776
+ display: flex;
777
+ flex-direction: column;
778
+ gap: 8px;
779
+ }
780
+
781
+ .settings-label {
782
+ font-size: 13px;
783
+ font-weight: 500;
784
+ color: var(--text-muted);
785
+ }
786
+
787
+ .settings-input, .settings-select {
788
+ width: 100%;
789
+ background-color: var(--input-bg);
790
+ border: 1px solid var(--sidebar-border);
791
+ padding: 10px 12px;
792
+ border-radius: 8px;
793
+ color: var(--text-color);
794
+ font-family: var(--font-sans);
795
+ font-size: 14px;
796
+ outline: none;
797
+ }
798
+
799
+ .settings-input:focus, .settings-select:focus {
800
+ border-color: var(--text-color);
801
+ }
802
+
803
+ /* CLI Info Alert */
804
+ .cli-info-banner {
805
+ background-color: var(--accent-bg);
806
+ color: var(--text-color);
807
+ border: 1px solid var(--sidebar-border);
808
+ border-radius: 8px;
809
+ padding: 12px;
810
+ font-size: 12px;
811
+ line-height: 1.5;
812
+ font-family: var(--font-mono);
813
+ }
814
+
815
+ /* Matrix Canvas */
816
+ #matrix-canvas {
817
+ position: absolute;
818
+ top: 0;
819
+ left: 0;
820
+ width: 100%;
821
+ height: 100%;
822
+ pointer-events: none;
823
+ z-index: 0;
824
+ opacity: 0.12;
825
+ display: none;
826
+ }
827
+
828
+ /* Animations */
829
+ @keyframes blink {
830
+ 0%, 49% { opacity: 1; }
831
+ 50%, 100% { opacity: 0; }
832
+ }
833
+
834
+ @keyframes fadeInUp {
835
+ from {
836
+ opacity: 0;
837
+ transform: translateY(20px);
838
+ }
839
+ to {
840
+ opacity: 1;
841
+ transform: translateY(0);
842
+ }
843
+ }
844
+
845
+ @keyframes fadeIn {
846
+ from { opacity: 0; }
847
+ to { opacity: 1; }
848
+ }
849
+
850
+ /* Scrollbar styling */
851
+ ::-webkit-scrollbar {
852
+ width: 6px;
853
+ height: 6px;
854
+ }
855
+ ::-webkit-scrollbar-track {
856
+ background: transparent;
857
+ }
858
+ ::-webkit-scrollbar-thumb {
859
+ background: var(--sidebar-border);
860
+ border-radius: 4px;
861
+ }
862
+ ::-webkit-scrollbar-thumb:hover {
863
+ background: var(--text-muted);
864
+ }
865
+
866
+ /* System notification log */
867
+ .system-log {
868
+ color: #eab308;
869
+ font-style: italic;
870
+ }
871
+
872
+ .classic-terminal-mode .system-log {
873
+ color: #ffb000;
874
+ }
875
+
876
+ /* Welcoming terminal dashboard box */
877
+ .welcome-terminal-box {
878
+ background-color: var(--input-bg);
879
+ border: 1px solid var(--input-border);
880
+ border-radius: 12px;
881
+ width: 100%;
882
+ max-width: 480px;
883
+ box-shadow: var(--input-shadow);
884
+ text-align: left;
885
+ margin: 20px 0;
886
+ overflow: hidden;
887
+ animation: fadeInUp 0.8s ease 0.2s both;
888
+ }
889
+
890
+ .welcome-terminal-header {
891
+ background-color: var(--sidebar-bg);
892
+ border-bottom: 1px solid var(--sidebar-border);
893
+ padding: 10px 16px;
894
+ display: flex;
895
+ align-items: center;
896
+ gap: 6px;
897
+ }
898
+
899
+ .terminal-dot {
900
+ width: 8px;
901
+ height: 8px;
902
+ border-radius: 50%;
903
+ display: inline-block;
904
+ }
905
+
906
+ .terminal-dot.red { background-color: #ef4444; }
907
+ .terminal-dot.yellow { background-color: #f59e0b; }
908
+ .terminal-dot.green { background-color: #10b981; }
909
+
910
+ .terminal-title {
911
+ font-family: var(--font-mono);
912
+ font-size: 11px;
913
+ color: var(--text-muted);
914
+ margin-left: 8px;
915
+ flex-grow: 1;
916
+ text-align: center;
917
+ margin-right: 30px;
918
+ }
919
+
920
+ .welcome-terminal-content {
921
+ padding: 16px 20px;
922
+ margin: 0;
923
+ overflow: auto;
924
+ }
925
+
926
+ .welcome-terminal-content code {
927
+ font-family: var(--font-mono);
928
+ font-size: 13px;
929
+ color: var(--text-terminal);
930
+ line-height: 1.6;
931
+ }
932
+
933
+ /* Chat Status Bar styles */
934
+ .chat-status-bar {
935
+ background-color: var(--sidebar-bg);
936
+ border-bottom: 1px solid var(--sidebar-border);
937
+ padding: 10px 24px;
938
+ font-size: 12px;
939
+ display: none;
940
+ align-items: center;
941
+ justify-content: space-between;
942
+ color: var(--text-muted);
943
+ z-index: 3;
944
+ user-select: none;
945
+ }
946
+
947
+ .chat-status-bar .status-item {
948
+ display: flex;
949
+ align-items: center;
950
+ gap: 6px;
951
+ }
952
+
953
+ .chat-status-bar .status-dot {
954
+ width: 6px;
955
+ height: 6px;
956
+ border-radius: 50%;
957
+ background-color: #10b981;
958
+ }
959
+
960
+ /* Message styling variations based on Mode */
961
+ .message-row.style-standard {
962
+ font-family: var(--font-sans);
963
+ font-size: 15px;
964
+ line-height: 1.6;
965
+ }
966
+
967
+ .message-row.style-standard .prompt-header {
968
+ margin-bottom: 6px;
969
+ }
970
+
971
+ .message-row.style-standard .standard-sender-user {
972
+ font-weight: 600;
973
+ font-size: 13px;
974
+ color: var(--text-color);
975
+ background-color: var(--sidebar-active-bg);
976
+ padding: 2px 8px;
977
+ border-radius: 4px;
978
+ }
979
+
980
+ .message-row.style-standard .standard-sender-system {
981
+ font-weight: 600;
982
+ font-size: 13px;
983
+ color: var(--text-color);
984
+ }
985
+
986
+ .message-row.style-standard .prompt-text {
987
+ padding-left: 0;
988
+ color: var(--text-color);
989
+ font-size: 15px;
990
+ }
991
+
992
+ .message-row.style-standard .message-content {
993
+ padding-left: 0;
994
+ border-left: none;
995
+ color: var(--text-color);
996
+ font-size: 15px;
997
+ }
998
+
999
+ .message-row.style-standard .message-content p {
1000
+ margin-bottom: 12px;
1001
+ }
1002
+
1003
+ /* Standard Mode code blocks styling */
1004
+ .message-row.style-standard .message-content pre {
1005
+ background-color: var(--capsule-bg);
1006
+ color: var(--text-color);
1007
+ border: 1px solid var(--sidebar-border);
1008
+ }
1009
+
1010
+ .message-row.style-standard .message-content code {
1011
+ color: inherit;
1012
+ }
1013
+
1014
+ .message-row.style-terminal {
1015
+ font-family: var(--font-mono);
1016
+ font-size: 14px;
1017
+ line-height: 1.6;
1018
+ }
1019
+ </style>
1020
+ </head>
1021
+ <body>
1022
+
1023
+ <!-- Matrix Code Rain Canvas -->
1024
+ <canvas id="matrix-canvas"></canvas>
1025
+
1026
+ <!-- Left Sidebar (Screenshots structure) -->
1027
+ <nav class="sidebar">
1028
+ <div class="sidebar-top">
1029
+ <!-- Brand GLM Logo (Minimalist Boxed Header) -->
1030
+ <button class="brand-btn" id="home-brand-btn" onclick="goToHome()" title="GLM 5.2 Workspace" style="font-size: 11px; font-weight: 800; letter-spacing: -0.5px;">GLM</button>
1031
+
1032
+ <!-- Hugging Face Sign-In / Profile -->
1033
+ <button class="sidebar-btn" id="hf-auth-btn" onclick="handleHfAuthClick()" data-tooltip="Sign in with Hugging Face" style="font-size: 20px; line-height: 1;">🤗</button>
1034
+
1035
+ <button class="sidebar-btn" id="new-chat-btn" onclick="clearSession()" data-tooltip="New Workspace Chat">
1036
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1037
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
1038
+ <path d="M18.5 2.5a2.121 2.121 0 1 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
1039
+ </svg>
1040
+ </button>
1041
+
1042
+
1043
+ </div>
1044
+
1045
+ <div class="sidebar-bottom">
1046
+ <!-- Theme Toggler (Sun/Moon/Retro CLI Terminal) -->
1047
+ <button class="sidebar-btn" id="theme-btn" onclick="cycleTheme()" data-tooltip="Cycle Theme Mode">
1048
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1049
+ <path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/>
1050
+ <circle cx="12" cy="12" r="4"/>
1051
+ </svg>
1052
+ </button>
1053
+
1054
+ <!-- Configuration / Settings Cog -->
1055
+ <button class="sidebar-btn" id="settings-btn" onclick="toggleSettings()" data-tooltip="Settings & Credentials">
1056
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1057
+ <circle cx="12" cy="12" r="3"/>
1058
+ <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
1059
+ </svg>
1060
+ </button>
1061
+ </div>
1062
+ </nav>
1063
+
1064
+ <!-- Sliding Settings Drawer -->
1065
+ <div class="settings-drawer" id="settings-drawer">
1066
+ <div class="settings-header">
1067
+ <span class="settings-title">Configuration Drawer</span>
1068
+ <button class="close-drawer-btn" onclick="toggleSettings()">
1069
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1070
+ <line x1="18" y1="6" x2="6" y2="18"/>
1071
+ <line x1="6" y1="6" x2="18" y2="18"/>
1072
+ </svg>
1073
+ </button>
1074
+ </div>
1075
+
1076
+
1077
+
1078
+
1079
+
1080
+ <div class="settings-group" id="hf-account-group" style="display:none;">
1081
+ <label class="settings-label">Hugging Face Account</label>
1082
+ <div class="cli-info-banner" style="display:flex; align-items:center; gap:10px; flex-wrap:wrap;">
1083
+ <img id="hf-account-avatar" src="" alt="" style="width:28px; height:28px; border-radius:50%; flex-shrink:0;">
1084
+ <span style="font-size:13px;">
1085
+ Signed in as <strong id="hf-account-name"></strong>
1086
+ </span>
1087
+ <button onclick="hfLogout()" style="margin-left:auto; font-size:11px; padding:4px 10px; border-radius:6px; border:1px solid var(--sidebar-border); background:transparent; color:var(--text-color); cursor:pointer;">Sign out</button>
1088
+ </div>
1089
+ </div>
1090
+
1091
+ <div class="settings-group">
1092
+ <label class="settings-label" for="temperature-input">Temperature (Creativity)</label>
1093
+ <input type="range" id="temperature-input" min="0.1" max="1.5" step="0.1" value="0.7" oninput="document.getElementById('temp-val').innerText = this.value; saveConfig()">
1094
+ <span id="temp-val" style="font-size:12px; margin-top:2px;">0.7</span>
1095
+ </div>
1096
+
1097
+ <div class="settings-group">
1098
+ <label class="settings-label" for="system-input">System Instruction / Agent Prompt</label>
1099
+ <textarea id="system-input" class="settings-input" rows="4" placeholder="You are a helpful assistant..." onchange="saveConfig()"></textarea>
1100
+ </div>
1101
+
1102
+ <div class="cli-info-banner">
1103
+ <strong>Terminal Mode Info:</strong><br>
1104
+ You can type commands directly in the chat bar!<br>
1105
+ • <code>/clear</code>: Reset chat<br>
1106
+ • <code>/theme</code>: Cycle styles<br>
1107
+ • <code>/help</code>: Display CLI manual<br>
1108
+ • <code>/matrix</code>: Toggle digital rain
1109
+ </div>
1110
+ </div>
1111
+
1112
+ <!-- Main Pane -->
1113
+ <div class="workspace">
1114
+ <!-- SVG Watermark Background 5.2 (Double Stencil Outline) -->
1115
+ <div class="watermark-container">
1116
+ <svg class="watermark-svg" viewBox="0 0 100 100">
1117
+ <path d="M 30,25 H 70 V 48 H 30 V 75 H 70" />
1118
+ <path d="M 35,30 H 65 V 43 H 35 V 70 H 65" stroke-dasharray="2, 2" />
1119
+ </svg>
1120
+ </div>
1121
+
1122
+ <!-- Welcome Initial Panel -->
1123
+ <div class="welcome-panel" id="welcome-panel">
1124
+ <h1 class="welcome-title">GLM 5.2</h1>
1125
+ <p class="welcome-subtitle" style="font-family: var(--font-serif); font-size: 24px; margin-bottom: 12px;">Create anything you can imagine</p>
1126
+
1127
+ <!-- Sleek terminal status dashboard inside welcome screen -->
1128
+ <div class="welcome-terminal-box">
1129
+ <div class="welcome-terminal-header">
1130
+ <span class="terminal-dot red"></span>
1131
+ <span class="terminal-dot yellow"></span>
1132
+ <span class="terminal-dot green"></span>
1133
+ <span class="terminal-title">[email protected]: ~</span>
1134
+ </div>
1135
+ <pre class="welcome-terminal-content"><code>system_boot_sequence: SUCCESS
1136
+ model_identity : zai-org/GLM-5.2:fireworks-ai
1137
+ connection_state : ONLINE
1138
+ stream_completions : ACTIVE
1139
+
1140
+ Type a query or /help to interact with GLM 5.2...</code></pre>
1141
+ </div>
1142
+
1143
+ <p class="welcome-subtext" style="font-size: 13px; color: var(--text-muted); margin-top: 8px;">Interact with GLM 5.2 and explore the boundless creative world</p>
1144
+ </div>
1145
+
1146
+ <!-- Chat Status Bar -->
1147
+ <div class="chat-status-bar" id="chat-status-bar">
1148
+ <span class="status-item"><span class="status-dot"></span> GLM 5.2 CLI Session</span>
1149
+ <span class="status-item font-mono" style="font-family: var(--font-mono)">Model: zai-org/GLM-5.2:fireworks-ai</span>
1150
+ <span class="status-item" id="status-agent-mode">Mode: Standard</span>
1151
+ </div>
1152
+
1153
+ <!-- Chat Logs Console -->
1154
+ <div class="chat-container" id="chat-container"></div>
1155
+
1156
+ <!-- Input Bar (Matches Floating GLM-5.2 Layout) -->
1157
+ <div class="input-panel">
1158
+ <div class="input-container">
1159
+ <div class="pending-upload-row" id="pending-upload-row">
1160
+ <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/></svg>
1161
+ <span id="pending-upload-name"></span>
1162
+ <span class="remove-upload" id="remove-upload-btn" onclick="clearPendingUpload()">&times;</span>
1163
+ </div>
1164
+ <div class="textarea-wrapper">
1165
+ <textarea
1166
+ id="chat-input"
1167
+ class="chat-input"
1168
+ placeholder="Send a Message"
1169
+ rows="1"
1170
+ oninput="adjustInputHeight(this); checkInputLength(this)"
1171
+ onkeydown="handleKeyPress(event)"
1172
+ ></textarea>
1173
+ </div>
1174
+
1175
+ <div class="input-controls">
1176
+ <div class="controls-left">
1177
+ <!-- Agent pill button (Standard / Terminal toggle) -->
1178
+ <div class="agent-capsule" id="agent-capsule" onclick="toggleAgentMode()">
1179
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
1180
+ <polyline points="4 17 10 11 4 5"/>
1181
+ <line x1="12" y1="19" x2="20" y2="19"/>
1182
+ </svg>
1183
+ <span id="agent-pill-label">Standard Mode</span>
1184
+ </div>
1185
+ <!-- Attach file button -->
1186
+ <button class="attach-btn" id="attach-btn" onclick="document.getElementById('file-upload-input').click()" title="Attach a file to the workspace">
1187
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/></svg>
1188
+ </button>
1189
+ <input type="file" id="file-upload-input" hidden onchange="handleFileUpload(event)">
1190
+ </div>
1191
+
1192
+ <!-- Send Up-Arrow Button -->
1193
+ <button class="send-btn" id="send-btn" onclick="submitChat()" disabled>
1194
+ <!-- Rounded arrow up -->
1195
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
1196
+ <line x1="12" y1="19" x2="12" y2="5"/>
1197
+ <polyline points="5 12 12 5 19 12"/>
1198
+ </svg>
1199
+ </button>
1200
+ </div>
1201
+ </div>
1202
+ </div>
1203
+ </div>
1204
+
1205
+ <!-- Scripting and logic -->
1206
+ <script>
1207
+ // State variables
1208
+ let activeAgentMode = 'standard'; // standard, terminal
1209
+ let currentTheme = 'light'; // light, dark, classic-terminal
1210
+ let matrixInterval = null; // Digital rain interval reference
1211
+ let hfUser = null; // Logged-in Hugging Face user profile (null = signed out)
1212
+ let pendingUpload = null; // {name, size} staged via the paperclip button, attached to the next message
1213
+ let historyHydrated = false; // guards against re-hydrating the transcript more than once per session
1214
+
1215
+ // On Page Load: Hydrate settings from local storage
1216
+ window.addEventListener('DOMContentLoaded', () => {
1217
+
1218
+
1219
+
1220
+
1221
+ const savedTemp = localStorage.getItem('temp');
1222
+ if (savedTemp) {
1223
+ document.getElementById('temperature-input').value = savedTemp;
1224
+ document.getElementById('temp-val').innerText = savedTemp;
1225
+ }
1226
+
1227
+ const savedSystem = localStorage.getItem('system_prompt');
1228
+ if (savedSystem) {
1229
+ document.getElementById('system-input').value = savedSystem;
1230
+ }
1231
+
1232
+ const savedTheme = localStorage.getItem('theme');
1233
+ if (savedTheme) {
1234
+ applyTheme(savedTheme);
1235
+ }
1236
+
1237
+ // Hydrate Hugging Face auth state from the backend session.
1238
+ hydrateHfAuth();
1239
+ });
1240
+
1241
+ // Refresh auth state instantly when the sign-in popup signals completion.
1242
+ window.addEventListener('message', (event) => {
1243
+ if (event && event.data && event.data.type === 'hf-auth-complete') {
1244
+ hydrateHfAuth();
1245
+ }
1246
+ });
1247
+
1248
+ // Fetch the logged-in user (if any) from the backend session and update the UI.
1249
+ async function hydrateHfAuth() {
1250
+ try {
1251
+ const res = await fetch('/me');
1252
+ const data = await res.json();
1253
+ if (data && data.user) {
1254
+ setHfUser(data.user);
1255
+ } else {
1256
+ setHfUser(null);
1257
+ }
1258
+ } catch (err) {
1259
+ console.error('Failed to hydrate HF auth:', err);
1260
+ setHfUser(null);
1261
+ }
1262
+ }
1263
+
1264
+ // Reflect the current HF auth state across the UI.
1265
+ function setHfUser(user) {
1266
+ hfUser = user;
1267
+ const authBtn = document.getElementById('hf-auth-btn');
1268
+ const accountGroup = document.getElementById('hf-account-group');
1269
+
1270
+ if (user) {
1271
+ authBtn.classList.add('active');
1272
+ authBtn.setAttribute('data-tooltip', `Signed in as ${user.preferred_username || user.name || 'HF user'} — click to sign out`);
1273
+ if (accountGroup) {
1274
+ accountGroup.style.display = 'block';
1275
+ const name = document.getElementById('hf-account-name');
1276
+ const avatar = document.getElementById('hf-account-avatar');
1277
+ if (name) name.textContent = `@${user.preferred_username || user.name}`;
1278
+ if (avatar && user.picture) {
1279
+ avatar.src = user.picture;
1280
+ avatar.style.display = 'inline-block';
1281
+ } else if (avatar) {
1282
+ avatar.style.display = 'none';
1283
+ }
1284
+ }
1285
+ // Restore this person's saved conversation + workspace from the
1286
+ // server the first time we see them signed in this session.
1287
+ if (!historyHydrated) {
1288
+ historyHydrated = true;
1289
+ hydrateHistory();
1290
+ }
1291
+ } else {
1292
+ historyHydrated = false;
1293
+ authBtn.classList.remove('active');
1294
+ authBtn.setAttribute('data-tooltip', 'Sign in with Hugging Face');
1295
+ if (accountGroup) accountGroup.style.display = 'none';
1296
+ }
1297
+ }
1298
+
1299
+ // Sidebar button: sign in (redirect) or, if already signed in, sign out.
1300
+ function handleHfAuthClick() {
1301
+ if (hfUser) {
1302
+ hfLogout();
1303
+ } else {
1304
+ // Open the HF authorize page in a new tab to avoid iframe cookie
1305
+ // issues. Gradio's callback redirects the popup back to "/" (our
1306
+ // app). We hold the popup handle and poll /me; the moment the user
1307
+ // is signed in we refresh this tab's state and close the popup
1308
+ // ourselves (window.close() from the opener works across the
1309
+ // cross-origin OAuth hops, unlike window.close() inside the popup).
1310
+ const popup = window.open('/login/huggingface', '_blank');
1311
+ let checksLeft = 120; // ~60s
1312
+ const checkAuth = setInterval(async () => {
1313
+ checksLeft -= 1;
1314
+ // If the popup was closed early (e.g. user cancelled), stop polling.
1315
+ if (popup && popup.closed) {
1316
+ clearInterval(checkAuth);
1317
+ hydrateHfAuth();
1318
+ return;
1319
+ }
1320
+ try {
1321
+ const res = await fetch('/me');
1322
+ const data = await res.json();
1323
+ if (data && data.user) {
1324
+ setHfUser(data.user);
1325
+ try { popup.close(); } catch (e) {}
1326
+ clearInterval(checkAuth);
1327
+ return;
1328
+ }
1329
+ } catch (e) { /* ignore transient fetch errors */ }
1330
+ if (checksLeft <= 0) clearInterval(checkAuth);
1331
+ }, 500);
1332
+ }
1333
+ }
1334
+
1335
+ // Sign out of HF and refresh auth state.
1336
+ async function hfLogout() {
1337
+ try {
1338
+ await fetch('/logout');
1339
+ } catch (err) {
1340
+ console.error('Logout request failed:', err);
1341
+ }
1342
+ setHfUser(null);
1343
+ }
1344
+
1345
+ // Config Drawer toggles
1346
+ function toggleSettings() {
1347
+ const drawer = document.getElementById('settings-drawer');
1348
+ drawer.classList.toggle('open');
1349
+ }
1350
+
1351
+ function saveConfig() {
1352
+ const temp = document.getElementById('temperature-input').value;
1353
+ const system = document.getElementById('system-input').value.trim();
1354
+
1355
+ localStorage.setItem('temp', temp);
1356
+ localStorage.setItem('system_prompt', system);
1357
+ }
1358
+
1359
+ // Navigation back home
1360
+ function goToHome() {
1361
+ document.getElementById('welcome-panel').style.display = 'flex';
1362
+ document.getElementById('chat-container').style.display = 'none';
1363
+ document.getElementById('chat-status-bar').style.display = 'none';
1364
+ }
1365
+
1366
+ // Clear Workspace History (both the visible transcript and the
1367
+ // server-side memory it's persisted to -- files already created in
1368
+ // the workspace are left alone, this only forgets the conversation).
1369
+ function clearSession() {
1370
+ const container = document.getElementById('chat-container');
1371
+ container.innerHTML = '';
1372
+ clearPendingUpload();
1373
+
1374
+ if (hfUser) {
1375
+ fetch('/api/history/clear', { method: 'POST' }).catch((err) => {
1376
+ console.error('Failed to clear server-side history:', err);
1377
+ });
1378
+ }
1379
+
1380
+ // Log local terminal reset
1381
+ appendLocalLog("Session reset. Terminal memory buffer cleared.");
1382
+ goToHome();
1383
+ }
1384
+
1385
+ // Append locally printed logs (for CLI actions)
1386
+ function appendLocalLog(text) {
1387
+ const container = document.getElementById('chat-container');
1388
+
1389
+ // Check if welcome panel is visible, hide it
1390
+ if (document.getElementById('welcome-panel').style.display !== 'none') {
1391
+ document.getElementById('welcome-panel').style.display = 'none';
1392
+ container.style.display = 'flex';
1393
+ document.getElementById('chat-status-bar').style.display = 'flex';
1394
+ }
1395
+
1396
+ const logRow = document.createElement('div');
1397
+ logRow.className = 'message-row';
1398
+ logRow.innerHTML = `
1399
+ <div class="prompt-header">
1400
+ <span class="prompt-prefix-system">[email protected]:~$</span>
1401
+ <span class="prompt-command">info</span>
1402
+ </div>
1403
+ <div class="prompt-text system-log">${text}</div>
1404
+ `;
1405
+ container.appendChild(logRow);
1406
+ container.scrollTop = container.scrollHeight;
1407
+ }
1408
+
1409
+ // Adjusts input textarea height depending on characters typed
1410
+ function adjustInputHeight(textarea) {
1411
+ textarea.style.height = 'auto';
1412
+ textarea.style.height = (textarea.scrollHeight) + 'px';
1413
+ }
1414
+
1415
+ // Toggles the send button color and activity state based on input presence
1416
+ function checkInputLength(textarea) {
1417
+ const sendBtn = document.getElementById('send-btn');
1418
+ if (textarea.value.trim().length > 0) {
1419
+ sendBtn.classList.add('active');
1420
+ sendBtn.disabled = false;
1421
+ } else {
1422
+ sendBtn.classList.remove('active');
1423
+ sendBtn.disabled = true;
1424
+ }
1425
+ }
1426
+
1427
+ // Key Press handling: Shift+Enter inserts newline, Enter submits
1428
+ function handleKeyPress(event) {
1429
+ if (event.key === 'Enter' && !event.shiftKey) {
1430
+ event.preventDefault();
1431
+ submitChat();
1432
+ }
1433
+ }
1434
+
1435
+ // Toggles theme through cycle: Light -> Dark -> Retro CLI Terminal
1436
+ function cycleTheme() {
1437
+ if (currentTheme === 'light') {
1438
+ applyTheme('dark');
1439
+ } else if (currentTheme === 'dark') {
1440
+ applyTheme('classic-terminal');
1441
+ } else {
1442
+ applyTheme('light');
1443
+ }
1444
+ }
1445
+
1446
+ function applyTheme(themeName) {
1447
+ document.body.classList.remove('dark-mode', 'classic-terminal-mode');
1448
+ const matrixCanvas = document.getElementById('matrix-canvas');
1449
+
1450
+ // Stop matrix loop unless in classic-terminal-mode
1451
+ if (matrixInterval) {
1452
+ clearInterval(matrixInterval);
1453
+ matrixInterval = null;
1454
+ }
1455
+ matrixCanvas.style.display = 'none';
1456
+
1457
+ if (themeName === 'dark') {
1458
+ document.body.classList.add('dark-mode');
1459
+ currentTheme = 'dark';
1460
+ } else if (themeName === 'classic-terminal') {
1461
+ document.body.classList.add('classic-terminal-mode');
1462
+ currentTheme = 'classic-terminal';
1463
+ initMatrixRain();
1464
+ } else {
1465
+ currentTheme = 'light';
1466
+ }
1467
+
1468
+ localStorage.setItem('theme', currentTheme);
1469
+ }
1470
+
1471
+ // Toggle Agents (Terminal mode or Standard chat mode)
1472
+ function toggleAgentMode() {
1473
+ const pillLabel = document.getElementById('agent-pill-label');
1474
+ const systemTextarea = document.getElementById('system-input');
1475
+ const chatContainer = document.getElementById('chat-container');
1476
+
1477
+ if (activeAgentMode === 'standard') {
1478
+ activeAgentMode = 'terminal';
1479
+ pillLabel.innerText = "Terminal Mode";
1480
+ systemTextarea.value = "You are a senior system administrator and Unix terminal helper. Answer requests using command-line commands, scripts, code configurations, and wrap instructions in monospaced outputs.";
1481
+ appendLocalLog("Agent profile set to: Unix Terminal CLI assistant.");
1482
+ chatContainer.classList.add('terminal-layout');
1483
+ } else {
1484
+ activeAgentMode = 'standard';
1485
+ pillLabel.innerText = "Standard Mode";
1486
+ systemTextarea.value = "";
1487
+ appendLocalLog("Agent profile restored to: default general assistant.");
1488
+ chatContainer.classList.remove('terminal-layout');
1489
+ }
1490
+
1491
+ const statusAgentLabel = document.getElementById('status-agent-mode');
1492
+ if (statusAgentLabel) {
1493
+ statusAgentLabel.innerText = "Mode: " + (activeAgentMode === 'standard' ? 'Standard' : 'Terminal');
1494
+ }
1495
+
1496
+ const statusBar = document.getElementById('chat-status-bar');
1497
+ if (statusBar) {
1498
+ statusBar.style.display = (activeAgentMode === 'terminal' && chatContainer.style.display === 'flex') ? 'flex' : 'none';
1499
+ }
1500
+
1501
+ saveConfig();
1502
+ }
1503
+
1504
+ // Handle slash command local executes
1505
+ async function handleSlashCommand(commandStr) {
1506
+ const container = document.getElementById('chat-container');
1507
+ const cmd = commandStr.trim();
1508
+
1509
+ // Append user prompt action to screen
1510
+ const promptRow = document.createElement('div');
1511
+ promptRow.className = 'message-row';
1512
+ promptRow.innerHTML = `
1513
+ <div class="prompt-header">
1514
+ <span class="prompt-prefix-user">[email protected]:~$</span>
1515
+ <span class="prompt-command">run</span>
1516
+ </div>
1517
+ <div class="prompt-text">${cmd}</div>
1518
+ `;
1519
+ container.appendChild(promptRow);
1520
+
1521
+ if (cmd === '/clear') {
1522
+ clearSession();
1523
+ return;
1524
+ }
1525
+
1526
+ // Create response layout for command output
1527
+ const respRow = document.createElement('div');
1528
+ respRow.className = 'message-row';
1529
+
1530
+ let outputText = '';
1531
+
1532
+ if (cmd.startsWith('/help')) {
1533
+ outputText = `
1534
+ <strong>GLM-5.2 CLI SYSTEM MANUAL</strong>
1535
+ ----------------------------------------
1536
+ Here is a list of available command line functions:
1537
+ • <code>/help</code> - Prints this helper instruction table.
1538
+ • <code>/clear</code> - Resets active session logs and moves to welcome home panel.
1539
+ • <code>/theme</code> - Alternates theme context (Light -> Dark -> Classic green terminal).
1540
+ • <code>/matrix</code> - Toggles background matrix digital rain canvas.
1541
+ • <code>/system [prompt]</code> - Updates assistant's instruction context in config dynamically.
1542
+ • <code>/files</code> - Lists files currently in your private workspace, with download links.
1543
+ ----------------------------------------
1544
+ `;
1545
+ } else if (cmd.startsWith('/files')) {
1546
+ if (!hfUser) {
1547
+ outputText = 'Sign in with Hugging Face first to use your private workspace.';
1548
+ } else {
1549
+ try {
1550
+ const res = await fetch('/api/workspace');
1551
+ const data = await res.json();
1552
+ const files = (data && data.files) || [];
1553
+ if (files.length === 0) {
1554
+ outputText = 'Your workspace is empty right now. Ask the assistant to build something!';
1555
+ } else {
1556
+ const lines = files.map(f =>
1557
+ `• <code>${escapeHtml(f.name)}</code> — ${formatBytes(f.size)} — <a href="/api/files/${encodeURIComponent(f.name)}" download>download</a>`
1558
+ );
1559
+ const persistNote = data.persistent
1560
+ ? ''
1561
+ : '<br><em>Note: no persistent storage is attached to this Space, so these files reset if it restarts.</em>';
1562
+ outputText = `<strong>Workspace files</strong><br>${lines.join('<br>')}${persistNote}`;
1563
+ }
1564
+ } catch (err) {
1565
+ outputText = `Failed to list workspace files: ${escapeHtml(err.message)}`;
1566
+ }
1567
+ }
1568
+ } else if (cmd.startsWith('/theme')) {
1569
+ cycleTheme();
1570
+ outputText = `Theme toggled to: <strong>${currentTheme.toUpperCase()}</strong>`;
1571
+ } else if (cmd.startsWith('/matrix')) {
1572
+ const canvas = document.getElementById('matrix-canvas');
1573
+ if (canvas.style.display === 'block') {
1574
+ canvas.style.display = 'none';
1575
+ if (matrixInterval) clearInterval(matrixInterval);
1576
+ matrixInterval = null;
1577
+ outputText = "Digital matrix rain canvas disabled.";
1578
+ } else {
1579
+ canvas.style.display = 'block';
1580
+ initMatrixRain();
1581
+ outputText = "Digital matrix rain canvas enabled.";
1582
+ }
1583
+ } else if (cmd.startsWith('/system ')) {
1584
+ const sysText = cmd.substring(8);
1585
+ document.getElementById('system-input').value = sysText;
1586
+ saveConfig();
1587
+ outputText = `System prompt set to: <em>"${sysText}"</em>`;
1588
+
1589
+ } else {
1590
+ outputText = `Unknown system command: <code>${cmd}</code>. Type <code>/help</code> for list of available CLI commands.`;
1591
+ }
1592
+
1593
+ respRow.innerHTML = `
1594
+ <div class="prompt-header">
1595
+ <span class="prompt-prefix-system">[email protected]:~$</span>
1596
+ <span class="prompt-command">stdout</span>
1597
+ </div>
1598
+ <div class="message-content">${outputText}</div>
1599
+ `;
1600
+ container.appendChild(respRow);
1601
+ container.scrollTop = container.scrollHeight;
1602
+ }
1603
+
1604
+ // Submits user message to the API endpoint and streams the output
1605
+ async function submitChat() {
1606
+ const inputField = document.getElementById('chat-input');
1607
+ const sendBtn = document.getElementById('send-btn');
1608
+ const welcomePanel = document.getElementById('welcome-panel');
1609
+ const chatContainer = document.getElementById('chat-container');
1610
+
1611
+ const text = inputField.value.trim();
1612
+ if (!text) return;
1613
+
1614
+ // Gate inference behind Hugging Face sign-in: requests are billed to
1615
+ // the signed-in user's HF account, so we require a valid session.
1616
+ if (!hfUser) {
1617
+ inputField.value = '';
1618
+ inputField.style.height = 'auto';
1619
+ sendBtn.classList.remove('active');
1620
+ sendBtn.disabled = true;
1621
+ const needLoginRow = document.createElement('div');
1622
+ needLoginRow.className = `message-row assistant-row style-${activeAgentMode}`;
1623
+ needLoginRow.innerHTML = `
1624
+ <div class="prompt-header">
1625
+ <span class="prompt-prefix-system">[email protected]:~$</span>
1626
+ <span class="prompt-command">auth_required</span>
1627
+ </div>
1628
+ <div class="message-content">Sign in with Hugging Face to run inference billed to your own account. Click the <strong>HF</strong> button in the left sidebar to sign in.</div>
1629
+ `;
1630
+ chatContainer.appendChild(needLoginRow);
1631
+ if (welcomePanel.style.display !== 'none') {
1632
+ welcomePanel.style.display = 'none';
1633
+ chatContainer.style.display = 'flex';
1634
+ }
1635
+ chatContainer.scrollTop = chatContainer.scrollHeight;
1636
+ return;
1637
+ }
1638
+
1639
+ // Reset Input field
1640
+ inputField.value = '';
1641
+ inputField.style.height = 'auto';
1642
+ sendBtn.classList.remove('active');
1643
+ sendBtn.disabled = true;
1644
+
1645
+ // Check if user is typing a local command
1646
+ if (text.startsWith('/')) {
1647
+ // Check if welcome page is visible
1648
+ if (welcomePanel.style.display !== 'none') {
1649
+ welcomePanel.style.display = 'none';
1650
+ chatContainer.style.display = 'flex';
1651
+ if (activeAgentMode === 'terminal') {
1652
+ document.getElementById('chat-status-bar').style.display = 'flex';
1653
+ }
1654
+ }
1655
+ await handleSlashCommand(text);
1656
+ return;
1657
+ }
1658
+
1659
+ // Clean UI welcome state
1660
+ if (welcomePanel.style.display !== 'none') {
1661
+ welcomePanel.style.display = 'none';
1662
+ chatContainer.style.display = 'flex';
1663
+ if (activeAgentMode === 'terminal') {
1664
+ document.getElementById('chat-status-bar').style.display = 'flex';
1665
+ }
1666
+ }
1667
+
1668
+ // Fold any pending upload (paperclip attachment) into what gets
1669
+ // sent, so the agent knows the file is already in its workspace.
1670
+ const upload = pendingUpload;
1671
+ clearPendingUpload();
1672
+ let messageToSend = text;
1673
+ if (upload) {
1674
+ messageToSend = `[I attached a file to my workspace: "${upload.name}" (${formatBytes(upload.size)}), available at that relative path.]\n\n${text}`;
1675
+ }
1676
+
1677
+ // Append User message row
1678
+ const userRow = document.createElement('div');
1679
+ userRow.className = `message-row user-row style-${activeAgentMode}`;
1680
+ const userHeaderHtml = activeAgentMode === 'standard'
1681
+ ? `<div class="prompt-header"><span class="standard-sender-user">You</span></div>`
1682
+ : `<div class="prompt-header"><span class="prompt-prefix-user">[email protected]:~$</span><span class="prompt-command">prompt</span></div>`;
1683
+ const uploadChipHtml = upload
1684
+ ? `<div class="file-chip" style="margin-top:8px; cursor: default;">
1685
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.44 11.05l-9.19 9.19a6 6 0 01-8.49-8.49l9.19-9.19a4 4 0 015.66 5.66l-9.2 9.19a2 2 0 01-2.83-2.83l8.49-8.48"/></svg>
1686
+ <span>${escapeHtml(upload.name)}</span>
1687
+ <span class="file-chip-meta">${formatBytes(upload.size)}</span>
1688
+ </div>`
1689
+ : '';
1690
+ userRow.innerHTML = `${userHeaderHtml}<div class="prompt-text">${escapeHtml(text)}</div>${uploadChipHtml}`;
1691
+ chatContainer.appendChild(userRow);
1692
+ chatContainer.scrollTop = chatContainer.scrollHeight;
1693
+
1694
+ const temperatureVal = parseFloat(document.getElementById('temperature-input').value);
1695
+ const systemPromptVal = document.getElementById('system-input').value.trim();
1696
+
1697
+ // --- Per-turn rendering helpers -----------------------------------
1698
+ // The agent loop can run several model turns (reasoning -> tool
1699
+ // call -> tool result -> reasoning -> ... -> final answer) for one
1700
+ // user message. Each turn gets its own bubble; tool calls render
1701
+ // as distinct cards in between, exactly as they happen.
1702
+ let currentBubble = null; // { row, contentEl, raw }
1703
+
1704
+ function startNewBubble() {
1705
+ const streamId = 'stream-' + Date.now() + '-' + Math.random().toString(36).slice(2, 7);
1706
+ const row = document.createElement('div');
1707
+ row.className = `message-row assistant-row style-${activeAgentMode}`;
1708
+ if (activeAgentMode === 'standard') {
1709
+ row.innerHTML = `
1710
+ <div class="prompt-header"><span class="standard-sender-system">GLM 5.2</span></div>
1711
+ <div class="message-content" id="${streamId}"><span class="standard-loader">typing...</span></div>
1712
+ `;
1713
+ } else {
1714
+ row.innerHTML = `
1715
+ <div class="prompt-header"><span class="prompt-prefix-system">[email protected]:~$</span><span class="prompt-command">./run_glm_5.2_agent</span></div>
1716
+ <div class="message-content" id="${streamId}"><span class="terminal-cursor"></span></div>
1717
+ `;
1718
+ }
1719
+ chatContainer.appendChild(row);
1720
+ chatContainer.scrollTop = chatContainer.scrollHeight;
1721
+ currentBubble = { row, contentEl: document.getElementById(streamId), raw: '' };
1722
+ }
1723
+
1724
+ function appendToken(delta) {
1725
+ if (!currentBubble) startNewBubble();
1726
+ currentBubble.raw += delta;
1727
+ const cursor = activeAgentMode === 'terminal' ? '<span class="terminal-cursor"></span>' : '';
1728
+ currentBubble.contentEl.innerHTML = marked.parse(currentBubble.raw) + cursor;
1729
+ chatContainer.scrollTop = chatContainer.scrollHeight;
1730
+ }
1731
+
1732
+ // Finalizes the open bubble. If cleanText is given, that replaces
1733
+ // the raw streamed text (used to strip a tool_call JSON block back
1734
+ // out once we know it was there); an empty result removes the
1735
+ // bubble entirely rather than showing a blank reply.
1736
+ function finalizeBubble(cleanText) {
1737
+ if (!currentBubble) return;
1738
+ const text = (cleanText !== undefined && cleanText !== null) ? cleanText : currentBubble.raw;
1739
+ if (!text || !text.trim()) {
1740
+ currentBubble.row.remove();
1741
+ } else {
1742
+ currentBubble.contentEl.innerHTML = marked.parse(text);
1743
+ }
1744
+ currentBubble = null;
1745
+ }
1746
+
1747
+ function renderToolCall(tool, args) {
1748
+ const card = document.createElement('div');
1749
+ card.className = 'tool-card';
1750
+ let argsPreview = '';
1751
+ try { argsPreview = JSON.stringify(args || {}); } catch (e) { /* ignore */ }
1752
+ card.innerHTML = `
1753
+ <div class="tool-card-header">
1754
+ <span class="tool-name">${escapeHtml(tool || 'tool')}</span>
1755
+ <span class="tool-args-preview">${escapeHtml(argsPreview)}</span>
1756
+ <span class="tool-status pending">running</span>
1757
+ </div>
1758
+ <div class="tool-card-body" style="display:none;"></div>
1759
+ `;
1760
+ chatContainer.appendChild(card);
1761
+ chatContainer.scrollTop = chatContainer.scrollHeight;
1762
+ return card;
1763
+ }
1764
+
1765
+ function fillToolResult(card, payload) {
1766
+ if (!card) return;
1767
+ const statusEl = card.querySelector('.tool-status');
1768
+ const bodyEl = card.querySelector('.tool-card-body');
1769
+ statusEl.textContent = payload.success ? 'done' : 'failed';
1770
+ statusEl.classList.remove('pending');
1771
+ statusEl.classList.add(payload.success ? 'success' : 'fail');
1772
+
1773
+ let bodyHtml = '';
1774
+ if (payload.stdout) bodyHtml += `<pre class="tool-card-output">${escapeHtml(payload.stdout)}</pre>`;
1775
+ if (payload.stderr) bodyHtml += `<pre class="tool-card-output stderr-block">${escapeHtml(payload.stderr)}</pre>`;
1776
+ if (!payload.stdout && !payload.stderr) bodyHtml += `<div class="tool-card-file-line">(no output)</div>`;
1777
+ bodyEl.innerHTML = bodyHtml;
1778
+ bodyEl.style.display = 'block';
1779
+ chatContainer.scrollTop = chatContainer.scrollHeight;
1780
+ }
1781
+
1782
+ function renderFileChip(name, url, size) {
1783
+ const a = document.createElement('a');
1784
+ a.className = 'file-chip';
1785
+ a.href = url;
1786
+ a.setAttribute('download', name);
1787
+ a.innerHTML = `
1788
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
1789
+ <span>${escapeHtml(name)}</span>
1790
+ <span class="file-chip-meta">${size != null ? formatBytes(size) + ' — ' : ''}Download</span>
1791
+ `;
1792
+ chatContainer.appendChild(a);
1793
+ chatContainer.scrollTop = chatContainer.scrollHeight;
1794
+ }
1795
+
1796
+ let pendingToolCard = null;
1797
+
1798
+ try {
1799
+ const response = await fetch('/api/chat', {
1800
+ method: 'POST',
1801
+ headers: { 'Content-Type': 'application/json' },
1802
+ body: JSON.stringify({
1803
+ message: messageToSend,
1804
+ temperature: temperatureVal,
1805
+ system_prompt: systemPromptVal || null,
1806
+ })
1807
+ });
1808
+
1809
+ if (!response.ok) {
1810
+ throw new Error(`Connection failure: Status ${response.status}`);
1811
+ }
1812
+
1813
+ const reader = response.body.getReader();
1814
+ const decoder = new TextDecoder();
1815
+ let buffer = "";
1816
+
1817
+ while (true) {
1818
+ const { value, done } = await reader.read();
1819
+ if (done) break;
1820
+
1821
+ buffer += decoder.decode(value, { stream: true });
1822
+ const lines = buffer.split('\n');
1823
+
1824
+ // Retain trailing partial line in buffer
1825
+ buffer = lines.pop();
1826
+
1827
+ for (const line of lines) {
1828
+ const cleanLine = line.trim();
1829
+ if (!cleanLine || !cleanLine.startsWith('data: ')) continue;
1830
+
1831
+ const dataStr = cleanLine.substring(6);
1832
+ let payload;
1833
+ try {
1834
+ payload = JSON.parse(dataStr);
1835
+ } catch (err) {
1836
+ console.error('Failed to unpack payload chunk:', cleanLine, err);
1837
+ continue;
1838
+ }
1839
+
1840
+ switch (payload.type) {
1841
+ case 'turn_start':
1842
+ startNewBubble();
1843
+ break;
1844
+ case 'token':
1845
+ appendToken(payload.content || '');
1846
+ break;
1847
+ case 'tool_call':
1848
+ finalizeBubble(payload.text);
1849
+ pendingToolCard = renderToolCall(payload.tool, payload.args);
1850
+ break;
1851
+ case 'tool_result':
1852
+ if (payload.tool === null) {
1853
+ // Malformed tool_call JSON: no tool_call event preceded this,
1854
+ // so finalize the reasoning bubble and show the parse error inline.
1855
+ finalizeBubble(payload.text);
1856
+ const errCard = renderToolCall('tool_call (format error)', {});
1857
+ fillToolResult(errCard, payload);
1858
+ } else if (pendingToolCard) {
1859
+ fillToolResult(pendingToolCard, payload);
1860
+ pendingToolCard = null;
1861
+ }
1862
+ break;
1863
+ case 'file':
1864
+ renderFileChip(payload.name, payload.url, payload.size);
1865
+ break;
1866
+ case 'error':
1867
+ if (!currentBubble) startNewBubble();
1868
+ finalizeBubble(`**Error:** ${payload.message}`);
1869
+ break;
1870
+ case 'done':
1871
+ finalizeBubble(); // no-op if already finalized; else renders the final answer as-is
1872
+ break;
1873
+ default:
1874
+ break;
1875
+ }
1876
+ }
1877
+ }
1878
+
1879
+ finalizeBubble(); // safety net in case the stream ended without an explicit 'done'
1880
+
1881
+ } catch (err) {
1882
+ if (!currentBubble) startNewBubble();
1883
+ finalizeBubble(`**Connection error:** ${err.message}`);
1884
+ console.error(err);
1885
+ }
1886
+ }
1887
+
1888
+ // Helper to prevent tag insertions in text content
1889
+ function escapeHtml(text) {
1890
+ return text
1891
+ .replace(/&/g, "&amp;")
1892
+ .replace(/</g, "&lt;")
1893
+ .replace(/>/g, "&gt;")
1894
+ .replace(/"/g, "&quot;")
1895
+ .replace(/'/g, "&#039;");
1896
+ }
1897
+
1898
+ // Human-readable file sizes for chips and /files output
1899
+ function formatBytes(n) {
1900
+ if (n === null || n === undefined) return '';
1901
+ if (n < 1024) return n + ' B';
1902
+ if (n < 1024 * 1024) return (n / 1024).toFixed(1) + ' KB';
1903
+ return (n / (1024 * 1024)).toFixed(2) + ' MB';
1904
+ }
1905
+
1906
+ // Paperclip button: stage an uploaded file into the private workspace
1907
+ // ahead of the next message, so the agent can read/process it.
1908
+ async function handleFileUpload(event) {
1909
+ const input = event.target;
1910
+ const file = input.files && input.files[0];
1911
+ input.value = ''; // allow re-selecting the same file later
1912
+ if (!file) return;
1913
+
1914
+ if (!hfUser) {
1915
+ alert('Sign in with Hugging Face first to attach files to your workspace.');
1916
+ return;
1917
+ }
1918
+
1919
+ const formData = new FormData();
1920
+ formData.append('file', file);
1921
+ try {
1922
+ const res = await fetch('/api/upload', { method: 'POST', body: formData });
1923
+ const data = await res.json();
1924
+ if (!res.ok || data.error) {
1925
+ throw new Error(data.error || `Upload failed (status ${res.status})`);
1926
+ }
1927
+ pendingUpload = { name: data.name, size: data.size };
1928
+ const row = document.getElementById('pending-upload-row');
1929
+ document.getElementById('pending-upload-name').textContent = `${data.name} (${formatBytes(data.size)}) attached — will be sent with your next message`;
1930
+ row.classList.add('visible');
1931
+ } catch (err) {
1932
+ console.error('Upload failed:', err);
1933
+ alert('Upload failed: ' + err.message);
1934
+ }
1935
+ }
1936
+
1937
+ function clearPendingUpload() {
1938
+ pendingUpload = null;
1939
+ const row = document.getElementById('pending-upload-row');
1940
+ if (row) row.classList.remove('visible');
1941
+ const nameEl = document.getElementById('pending-upload-name');
1942
+ if (nameEl) nameEl.textContent = '';
1943
+ }
1944
+
1945
+ // Restore a signed-in user's saved conversation (and the tool calls,
1946
+ // results, and files that happened in it) from the server on load.
1947
+ async function hydrateHistory() {
1948
+ try {
1949
+ const res = await fetch('/api/history');
1950
+ const data = await res.json();
1951
+ const items = (data && data.history) || [];
1952
+ const chatContainer = document.getElementById('chat-container');
1953
+ const welcomePanel = document.getElementById('welcome-panel');
1954
+ chatContainer.innerHTML = '';
1955
+ if (items.length === 0) return; // nothing saved yet, stay on the welcome panel
1956
+
1957
+ welcomePanel.style.display = 'none';
1958
+ chatContainer.style.display = 'flex';
1959
+ if (activeAgentMode === 'terminal') {
1960
+ document.getElementById('chat-status-bar').style.display = 'flex';
1961
+ }
1962
+
1963
+ let lastToolArgs = {};
1964
+ for (const item of items) {
1965
+ try {
1966
+ if (item.type === 'assistant_text') {
1967
+ renderHistoryTextRow(chatContainer, 'assistant', item.text || '');
1968
+ lastToolArgs = (item.tool_call && item.tool_call.args) || {};
1969
+ } else if (item.type === 'user') {
1970
+ renderHistoryTextRow(chatContainer, 'user', item.text || '');
1971
+ } else if (item.type === 'tool_result') {
1972
+ renderHistoryToolResult(chatContainer, item, lastToolArgs);
1973
+ lastToolArgs = {};
1974
+ }
1975
+ } catch (itemErr) {
1976
+ console.error('Failed to render a history item, skipping it:', item, itemErr);
1977
+ }
1978
+ }
1979
+ chatContainer.scrollTop = chatContainer.scrollHeight;
1980
+ } catch (err) {
1981
+ console.error('Failed to hydrate chat history:', err);
1982
+ }
1983
+ }
1984
+
1985
+ function renderHistoryTextRow(container, who, text) {
1986
+ const clean = (text || '').trim();
1987
+ if (who === 'assistant' && !clean) return; // tool-call-only turn, nothing visible to replay
1988
+ const row = document.createElement('div');
1989
+ if (who === 'user') {
1990
+ row.className = `message-row user-row style-${activeAgentMode}`;
1991
+ const header = activeAgentMode === 'standard'
1992
+ ? `<div class="prompt-header"><span class="standard-sender-user">You</span></div>`
1993
+ : `<div class="prompt-header"><span class="prompt-prefix-user">[email protected]:~$</span><span class="prompt-command">prompt</span></div>`;
1994
+ row.innerHTML = `${header}<div class="prompt-text">${escapeHtml(clean)}</div>`;
1995
+ } else {
1996
+ row.className = `message-row assistant-row style-${activeAgentMode}`;
1997
+ const header = activeAgentMode === 'standard'
1998
+ ? `<div class="prompt-header"><span class="standard-sender-system">GLM 5.2</span></div>`
1999
+ : `<div class="prompt-header"><span class="prompt-prefix-system">[email protected]:~$</span><span class="prompt-command">./run_glm_5.2_agent</span></div>`;
2000
+ row.innerHTML = `${header}<div class="message-content">${marked.parse(clean)}</div>`;
2001
+ }
2002
+ container.appendChild(row);
2003
+ }
2004
+
2005
+ function renderHistoryToolResult(container, item, args) {
2006
+ const card = document.createElement('div');
2007
+ card.className = 'tool-card';
2008
+ let argsPreview = '';
2009
+ try { argsPreview = JSON.stringify(args || {}); } catch (e) { /* ignore */ }
2010
+ card.innerHTML = `
2011
+ <div class="tool-card-header">
2012
+ <span class="tool-name">${escapeHtml(item.tool || 'tool_call (format error)')}</span>
2013
+ <span class="tool-args-preview">${escapeHtml(argsPreview)}</span>
2014
+ <span class="tool-status ${item.success ? 'success' : 'fail'}">${item.success ? 'done' : 'failed'}</span>
2015
+ </div>
2016
+ <div class="tool-card-body"></div>
2017
+ `;
2018
+ const bodyEl = card.querySelector('.tool-card-body');
2019
+ let bodyHtml = '';
2020
+ if (item.stdout) bodyHtml += `<pre class="tool-card-output">${escapeHtml(item.stdout)}</pre>`;
2021
+ if (item.stderr) bodyHtml += `<pre class="tool-card-output stderr-block">${escapeHtml(item.stderr)}</pre>`;
2022
+ if (!item.stdout && !item.stderr) bodyHtml += `<div class="tool-card-file-line">(no output)</div>`;
2023
+ bodyEl.innerHTML = bodyHtml;
2024
+ container.appendChild(card);
2025
+
2026
+ if (item.file) {
2027
+ const a = document.createElement('a');
2028
+ a.className = 'file-chip';
2029
+ a.href = `/api/files/${encodeURIComponent(item.file)}`;
2030
+ a.setAttribute('download', item.file);
2031
+ a.innerHTML = `
2032
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
2033
+ <span>${escapeHtml(item.file)}</span>
2034
+ <span class="file-chip-meta">Download</span>
2035
+ `;
2036
+ container.appendChild(a);
2037
+ }
2038
+ }
2039
+
2040
+ // Digital Code Rain Matrix Animation
2041
+ function initMatrixRain() {
2042
+ const canvas = document.getElementById('matrix-canvas');
2043
+ const ctx = canvas.getContext('2d');
2044
+
2045
+ canvas.style.display = 'block';
2046
+
2047
+ // Resize canvas to cover viewport
2048
+ function resizeCanvas() {
2049
+ canvas.width = window.innerWidth;
2050
+ canvas.height = window.innerHeight;
2051
+ }
2052
+ resizeCanvas();
2053
+ window.addEventListener('resize', resizeCanvas);
2054
+
2055
+ // Matrix character list
2056
+ const matrixChars = "01010101XYZGLM52SYSTEMCLIWORKSPACECONNECT";
2057
+ const fontArr = matrixChars.split("");
2058
+ const fontSize = 14;
2059
+ const columns = canvas.width / fontSize;
2060
+
2061
+ // Track drop positions vertically
2062
+ const drops = [];
2063
+ for (let x = 0; x < columns; x++) {
2064
+ drops[x] = 1;
2065
+ }
2066
+
2067
+ function drawMatrix() {
2068
+ // Set light/dark overlay blending depending on theme
2069
+ ctx.fillStyle = currentTheme === 'classic-terminal' ? 'rgba(5, 8, 6, 0.04)' : 'rgba(9, 9, 11, 0.04)';
2070
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
2071
+
2072
+ // Set green text
2073
+ ctx.fillStyle = currentTheme === 'classic-terminal' ? '#33ff33' : '#a1a1aa';
2074
+ ctx.font = fontSize + "px monospace";
2075
+
2076
+ for (let i = 0; i < drops.length; i++) {
2077
+ const text = fontArr[Math.floor(Math.random() * fontArr.length)];
2078
+ ctx.fillText(text, i * fontSize, drops[i] * fontSize);
2079
+
2080
+ // Reset drop when hitting screen bottom boundary
2081
+ if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) {
2082
+ drops[i] = 0;
2083
+ }
2084
+ drops[i]++;
2085
+ }
2086
+ }
2087
+
2088
+ if (matrixInterval) clearInterval(matrixInterval);
2089
+ matrixInterval = setInterval(drawMatrix, 35);
2090
+ }
2091
+ </script>
2092
+ </body>
2093
+ </html>
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ openai
2
+ huggingface_hub
3
+ gradio[oauth]>=6.18.0
4
+ python-multipart