muralipala1504 commited on
Commit
9a5c4c1
·
1 Parent(s): b99a374

feat: Polish free version - Export, Clear, Loading indicators

Browse files
Files changed (3) hide show
  1. README.md +137 -86
  2. app.js +94 -28
  3. index.html +62 -3
README.md CHANGED
@@ -5,184 +5,235 @@
5
  ![FastAPI](https://img.shields.io/badge/FastAPI-async-green.svg)
6
  ![License](https://img.shields.io/badge/license-MIT-purple.svg)
7
 
8
- A lightweight web UI + backend for DeepShell — an infra‑only assistant for Linux SysAdmin, DevOps, IaC, Cloud, and Linux scripting tasks.
9
 
10
- DeepShell intentionally rejects out‑of‑domain queries. It focuses on actionable commands, troubleshooting steps, automation snippets, and config examples for real‑world ops.
11
 
12
  ---
13
 
14
  ## 🚀 What is DeepShell ModUI?
15
 
16
- DeepShell ModUI is a minimal, fast web application:
17
- - Frontend: a clean chat interface served by the backend itself
18
- - Backend: FastAPI service with pluggable LLM client (default: Groq) and guardrails
19
 
20
- Current default mode is “hard‑block” (demo/safe mode): any prompt returns an admin‑only scope message. This keeps the UI responsive while preventing unintended LLM usage. You can re‑enable LLM calls later with a one‑line change in deepshell-backend/deepshell/main.py.
 
 
 
 
 
21
 
22
  ---
23
 
24
  ## ✨ Features
25
 
26
- - Simple, single‑page chat UI
27
- - FastAPI backend serves UI assets (index.html, app.js, CSS)
28
- - Health/readiness endpoint
29
- - Polite block message for non‑admin use cases
30
- - Ready to wire Groq LLM for infra answers (opt‑in)
31
- - Works locally or over LAN
 
 
 
 
 
 
 
 
 
32
 
33
  ---
34
 
35
  ## 🛠️ Tech Stack
36
 
37
- - Frontend: HTML, CSS, Vanilla JS
38
- - Backend: FastAPI, Uvicorn
39
- - LLM Client: Groq (via deepshell-backend/deepshell/llm.py)
40
- - Python: 3.9+
41
 
42
  ---
43
 
44
  ## 🖼️ Preview
45
 
46
- Example screen (demo mode blocks general trivia; encourages admin/DevOps prompts).
47
-
48
  ![DeepShell UI Screenshot](docs/screenshot.png)
49
 
 
 
50
  ---
51
 
52
  ## ⚡ Quick Start
53
 
54
- Prerequisites
55
  - Python 3.9+
56
  - Git
57
- - Network: open TCP 8001 if accessing from another machine
58
 
59
- Clone and install
60
 
61
  ```bash
62
  git clone https://github.com/muralipala1504/deepshell_modui.git
63
  cd deepshell_modui
64
 
65
- # Install backend deps and package
66
  pip install -r deepshell-backend/requirements.txt
67
  pip install -e deepshell-backend/
68
 
69
- Run the app
70
-
71
- ```bash
72
-
73
- python run_deepshell.py
74
-
75
  ```
76
 
77
- Open in your browser
78
-
79
- Local: http://localhost:8001
80
- Remote/LAN: http://:8001
81
 
82
- Linux firewall (if remote access needed)
83
 
84
  ```bash
85
 
86
- sudo firewall-cmd --add-port=8001/tcp --permanent
87
- sudo firewall-cmd --reload
88
 
89
  ```
90
 
91
- Stop
92
 
93
- Ctrl + C in the terminal that’s running the server.
94
-
95
-
96
- 🔌 LLM Provider: Groq (optional)
97
-
98
- By default, the app is in “hard‑block” mode and does not call any LLM. When you’re ready to enable responses:
99
 
100
- Set your Groq API key in the environment
101
 
102
- Linux/macOS:
103
 
 
104
 
105
  ```bash
106
 
107
- export GROQ_API_KEY="your_groq_api_key"
108
 
109
  ```
110
 
111
- Windows PowerShell
112
 
113
- ```pshell
 
114
 
115
- $env:GROQ_API_KEY="your_groq_api_key"
116
 
117
- ```
118
 
119
- Enable LLM in the backend
 
120
 
121
- Edit deepshell-backend/deepshell/main.py
122
- In run_agent(), replace the hard‑block return with the provided LLM call (commented in the file).
123
-
124
- Restart the server.
125
 
126
- The LLM client code lives in:
127
 
128
- deepshell-backend/deepshell/llm.py (get_global_client / provider wiring)
129
 
130
 
131
- 🔧 Project Layout
132
 
133
  deepshell_modui/
134
- ├── app.js # Frontend script
135
- ├── index.html # Frontend page
136
  ├── services.css # Styling
137
  ├── run_deepshell.py # Convenience launcher
138
  ├── deepshell-backend/
139
  │ ├── deepshell/
140
  │ │ ├── __init__.py
141
- │ │ ├── main.py # FastAPI app (entrypoint)
142
- │ │ ├── __main__.py # Shim: python -m deepshell → main.main()
143
- │ │ ├── llm.py # LLM provider wiring (Groq)
144
- │ │ ├── handlers/ # Routers/handlers (extensible)
145
  │ │ └── utils.py # Helpers
146
  │ ├── requirements.txt
147
  │ └── setup.py
148
- ── docs/
149
- └── screenshot.png
 
 
 
150
 
151
- How to run (alternatives)
152
 
153
- python run_deepshell.py
154
- python -m deepshell # via main shim
155
- python -m deepshell.main
156
- uvicorn deepshell.main:app --host 0.0.0.0 --port 8001
 
 
157
 
158
 
159
- 🔒 Scope and Guardrails
160
 
161
- DeepShell is for:
 
 
 
 
 
 
162
 
163
- Linux SysAdmin commands and troubleshooting
164
- DevOps tooling (Docker, Kubernetes, Helm, etc.)
165
- IaC (Terraform, Ansible, CloudFormation)
166
- Cloud CLIs and services (AWS/GCP/Azure)
167
- Bash/Python scripting for ops
168
 
169
- Out‑of‑scope prompts are politely declined.
 
 
 
 
 
 
 
 
 
170
 
 
 
 
 
171
 
172
  🛣️ Roadmap
 
 
 
 
 
 
 
 
 
173
 
174
- Optional “allow‑list” mode for infra keywords
175
- Persistent chat history
176
- Export responses
177
- More helper modules (cloud/infra recipes)
178
- Multi‑LLM support via provider switch
179
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
  🤝 Contributing
182
 
183
- Issues and PRs are welcome. Please keep contributions focused on infra/DevOps/IaC use cases and simple, maintainable code.
 
 
 
 
 
184
 
 
185
 
186
- 📜 License
 
187
 
188
- MIT. See LICENSE for details.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  ![FastAPI](https://img.shields.io/badge/FastAPI-async-green.svg)
6
  ![License](https://img.shields.io/badge/license-MIT-purple.svg)
7
 
8
+ A modern, lightweight web UI for **DeepShell** — an AI-powered assistant specialized in Linux SysAdmin, DevOps, Docker, Kubernetes, IaC, and Cloud operations.
9
 
10
+ **Real-time streaming responses** **Session context** **Copy-to-clipboard** **Clean, professional UI**
11
 
12
  ---
13
 
14
  ## 🚀 What is DeepShell ModUI?
15
 
16
+ DeepShell ModUI is a fast, minimal web application that brings AI assistance to your infrastructure workflows:
 
 
17
 
18
+ - **Frontend**: Clean, responsive chat interface with real-time streaming
19
+ - **Backend**: FastAPI service with SSE (Server-Sent Events) support
20
+ - **LLM Provider**: Groq (llama-3.3-70b-versatile)
21
+ - **Smart Context**: Maintains conversation history for better responses
22
+
23
+ Perfect for DevOps engineers, SysAdmins, and Cloud architects who need quick, accurate commands and scripts.
24
 
25
  ---
26
 
27
  ## ✨ Features
28
 
29
+ ### **Current (Free Version)**
30
+ - **Real-time streaming responses** via SSE
31
+ - ✅ **Session context/history** - AI remembers your conversation
32
+ - **Copy buttons** on all code blocks
33
+ - **Markdown rendering** with syntax highlighting
34
+ - **Clean, professional UI** with dark theme
35
+ - ✅ **Health/readiness endpoints** for monitoring
36
+ - ✅ **Works locally or over LAN**
37
+
38
+ ### **Coming Soon (Paid Version)** 💎
39
+ - 🔜 One-click command execution with approval flow
40
+ - 🔜 Persistent chat history across sessions
41
+ - 🔜 Export conversations (Markdown/PDF)
42
+ - 🔜 Multi-host support (remote server connections)
43
+ - 🔜 Workflow automation and command chaining
44
 
45
  ---
46
 
47
  ## 🛠️ Tech Stack
48
 
49
+ - **Frontend**: HTML5, CSS3, Vanilla JavaScript (no frameworks!)
50
+ - **Backend**: FastAPI, Uvicorn, SSE
51
+ - **LLM**: Groq API (llama-3.3-70b-versatile)
52
+ - **Python**: 3.9+
53
 
54
  ---
55
 
56
  ## 🖼️ Preview
57
 
 
 
58
  ![DeepShell UI Screenshot](docs/screenshot.png)
59
 
60
+ *Clean interface with real-time streaming and copy buttons on every code block*
61
+
62
  ---
63
 
64
  ## ⚡ Quick Start
65
 
66
+ ### **Prerequisites**
67
  - Python 3.9+
68
  - Git
69
+ - Groq API key ([Get one free](https://console.groq.com))
70
 
71
+ ### **1. Clone and Install**
72
 
73
  ```bash
74
  git clone https://github.com/muralipala1504/deepshell_modui.git
75
  cd deepshell_modui
76
 
77
+ # Install backend dependencies
78
  pip install -r deepshell-backend/requirements.txt
79
  pip install -e deepshell-backend/
80
 
 
 
 
 
 
 
81
  ```
82
 
83
+ 2. Set Your Groq API Key
 
 
 
84
 
85
+ Linux/macOS:
86
 
87
  ```bash
88
 
89
+ export GROQ_API_KEY="your_groq_api_key_here"
 
90
 
91
  ```
92
 
93
+ Windows PowerShell:
94
 
95
+ ```pshell
 
 
 
 
 
96
 
97
+ $env:GROQ_API_KEY="your_groq_api_key_here"
98
 
99
+ ```
100
 
101
+ 3. Run the App
102
 
103
  ```bash
104
 
105
+ python run_deepshell.py
106
 
107
  ```
108
 
109
+ 4. Open in Browser
110
 
111
+ Local: http://localhost:8001
112
+ Remote/LAN: http://:8001
113
 
114
+ 5. Allow Firewall (if accessing remotely)
115
 
116
+ ```bash
117
 
118
+ sudo firewall-cmd --add-port=8001/tcp --permanent
119
+ sudo firewall-cmd --reload
120
 
121
+ ```
 
 
 
122
 
123
+ 6. Stop the Server
124
 
125
+ Press Ctrl + C in the terminal.
126
 
127
 
128
+ 🔧 Project Structure
129
 
130
  deepshell_modui/
131
+ ├── app.js # Frontend JavaScript (SSE, markdown, copy buttons)
132
+ ├── index.html # Main UI page
133
  ├── services.css # Styling
134
  ├── run_deepshell.py # Convenience launcher
135
  ├── deepshell-backend/
136
  │ ├── deepshell/
137
  │ │ ├── __init__.py
138
+ │ │ ├── __main__.py # FastAPI app with SSE endpoints
139
+ │ │ ├── llm.py # Groq LLM client wrapper
 
 
140
  │ │ └── utils.py # Helpers
141
  │ ├── requirements.txt
142
  │ └── setup.py
143
+ ── docs/
144
+ └── screenshot.png
145
+ └── README.md
146
+
147
+ 🎯 What DeepShell Excels At
148
 
149
+ DeepShell is specialized for:
150
 
151
+ 🐳 Docker: Container management, Dockerfile optimization, compose files
152
+ ☸️ Kubernetes: kubectl commands, YAML manifests, troubleshooting
153
+ 🔧 Linux SysAdmin: System commands, log analysis, performance tuning
154
+ 🏗️ IaC: Terraform, Ansible, CloudFormation scripts
155
+ ☁️ Cloud: AWS/GCP/Azure CLI commands and best practices
156
+ 📜 Scripting: Bash, Python automation for ops tasks
157
 
158
 
159
+ 🔒 Security & Safety
160
 
161
+ No automatic execution: All commands are displayed with copy buttons
162
+
163
+ User control: You decide what to run on your system
164
+
165
+ Session isolation: Each browser session has independent context
166
+
167
+ No data persistence: Conversations are in-memory only (free version)
168
 
 
 
 
 
 
169
 
170
+ 🚀 Alternative Run Methods
171
+
172
+
173
+ ```bash
174
+
175
+ # Method 1: Convenience script
176
+ python run_deepshell.py
177
+
178
+ # Method 2: Python module
179
+ python -m deepshell
180
 
181
+ # Method 3: Direct uvicorn
182
+ uvicorn deepshell.__main__:app --host 0.0.0.0 --port 8001
183
+
184
+ ```
185
 
186
  🛣️ Roadmap
187
+ Phase 1: Polish Free Version (Current)
188
+
189
+ SSE streaming responses
190
+ Session context/history
191
+ Copy buttons on code blocks
192
+ Clear chat button
193
+ Export chat history
194
+ Dark/Light theme toggle
195
+ Better error handling
196
 
197
+ Phase 2: Paid Features 💎
 
 
 
 
198
 
199
+ One-click command execution
200
+ Command approval flow
201
+ Persistent history (database)
202
+ Multi-session management
203
+ Workflow automation
204
+
205
+ Phase 3: Advanced Features
206
+
207
+ File upload (analyze logs, configs)
208
+ Multi-host support
209
+ Kubernetes dashboard integration
210
+ Custom prompt templates
211
 
212
  🤝 Contributing
213
 
214
+ Contributions are welcome! Please:
215
+
216
+ Keep code simple and maintainable
217
+ Focus on DevOps/SysAdmin/IaC use cases
218
+ Follow existing code style
219
+ Test thoroughly before submitting PRs
220
 
221
+ 📝 License
222
 
223
+ MIT License. See LICENSE for details.
224
+ 🙏 Acknowledgments
225
 
226
+ Groq for blazing-fast LLM inference
227
+ FastAPI for the excellent async framework
228
+ marked.js for markdown rendering
229
+ highlight.js for syntax highlighting
230
+
231
+ 📧 Contact
232
+
233
+ Maintainer: @muralipala1504
234
+
235
+ Issues: GitHub Issues
236
+
237
+ Built with ❤️ for DevOps engineers who value speed, accuracy, and control.
238
+
239
+ ---
app.js CHANGED
@@ -1,4 +1,4 @@
1
- // Plain Vanilla Chat Frontend with Copy Button, SSE Streaming, and Command Execution
2
  (function () {
3
  "use strict";
4
 
@@ -102,51 +102,42 @@
102
  return commands;
103
  }
104
 
105
- // NEW: Add copy buttons to all code blocks in a container
106
  function addCopyButtonsToCodeBlocks(container) {
107
- // Find all <pre> elements that don't already have a copy button
108
  container.querySelectorAll("pre").forEach((pre) => {
109
- // Skip if already has a copy button wrapper
110
  if (pre.parentElement && pre.parentElement.classList.contains("code-wrapper")) {
111
  return;
112
  }
113
 
114
- // Get the code content
115
  const codeEl = pre.querySelector("code");
116
  const code = codeEl ? codeEl.textContent : pre.textContent;
117
 
118
  if (!code || !code.trim()) return;
119
 
120
- // Create wrapper
121
  const wrapper = document.createElement("div");
122
  wrapper.className = "code-wrapper";
123
 
124
- // Create copy button
125
  const copyBtn = createCopyButton(code);
126
 
127
- // Insert wrapper before pre
128
  pre.parentNode.insertBefore(wrapper, pre);
129
-
130
- // Move pre into wrapper and add button
131
  wrapper.appendChild(copyBtn);
132
  wrapper.appendChild(pre);
133
  });
134
  }
135
 
136
- function appendMessage(container, sender, message, useMarkdown = false) {
137
  const msgDiv = document.createElement("div");
138
  msgDiv.classList.add("message");
 
139
 
140
- // Check for execute tags
141
  const commands = extractExecuteTags(message);
142
 
143
  if (commands.length > 0) {
144
- // Remove execute tags from display
145
  let displayText = message.replace(/<execute>.*?<\/execute>/gs, "");
146
 
147
  msgDiv.innerHTML = `<strong>${sender}:</strong>${useMarkdown ? renderMarkdown(displayText) : `<pre>${escapeHtml(displayText)}</pre>`}`;
148
 
149
- // Add execute buttons for each command
150
  commands.forEach(cmd => {
151
  const cmdWrapper = document.createElement("div");
152
  cmdWrapper.className = "command-wrapper";
@@ -169,20 +160,15 @@
169
  msgDiv.appendChild(cmdWrapper);
170
  });
171
  } else if (useMarkdown) {
172
- // Render markdown first
173
  msgDiv.innerHTML = `<strong>${sender}:</strong><div class="markdown-content">${renderMarkdown(String(message))}</div>`;
174
-
175
- // Add copy buttons to all code blocks
176
  addCopyButtonsToCodeBlocks(msgDiv);
177
  } else {
178
- // Plain text message
179
  msgDiv.innerHTML = `<strong>${sender}:</strong> <pre>${escapeHtml(String(message))}</pre>`;
180
  }
181
 
182
  container.appendChild(msgDiv);
183
  container.scrollTop = container.scrollHeight;
184
 
185
- // Syntax highlighting
186
  if (window.Prism) {
187
  try {
188
  Prism.highlightAllUnder(msgDiv);
@@ -195,8 +181,8 @@
195
  // ---------- Streaming UI ----------
196
  function createStreamingMessage(container, sender) {
197
  const msgDiv = document.createElement("div");
198
- msgDiv.classList.add("message");
199
- msgDiv.innerHTML = `<strong>${sender}:</strong> <span class="streaming-content"></span>`;
200
 
201
  const contentSpan = msgDiv.querySelector(".streaming-content");
202
 
@@ -211,7 +197,6 @@
211
  container.scrollTop = container.scrollHeight;
212
  },
213
  finalize: (text) => {
214
- // Replace with properly formatted message
215
  msgDiv.remove();
216
  appendMessage(container, sender, text, true);
217
  }
@@ -244,14 +229,13 @@
244
 
245
  buffer += decoder.decode(value, { stream: true });
246
 
247
- // SSE format: "data: {json}\n\n"
248
  const lines = buffer.split("\n\n");
249
- buffer = lines.pop(); // Keep incomplete data in buffer
250
 
251
  for (const line of lines) {
252
  if (!line.trim() || !line.startsWith("data: ")) continue;
253
 
254
- const jsonStr = line.substring(6); // Remove "data: " prefix
255
 
256
  try {
257
  const data = JSON.parse(jsonStr);
@@ -271,13 +255,12 @@
271
  }
272
  }
273
 
274
- // Finalize with whatever we got
275
  if (fullText) {
276
  streamUI.finalize(fullText);
277
  }
278
  } catch (err) {
279
  streamUI.msgDiv.remove();
280
- appendMessage(output, "Error", `${err.message}`);
281
  console.error("Streaming error:", err);
282
  } finally {
283
  if (sendBtn) sendBtn.disabled = false;
@@ -285,18 +268,80 @@
285
  }
286
  }
287
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
288
  // ---------- Chat wiring ----------
289
  document.addEventListener("DOMContentLoaded", () => {
290
  const form = document.getElementById("chat-form");
291
  const input = document.getElementById("chat-input");
292
  const output = document.getElementById("chat-output");
293
  const sendBtn = document.getElementById("chat-send");
 
 
294
 
295
  if (!form || !input || !output) {
296
  console.warn("Chat elements not found (chat-form/chat-input/chat-output).");
297
  return;
298
  }
299
 
 
300
  form.addEventListener("submit", (e) => {
301
  e.preventDefault();
302
  const prompt = input.value.trim();
@@ -306,7 +351,6 @@
306
  input.value = "";
307
  if (sendBtn) sendBtn.disabled = true;
308
 
309
- // Use streaming
310
  sendPromptStreaming(prompt, output, sendBtn, input);
311
  });
312
 
@@ -316,5 +360,27 @@
316
  form.requestSubmit();
317
  });
318
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  });
320
  })();
 
1
+ // Plain Vanilla Chat Frontend with Copy Button, SSE Streaming, Export, and Clear
2
  (function () {
3
  "use strict";
4
 
 
102
  return commands;
103
  }
104
 
105
+ // Add copy buttons to all code blocks in a container
106
  function addCopyButtonsToCodeBlocks(container) {
 
107
  container.querySelectorAll("pre").forEach((pre) => {
 
108
  if (pre.parentElement && pre.parentElement.classList.contains("code-wrapper")) {
109
  return;
110
  }
111
 
 
112
  const codeEl = pre.querySelector("code");
113
  const code = codeEl ? codeEl.textContent : pre.textContent;
114
 
115
  if (!code || !code.trim()) return;
116
 
 
117
  const wrapper = document.createElement("div");
118
  wrapper.className = "code-wrapper";
119
 
 
120
  const copyBtn = createCopyButton(code);
121
 
 
122
  pre.parentNode.insertBefore(wrapper, pre);
123
+
 
124
  wrapper.appendChild(copyBtn);
125
  wrapper.appendChild(pre);
126
  });
127
  }
128
 
129
+ function appendMessage(container, sender, message, useMarkdown = false, isError = false) {
130
  const msgDiv = document.createElement("div");
131
  msgDiv.classList.add("message");
132
+ if (isError) msgDiv.classList.add("error");
133
 
 
134
  const commands = extractExecuteTags(message);
135
 
136
  if (commands.length > 0) {
 
137
  let displayText = message.replace(/<execute>.*?<\/execute>/gs, "");
138
 
139
  msgDiv.innerHTML = `<strong>${sender}:</strong>${useMarkdown ? renderMarkdown(displayText) : `<pre>${escapeHtml(displayText)}</pre>`}`;
140
 
 
141
  commands.forEach(cmd => {
142
  const cmdWrapper = document.createElement("div");
143
  cmdWrapper.className = "command-wrapper";
 
160
  msgDiv.appendChild(cmdWrapper);
161
  });
162
  } else if (useMarkdown) {
 
163
  msgDiv.innerHTML = `<strong>${sender}:</strong><div class="markdown-content">${renderMarkdown(String(message))}</div>`;
 
 
164
  addCopyButtonsToCodeBlocks(msgDiv);
165
  } else {
 
166
  msgDiv.innerHTML = `<strong>${sender}:</strong> <pre>${escapeHtml(String(message))}</pre>`;
167
  }
168
 
169
  container.appendChild(msgDiv);
170
  container.scrollTop = container.scrollHeight;
171
 
 
172
  if (window.Prism) {
173
  try {
174
  Prism.highlightAllUnder(msgDiv);
 
181
  // ---------- Streaming UI ----------
182
  function createStreamingMessage(container, sender) {
183
  const msgDiv = document.createElement("div");
184
+ msgDiv.classList.add("message", "loading");
185
+ msgDiv.innerHTML = `<strong>${sender}:</strong> <span class="streaming-content"></span><span class="spinner"></span>`;
186
 
187
  const contentSpan = msgDiv.querySelector(".streaming-content");
188
 
 
197
  container.scrollTop = container.scrollHeight;
198
  },
199
  finalize: (text) => {
 
200
  msgDiv.remove();
201
  appendMessage(container, sender, text, true);
202
  }
 
229
 
230
  buffer += decoder.decode(value, { stream: true });
231
 
 
232
  const lines = buffer.split("\n\n");
233
+ buffer = lines.pop();
234
 
235
  for (const line of lines) {
236
  if (!line.trim() || !line.startsWith("data: ")) continue;
237
 
238
+ const jsonStr = line.substring(6);
239
 
240
  try {
241
  const data = JSON.parse(jsonStr);
 
255
  }
256
  }
257
 
 
258
  if (fullText) {
259
  streamUI.finalize(fullText);
260
  }
261
  } catch (err) {
262
  streamUI.msgDiv.remove();
263
+ appendMessage(output, "Error", `${err.message}`, false, true);
264
  console.error("Streaming error:", err);
265
  } finally {
266
  if (sendBtn) sendBtn.disabled = false;
 
268
  }
269
  }
270
 
271
+ // ---------- Clear Chat ----------
272
+ async function clearChat(output) {
273
+ if (!confirm("Clear conversation history? This cannot be undone.")) {
274
+ return;
275
+ }
276
+
277
+ try {
278
+ const response = await fetch("/chat/clear", {
279
+ method: "POST",
280
+ });
281
+
282
+ if (response.ok) {
283
+ output.innerHTML = "";
284
+ appendMessage(output, "System", "Chat cleared. Session history reset.", false);
285
+ } else {
286
+ throw new Error("Failed to clear chat");
287
+ }
288
+ } catch (err) {
289
+ appendMessage(output, "Error", `Failed to clear chat: ${err.message}`, false, true);
290
+ }
291
+ }
292
+
293
+ // ---------- Export Chat ----------
294
+ function exportChat(output) {
295
+ const messages = output.querySelectorAll(".message");
296
+ if (messages.length === 0) {
297
+ alert("No messages to export!");
298
+ return;
299
+ }
300
+
301
+ let markdown = "# DeepShell Chat Export\n\n";
302
+ markdown += `**Exported:** ${new Date().toLocaleString()}\n\n---\n\n`;
303
+
304
+ messages.forEach((msg) => {
305
+ const sender = msg.querySelector("strong")?.textContent.replace(":", "") || "Unknown";
306
+
307
+ // Get text content, excluding buttons
308
+ const clone = msg.cloneNode(true);
309
+ clone.querySelectorAll("button").forEach(btn => btn.remove());
310
+
311
+ const content = clone.textContent
312
+ .replace(sender + ":", "")
313
+ .trim();
314
+
315
+ markdown += `## ${sender}\n\n${content}\n\n---\n\n`;
316
+ });
317
+
318
+ // Create download
319
+ const blob = new Blob([markdown], { type: "text/markdown" });
320
+ const url = URL.createObjectURL(blob);
321
+ const a = document.createElement("a");
322
+ a.href = url;
323
+ a.download = `deepshell-chat-${Date.now()}.md`;
324
+ document.body.appendChild(a);
325
+ a.click();
326
+ document.body.removeChild(a);
327
+ URL.revokeObjectURL(url);
328
+ }
329
+
330
  // ---------- Chat wiring ----------
