Upload 6 files
Browse files- .gitignore +2 -92
- README.md +11 -155
- agent.py +577 -0
- app.py +321 -0
- index.html +2093 -0
- requirements.txt +4 -0
.gitignore
CHANGED
|
@@ -1,94 +1,4 @@
|
|
| 1 |
-
# Byte-compiled / optimized / DLL files
|
| 2 |
__pycache__/
|
| 3 |
-
*.
|
| 4 |
-
|
| 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:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk:
|
| 7 |
-
|
|
|
|
|
|
|
| 8 |
pinned: false
|
| 9 |
-
|
| 10 |
hf_oauth: true
|
| 11 |
hf_oauth_expiration_minutes: 43200
|
| 12 |
hf_oauth_scopes:
|
| 13 |
-
-
|
| 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()">×</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, "&")
|
| 1892 |
+
.replace(/</g, "<")
|
| 1893 |
+
.replace(/>/g, ">")
|
| 1894 |
+
.replace(/"/g, """)
|
| 1895 |
+
.replace(/'/g, "'");
|
| 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
|