331
  document.addEventListener("DOMContentLoaded", () => {
332
  const form = document.getElementById("chat-form");
333
  const input = document.getElementById("chat-input");
334
  const output = document.getElementById("chat-output");
335
  const sendBtn = document.getElementById("chat-send");
336
+ const clearBtn = document.getElementById("clear-btn");
337
+ const exportBtn = document.getElementById("export-btn");
338
 
339
  if (!form || !input || !output) {
340
  console.warn("Chat elements not found (chat-form/chat-input/chat-output).");
341
  return;
342
  }
343
 
344
+ // Send message
345
  form.addEventListener("submit", (e) => {
346
  e.preventDefault();
347
  const prompt = input.value.trim();
 
351
  input.value = "";
352
  if (sendBtn) sendBtn.disabled = true;
353
 
 
354
  sendPromptStreaming(prompt, output, sendBtn, input);
355
  });
356
 
 
360
  form.requestSubmit();
361
  });
362
  }
363
+
364
+ // Clear chat button
365
+ if (clearBtn) {
366
+ clearBtn.addEventListener("click", () => {
367
+ clearChat(output);
368
+ });
369
+ }
370
+
371
+ // Export chat button
372
+ if (exportBtn) {
373
+ exportBtn.addEventListener("click", () => {
374
+ exportChat(output);
375
+ });
376
+ }
377
+
378
+ // Enter to send, Shift+Enter for newline
379
+ input.addEventListener("keydown", (e) => {
380
+ if (e.key === "Enter" && !e.shiftKey) {
381
+ e.preventDefault();
382
+ form.requestSubmit();
383
+ }
384
+ });
385
  });
386
  })();
index.html CHANGED
@@ -50,6 +50,22 @@
50
  font-size: 12px;
51
  color: var(--muted);
52
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  .panel {
54
  background: var(--panel);
55
  border: 1px solid var(--border);
@@ -77,6 +93,20 @@
77
  color: var(--accent-2);
78
  margin-bottom: 6px;
79
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  pre {
81
  margin: 6px 0 0;
82
  white-space: pre-wrap;
@@ -111,10 +141,16 @@
111
  border-radius: 8px;
112
  cursor: pointer;
113
  font-weight: 600;
 
114
  }
115
- #chat-send:hover {
116
  border-color: #334155;
117
  }
 
 
 
 
 
118
  .hint {
119
  margin-top: 8px;
120
  color: var(--muted);
@@ -147,6 +183,21 @@
147
  border-color: #475569;
148
  }
149
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
150
  /* Command execution styles */
151
  .command-wrapper {
152
  margin: 10px 0;
@@ -193,7 +244,15 @@
193
  <div class="container">
194
  <header>
195
  <div class="brand">DeepShell Chat</div>
196
- <div class="status">Backend: <span id="status">checking…</span></div>
 
 
 
 
 
 
 
 
197
  </header>
198
 
199
  <div class="panel">
@@ -205,7 +264,7 @@
205
  </form>
206
 
207
  <div class="hint">
208
- Tip: Commands will appear with Execute buttons. Click to run them safely.
209
  </div>
210
  </div>
211
  </div>
 
50
  font-size: 12px;
51
  color: var(--muted);
52
  }
53
+ .header-btn {
54
+ padding: 6px 12px;
55
+ font-size: 12px;
56
+ background: #1e293b;
57
+ color: var(--text);
58
+ border: 1px solid var(--border);
59
+ border-radius: 6px;
60
+ cursor: pointer;
61
+ font-weight: 600;
62
+ transition: all 0.2s;
63
+ }
64
+ .header-btn:hover {
65
+ background: #334155;
66
+ border-color: #475569;
67
+ transform: translateY(-1px);
68
+ }
69
  .panel {
70
  background: var(--panel);
71
  border: 1px solid var(--border);
 
93
  color: var(--accent-2);
94
  margin-bottom: 6px;
95
  }
96
+ .message.error {
97
+ border-color: #ef4444;
98
+ background: #1a0f0f;
99
+ }
100
+ .message.error strong {
101
+ color: #ef4444;
102
+ }
103
+ .message.loading {
104
+ border-color: #f59e0b;
105
+ background: #1a1508;
106
+ }
107
+ .message.loading strong {
108
+ color: #f59e0b;
109
+ }
110
  pre {
111
  margin: 6px 0 0;
112
  white-space: pre-wrap;
 
141
  border-radius: 8px;
142
  cursor: pointer;
143
  font-weight: 600;
144
+ transition: all 0.2s;
145
  }
146
+ #chat-send:hover:not(:disabled) {
147
  border-color: #334155;
148
  }
149
+ #chat-send:disabled {
150
+ opacity: 0.6;
151
+ cursor: not-allowed;
152
+ background: #1a1a1a;
153
+ }
154
  .hint {
155
  margin-top: 8px;
156
  color: var(--muted);
 
183
  border-color: #475569;
184
  }
185
 
186
+ /* Loading spinner */
187
+ .spinner {
188
+ display: inline-block;
189
+ width: 12px;
190
+ height: 12px;
191
+ border: 2px solid var(--border);
192
+ border-top-color: var(--accent);
193
+ border-radius: 50%;
194
+ animation: spin 0.8s linear infinite;
195
+ margin-left: 8px;
196
+ }
197
+ @keyframes spin {
198
+ to { transform: rotate(360deg); }
199
+ }
200
+
201
  /* Command execution styles */
202
  .command-wrapper {
203
  margin: 10px 0;
 
244
  <div class="container">
245
  <header>
246
  <div class="brand">DeepShell Chat</div>
247
+ <div style="display: flex; align-items: center; gap: 12px;">
248
+ <button id="export-btn" class="header-btn" title="Export chat as Markdown">
249
+ 📥 Export
250
+ </button>
251
+ <button id="clear-btn" class="header-btn" title="Clear conversation">
252
+ 🗑️ Clear
253
+ </button>
254
+ <div class="status">Backend: <span id="status">checking…</span></div>
255
+ </div>
256
  </header>
257
 
258
  <div class="panel">
 
264
  </form>
265
 
266
  <div class="hint">
267
+ Tip: All code blocks have copy buttons. Click to copy commands to your clipboard.
268
  </div>
269
  </div>
270
  </div>