tommytracx commited on
Commit
178cdc5
·
verified ·
1 Parent(s): 170a0e7

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +307 -55
app.py CHANGED
@@ -1,11 +1,12 @@
1
  # app.py
2
- from flask import Flask, request, jsonify, render_template_string
3
  import os
4
  import requests
5
  import json
6
  import logging
7
  from typing import Dict, Any, List
8
  import time
 
9
 
10
  app = Flask(__name__)
11
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@@ -43,8 +44,8 @@ class OllamaManager:
43
  """Return the list of available models."""
44
  return self.available_models
45
 
46
- def generate(self, model_name: str, prompt: str, **kwargs) -> Dict[str, Any]:
47
- """Generate text using a model."""
48
  if model_name not in self.available_models:
49
  return {"status": "error", "message": f"Model {model_name} not available"}
50
 
@@ -52,18 +53,23 @@ class OllamaManager:
52
  payload = {
53
  "model": model_name,
54
  "prompt": prompt,
55
- "stream": False,
56
  **kwargs
57
  }
58
- response = requests.post(f"{self.base_url}/api/generate", json=payload, timeout=120)
59
- response.raise_for_status()
60
- data = response.json()
61
- return {
62
- "status": "success",
63
- "response": data.get('response', ''),
64
- "model": model_name,
65
- "usage": data.get('usage', {})
66
- }
 
 
 
 
 
67
  except Exception as e:
68
  logging.error(f"Error generating response: {e}")
69
  return {"status": "error", "message": str(e)}
@@ -81,7 +87,7 @@ class OllamaManager:
81
  # Initialize Ollama manager
82
  ollama_manager = OllamaManager(OLLAMA_BASE_URL)
83
 
84
- # HTML template for the chat interface
85
  HTML_TEMPLATE = '''
86
  <!DOCTYPE html>
87
  <html lang="en">
@@ -89,7 +95,41 @@ HTML_TEMPLATE = '''
89
  <meta charset="UTF-8">
90
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
91
  <title>OpenWebUI - Ollama Chat</title>
 
 
 
 
 
 
 
 
 
 
92
  <style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  * {
94
  margin: 0;
95
  padding: 0;
@@ -97,20 +137,32 @@ HTML_TEMPLATE = '''
97
  }
98
  body {
99
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
100
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
 
101
  min-height: 100vh;
102
  padding: 20px;
103
  }
104
  .container {
105
  max-width: 1200px;
106
  margin: 0 auto;
107
- background: white;
108
  border-radius: 20px;
109
  box-shadow: 0 20px 40px rgba(0,0,0,0.1);
110
  overflow: hidden;
 
 
 
 
 
 
 
 
 
 
 
111
  }
112
  .header {
113
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
114
  color: white;
115
  padding: 30px;
116
  text-align: center;
@@ -126,8 +178,8 @@ HTML_TEMPLATE = '''
126
  }
127
  .controls {
128
  padding: 20px 30px;
129
- background: #f8f9fa;
130
- border-bottom: 1px solid #e9ecef;
131
  display: flex;
132
  gap: 15px;
133
  align-items: center;
@@ -140,32 +192,37 @@ HTML_TEMPLATE = '''
140
  }
141
  .control-group label {
142
  font-weight: 600;
143
- color: #495057;
144
  min-width: 80px;
145
  }
146
  .control-group select,
147
  .control-group input {
148
  padding: 8px 12px;
149
- border: 2px solid #e9ecef;
150
  border-radius: 8px;
151
  font-size: 14px;
152
  transition: border-color 0.3s;
 
 
153
  }
154
  .control-group select:focus,
155
  .control-group input:focus {
156
  outline: none;
157
- border-color: #667eea;
158
  }
159
  .chat-container {
160
  height: 500px;
161
  overflow-y: auto;
162
  padding: 20px;
163
- background: #fafbfc;
 
164
  }
165
  .message {
166
  margin-bottom: 20px;
167
  display: flex;
168
  gap: 15px;
 
 
169
  }
170
  .message.user {
171
  flex-direction: row-reverse;
@@ -180,33 +237,53 @@ HTML_TEMPLATE = '''
180
  font-weight: bold;
181
  color: white;
182
  flex-shrink: 0;
 
183
  }
184
  .message.user .message-avatar {
185
- background: #667eea;
186
- }
187
- .message.assistant .message-avatar {
188
- background: #28a745;
189
  }
190
  .message-content {
191
- background: white;
192
  padding: 15px 20px;
193
  border-radius: 18px;
194
  max-width: 70%;
195
  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
196
  line-height: 1.5;
 
197
  }
198
  .message.user .message-content {
199
- background: #667eea;
200
  color: white;
201
  }
202
- .message.assistant .message-content {
203
- background: white;
204
- color: #333;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
  }
206
  .input-container {
207
  padding: 20px 30px;
208
- background: white;
209
- border-top: 1px solid #e9ecef;
210
  }
211
  .input-form {
212
  display: flex;
@@ -215,21 +292,23 @@ HTML_TEMPLATE = '''
215
  .input-field {
216
  flex: 1;
217
  padding: 15px 20px;
218
- border: 2px solid #e9ecef;
219
  border-radius: 25px;
220
  font-size: 16px;
221
  transition: border-color 0.3s;
222
  resize: none;
223
  min-height: 50px;
224
  max-height: 120px;
 
 
225
  }
226
  .input-field:focus {
227
  outline: none;
228
- border-color: #667eea;
229
  }
230
  .send-button {
231
  padding: 15px 30px;
232
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
233
  color: white;
234
  border: none;
235
  border-radius: 25px;
@@ -262,11 +341,15 @@ HTML_TEMPLATE = '''
262
  .typing-indicator {
263
  display: none;
264
  padding: 15px 20px;
265
- background: white;
266
  border-radius: 18px;
267
  color: #6c757d;
268
  font-style: italic;
269
  }
 
 
 
 
270
  @media (max-width: 768px) {
271
  .controls {
272
  flex-direction: column;
@@ -283,6 +366,7 @@ HTML_TEMPLATE = '''
283
  </head>
284
  <body>
285
  <div class="container">
 
286
  <div class="header">
287
  <h1>🤖 OpenWebUI</h1>
288
  <p>Chat with your local Ollama models through Hugging Face Spaces</p>
@@ -336,15 +420,33 @@ HTML_TEMPLATE = '''
336
  <div class="status" id="status"></div>
337
  </div>
338
 
339
- <script>
 
 
340
  let conversationHistory = [];
341
-
 
 
342
  document.addEventListener('DOMContentLoaded', function() {
343
  loadModels();
344
  setupEventListeners();
345
  autoResizeTextarea();
346
  });
347
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
348
  async function loadModels() {
349
  const modelSelect = document.getElementById('model-select');
350
  modelSelect.innerHTML = '<option value="">Loading models...</option>';
@@ -383,6 +485,8 @@ HTML_TEMPLATE = '''
383
  document.getElementById('temp-value').textContent = this.value;
384
  });
385
  document.getElementById('message-input').addEventListener('input', autoResizeTextarea);
 
 
386
  }
387
 
388
  function autoResizeTextarea() {
@@ -417,27 +521,57 @@ HTML_TEMPLATE = '''
417
  const response = await fetch('/api/chat', {
418
  method: 'POST',
419
  headers: { 'Content-Type': 'application/json' },
420
- body: JSON.stringify({ model, prompt: message, temperature, max_tokens: maxTokens })
421
  });
422
- const data = await response.json();
 
 
 
423
 
424
  showTypingIndicator(false);
 
 
425
 
426
- if (data.status === 'success') {
427
- addMessage(data.response, 'assistant');
428
- showStatus(`Response generated using ${model}`, 'success');
429
- } else {
430
- addMessage('Sorry, I encountered an error while processing your request.', 'assistant');
431
- showStatus(`Error: ${data.message}`, 'error');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
432
  }
 
 
 
433
  } catch (error) {
434
  showTypingIndicator(false);
435
- addMessage('Sorry, I encountered a network error.', 'assistant');
 
 
 
 
436
  showStatus('Network error: ' + error.message, 'error');
437
  }
438
  }
439
 
440
- function addMessage(content, sender) {
441
  const chatContainer = document.getElementById('chat-container');
442
  const messageDiv = document.createElement('div');
443
  messageDiv.className = `message ${sender}`;
@@ -455,7 +589,117 @@ HTML_TEMPLATE = '''
455
  chatContainer.appendChild(messageDiv);
456
  chatContainer.scrollTop = chatContainer.scrollHeight;
457
 
458
- conversationHistory.push({ role: sender, content: content });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
459
  }
460
 
461
  function showTypingIndicator(show) {
@@ -488,7 +732,7 @@ def home():
488
 
489
  @app.route('/api/chat', methods=['POST'])
490
  def chat():
491
- """Chat API endpoint."""
492
  try:
493
  data = request.get_json()
494
  if not data or 'prompt' not in data or 'model' not in data:
@@ -498,9 +742,17 @@ def chat():
498
  model = data['model']
499
  temperature = data.get('temperature', TEMPERATURE)
500
  max_tokens = data.get('max_tokens', MAX_TOKENS)
 
 
 
501
 
502
- result = ollama_manager.generate(model, prompt, temperature=temperature, max_tokens=max_tokens)
503
- return jsonify(result), 200 if result["status"] == "success" else 500
 
 
 
 
 
504
  except Exception as e:
505
  logging.error(f"Chat endpoint error: {e}")
506
  return jsonify({"status": "error", "message": str(e)}), 500
 
1
  # app.py
2
+ from flask import Flask, request, jsonify, render_template_string, Response
3
  import os
4
  import requests
5
  import json
6
  import logging
7
  from typing import Dict, Any, List
8
  import time
9
+ import re
10
 
11
  app = Flask(__name__)
12
  logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
 
44
  """Return the list of available models."""
45
  return self.available_models
46
 
47
+ def generate(self, model_name: str, prompt: str, stream: bool = False, **kwargs) -> Any:
48
+ """Generate text using a model, with optional streaming."""
49
  if model_name not in self.available_models:
50
  return {"status": "error", "message": f"Model {model_name} not available"}
51
 
 
53
  payload = {
54
  "model": model_name,
55
  "prompt": prompt,
56
+ "stream": stream,
57
  **kwargs
58
  }
59
+ if stream:
60
+ response = requests.post(f"{self.base_url}/api/generate", json=payload, stream=True, timeout=120)
61
+ response.raise_for_status()
62
+ return response
63
+ else:
64
+ response = requests.post(f"{self.base_url}/api/generate", json=payload, timeout=120)
65
+ response.raise_for_status()
66
+ data = response.json()
67
+ return {
68
+ "status": "success",
69
+ "response": data.get('response', ''),
70
+ "model": model_name,
71
+ "usage": data.get('usage', {})
72
+ }
73
  except Exception as e:
74
  logging.error(f"Error generating response: {e}")
75
  return {"status": "error", "message": str(e)}
 
87
  # Initialize Ollama manager
88
  ollama_manager = OllamaManager(OLLAMA_BASE_URL)
89
 
90
+ # HTML template for the chat interface with improved UI and Sandpack
91
  HTML_TEMPLATE = '''
92
  <!DOCTYPE html>
93
  <html lang="en">
 
95
  <meta charset="UTF-8">
96
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
97
  <title>OpenWebUI - Ollama Chat</title>
98
+ <script type="importmap">
99
+ {
100
+ "imports": {
101
+ "react": "https://esm.sh/react@18",
102
+ "react-dom": "https://esm.sh/react-dom@18",
103
+ "react-dom/": "https://esm.sh/react-dom@18/",
104
+ "@codesandbox/sandpack-react": "https://esm.sh/@codesandbox/sandpack-react@latest"
105
+ }
106
+ }
107
+ </script>
108
  <style>
109
+ :root {
110
+ --primary-color: #667eea;
111
+ --secondary-color: #764ba2;
112
+ --text-color: #333;
113
+ --bg-color: #fafbfc;
114
+ --message-bg-user: var(--primary-color);
115
+ --message-bg-assistant: white;
116
+ --avatar-user: var(--primary-color);
117
+ --avatar-assistant: #28a745;
118
+ --border-color: #e9ecef;
119
+ --input-bg: white;
120
+ }
121
+ .dark-mode {
122
+ --primary-color: #3b4a8c;
123
+ --secondary-color: #4a2e6b;
124
+ --text-color: #f0f0f0;
125
+ --bg-color: #1a1a1a;
126
+ --message-bg-user: var(--primary-color);
127
+ --message-bg-assistant: #2a2a2a;
128
+ --avatar-user: var(--primary-color);
129
+ --avatar-assistant: #1a7a3a;
130
+ --border-color: #4a4a4a;
131
+ --input-bg: #3a3a3a;
132
+ }
133
  * {
134
  margin: 0;
135
  padding: 0;
 
137
  }
138
  body {
139
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
140
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
141
+ color: var(--text-color);
142
  min-height: 100vh;
143
  padding: 20px;
144
  }
145
  .container {
146
  max-width: 1200px;
147
  margin: 0 auto;
148
+ background: var(--bg-color);
149
  border-radius: 20px;
150
  box-shadow: 0 20px 40px rgba(0,0,0,0.1);
151
  overflow: hidden;
152
+ position: relative;
153
+ }
154
+ .theme-toggle {
155
+ position: absolute;
156
+ top: 10px;
157
+ right: 10px;
158
+ background: none;
159
+ border: none;
160
+ cursor: pointer;
161
+ font-size: 1.2rem;
162
+ color: white;
163
  }
164
  .header {
165
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
166
  color: white;
167
  padding: 30px;
168
  text-align: center;
 
178
  }
179
  .controls {
180
  padding: 20px 30px;
181
+ background: var(--bg-color);
182
+ border-bottom: 1px solid var(--border-color);
183
  display: flex;
184
  gap: 15px;
185
  align-items: center;
 
192
  }
193
  .control-group label {
194
  font-weight: 600;
195
+ color: var(--text-color);
196
  min-width: 80px;
197
  }
198
  .control-group select,
199
  .control-group input {
200
  padding: 8px 12px;
201
+ border: 2px solid var(--border-color);
202
  border-radius: 8px;
203
  font-size: 14px;
204
  transition: border-color 0.3s;
205
+ background: var(--input-bg);
206
+ color: var(--text-color);
207
  }
208
  .control-group select:focus,
209
  .control-group input:focus {
210
  outline: none;
211
+ border-color: var(--primary-color);
212
  }
213
  .chat-container {
214
  height: 500px;
215
  overflow-y: auto;
216
  padding: 20px;
217
+ background: var(--bg-color);
218
+ transition: background 0.3s;
219
  }
220
  .message {
221
  margin-bottom: 20px;
222
  display: flex;
223
  gap: 15px;
224
+ position: relative;
225
+ animation: fadeIn 0.3s ease-in;
226
  }
227
  .message.user {
228
  flex-direction: row-reverse;
 
237
  font-weight: bold;
238
  color: white;
239
  flex-shrink: 0;
240
+ background: var(--avatar-assistant);
241
  }
242
  .message.user .message-avatar {
243
+ background: var(--avatar-user);
 
 
 
244
  }
245
  .message-content {
246
+ background: var(--message-bg-assistant);
247
  padding: 15px 20px;
248
  border-radius: 18px;
249
  max-width: 70%;
250
  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
251
  line-height: 1.5;
252
+ color: var(--text-color);
253
  }
254
  .message.user .message-content {
255
+ background: var(--message-bg-user);
256
  color: white;
257
  }
258
+ .code-actions {
259
+ position: absolute;
260
+ top: 5px;
261
+ right: 5px;
262
+ display: flex;
263
+ gap: 5px;
264
+ }
265
+ .code-button {
266
+ background: rgba(0,0,0,0.1);
267
+ border: none;
268
+ padding: 5px 10px;
269
+ border-radius: 5px;
270
+ cursor: pointer;
271
+ font-size: 12px;
272
+ color: var(--text-color);
273
+ }
274
+ .code-button:hover {
275
+ background: rgba(0,0,0,0.2);
276
+ }
277
+ .sandbox-container {
278
+ margin-top: 10px;
279
+ border: 1px solid var(--border-color);
280
+ border-radius: 8px;
281
+ overflow: hidden;
282
  }
283
  .input-container {
284
  padding: 20px 30px;
285
+ background: var(--bg-color);
286
+ border-top: 1px solid var(--border-color);
287
  }
288
  .input-form {
289
  display: flex;
 
292
  .input-field {
293
  flex: 1;
294
  padding: 15px 20px;
295
+ border: 2px solid var(--border-color);
296
  border-radius: 25px;
297
  font-size: 16px;
298
  transition: border-color 0.3s;
299
  resize: none;
300
  min-height: 50px;
301
  max-height: 120px;
302
+ background: var(--input-bg);
303
+ color: var(--text-color);
304
  }
305
  .input-field:focus {
306
  outline: none;
307
+ border-color: var(--primary-color);
308
  }
309
  .send-button {
310
  padding: 15px 30px;
311
+ background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
312
  color: white;
313
  border: none;
314
  border-radius: 25px;
 
341
  .typing-indicator {
342
  display: none;
343
  padding: 15px 20px;
344
+ background: var(--message-bg-assistant);
345
  border-radius: 18px;
346
  color: #6c757d;
347
  font-style: italic;
348
  }
349
+ @keyframes fadeIn {
350
+ from { opacity: 0; transform: translateY(10px); }
351
+ to { opacity: 1; transform: translateY(0); }
352
+ }
353
  @media (max-width: 768px) {
354
  .controls {
355
  flex-direction: column;
 
366
  </head>
367
  <body>
368
  <div class="container">
369
+ <button class="theme-toggle" id="theme-toggle">🌙</button>
370
  <div class="header">
371
  <h1>🤖 OpenWebUI</h1>
372
  <p>Chat with your local Ollama models through Hugging Face Spaces</p>
 
420
  <div class="status" id="status"></div>
421
  </div>
422
 
423
+ <script type="module">
424
+ import { Sandpack } from 'https://esm.sh/@codesandbox/sandpack-react@latest';
425
+
426
  let conversationHistory = [];
427
+ let currentMessageDiv = null;
428
+ let currentCodeBlocks = [];
429
+
430
  document.addEventListener('DOMContentLoaded', function() {
431
  loadModels();
432
  setupEventListeners();
433
  autoResizeTextarea();
434
  });
435
+
436
+ function toggleTheme() {
437
+ document.body.classList.toggle('dark-mode');
438
+ const themeToggle = document.getElementById('theme-toggle');
439
+ themeToggle.textContent = document.body.classList.contains('dark-mode') ? '☀️' : '🌙';
440
+ localStorage.setItem('theme', document.body.classList.contains('dark-mode') ? 'dark' : 'light');
441
+ }
442
+
443
+ function loadTheme() {
444
+ if (localStorage.getItem('theme') === 'dark') {
445
+ document.body.classList.add('dark-mode');
446
+ document.getElementById('theme-toggle').textContent = '☀️';
447
+ }
448
+ }
449
+
450
  async function loadModels() {
451
  const modelSelect = document.getElementById('model-select');
452
  modelSelect.innerHTML = '<option value="">Loading models...</option>';
 
485
  document.getElementById('temp-value').textContent = this.value;
486
  });
487
  document.getElementById('message-input').addEventListener('input', autoResizeTextarea);
488
+ document.getElementById('theme-toggle').addEventListener('click', toggleTheme);
489
+ loadTheme();
490
  }
491
 
492
  function autoResizeTextarea() {
 
521
  const response = await fetch('/api/chat', {
522
  method: 'POST',
523
  headers: { 'Content-Type': 'application/json' },
524
+ body: JSON.stringify({ model, prompt: message, temperature, max_tokens: maxTokens, stream: true })
525
  });
526
+
527
+ if (!response.ok) {
528
+ throw new Error('Network response was not ok');
529
+ }
530
 
531
  showTypingIndicator(false);
532
+ currentMessageDiv = addMessage('', 'assistant', true);
533
+ currentCodeBlocks = [];
534
 
535
+ const reader = response.body.getReader();
536
+ const decoder = new TextDecoder();
537
+ let accumulatedResponse = '';
538
+
539
+ while (true) {
540
+ const { done, value } = await reader.read();
541
+ if (done) break;
542
+
543
+ const chunk = decoder.decode(value);
544
+ const lines = chunk.split('\n');
545
+
546
+ for (const line of lines) {
547
+ if (line.trim()) {
548
+ try {
549
+ const data = JSON.parse(line);
550
+ if (data.response) {
551
+ accumulatedResponse += data.response;
552
+ updateMessage(currentMessageDiv, accumulatedResponse);
553
+ }
554
+ } catch (e) {
555
+ console.error('Error parsing chunk:', e);
556
+ }
557
+ }
558
+ }
559
  }
560
+
561
+ processCodeBlocks(currentMessageDiv, accumulatedResponse);
562
+ showStatus(`Response generated using ${model}`, 'success');
563
  } catch (error) {
564
  showTypingIndicator(false);
565
+ if (currentMessageDiv) {
566
+ updateMessage(currentMessageDiv, 'Sorry, I encountered a network error.');
567
+ } else {
568
+ addMessage('Sorry, I encountered a network error.', 'assistant');
569
+ }
570
  showStatus('Network error: ' + error.message, 'error');
571
  }
572
  }
573
 
574
+ function addMessage(content, sender, isStreaming = false) {
575
  const chatContainer = document.getElementById('chat-container');
576
  const messageDiv = document.createElement('div');
577
  messageDiv.className = `message ${sender}`;
 
589
  chatContainer.appendChild(messageDiv);
590
  chatContainer.scrollTop = chatContainer.scrollHeight;
591
 
592
+ if (!isStreaming) {
593
+ conversationHistory.push({ role: sender, content: content });
594
+ }
595
+ return messageDiv;
596
+ }
597
+
598
+ function updateMessage(messageDiv, content) {
599
+ const messageContent = messageDiv.querySelector('.message-content');
600
+ messageContent.textContent = content;
601
+ const chatContainer = document.getElementById('chat-container');
602
+ chatContainer.scrollTop = chatContainer.scrollHeight;
603
+ }
604
+
605
+ function processCodeBlocks(messageDiv, content) {
606
+ const codeBlockRegex = /```(\w+)?\n([\s\S]*?)```/g;
607
+ let match;
608
+ let lastIndex = 0;
609
+ const messageContent = messageDiv.querySelector('.message-content');
610
+ const fragments = [];
611
+
612
+ while ((match = codeBlockRegex.exec(content)) !== null) {
613
+ const language = match[1] || 'javascript';
614
+ const code = match[2].trim();
615
+ const startIndex = match.index;
616
+
617
+ // Add text before the code block
618
+ if (startIndex > lastIndex) {
619
+ fragments.push({ type: 'text', content: content.slice(lastIndex, startIndex) });
620
+ }
621
+
622
+ // Add code block
623
+ fragments.push({ type: 'code', language, content: code });
624
+ currentCodeBlocks.push({ language, content: code });
625
+ lastIndex = codeBlockRegex.lastIndex;
626
+ }
627
+
628
+ // Add remaining text
629
+ if (lastIndex < content.length) {
630
+ fragments.push({ type: 'text', content: content.slice(lastIndex) });
631
+ }
632
+
633
+ // Clear message content and rebuild with fragments
634
+ messageContent.innerHTML = '';
635
+ fragments.forEach((fragment, index) => {
636
+ if (fragment.type === 'text') {
637
+ const textSpan = document.createElement('span');
638
+ textSpan.textContent = fragment.content;
639
+ messageContent.appendChild(textSpan);
640
+ } else if (fragment.type === 'code') {
641
+ const codeContainer = document.createElement('div');
642
+ codeContainer.className = 'sandbox-container';
643
+
644
+ const actions = document.createElement('div');
645
+ actions.className = 'code-actions';
646
+
647
+ const copyButton = document.createElement('button');
648
+ copyButton.className = 'code-button';
649
+ copyButton.textContent = 'Copy';
650
+ copyButton.onclick = () => {
651
+ navigator.clipboard.writeText(fragment.content);
652
+ showStatus('Code copied to clipboard', 'success');
653
+ };
654
+
655
+ const downloadButton = document.createElement('button');
656
+ downloadButton.className = 'code-button';
657
+ downloadButton.textContent = 'Download';
658
+ downloadButton.onclick = () => {
659
+ const blob = new Blob([fragment.content], { type: 'text/plain' });
660
+ const url = URL.createObjectURL(blob);
661
+ const a = document.createElement('a');
662
+ a.href = url;
663
+ a.download = `code.${fragment.language || 'txt'}`;
664
+ a.click();
665
+ URL.revokeObjectURL(url);
666
+ };
667
+
668
+ actions.appendChild(copyButton);
669
+ actions.appendChild(downloadButton);
670
+ messageContent.appendChild(actions);
671
+
672
+ const script = document.createElement('script');
673
+ script.type = 'module';
674
+ script.textContent = `
675
+ import { Sandpack } from '@codesandbox/sandpack-react';
676
+ import { createRoot } from 'react-dom';
677
+ const root = createRoot(document.getElementById('sandpack-${index}'));
678
+ root.render(
679
+ React.createElement(Sandpack, {
680
+ template: "${fragment.language === 'javascript' ? 'react' : fragment.language}",
681
+ files: {
682
+ '/App.${fragment.language === 'javascript' ? 'jsx' : fragment.language}': \`${fragment.content.replace(/`/g, '\\`')}\`
683
+ },
684
+ options: {
685
+ showNavigator: false,
686
+ showTabs: true,
687
+ showLineNumbers: true,
688
+ editorHeight: 300
689
+ }
690
+ })
691
+ );
692
+ `;
693
+
694
+ const sandboxDiv = document.createElement('div');
695
+ sandboxDiv.id = `sandpack-${index}`;
696
+ codeContainer.appendChild(sandboxDiv);
697
+ codeContainer.appendChild(script);
698
+ messageContent.appendChild(codeContainer);
699
+ }
700
+ });
701
+
702
+ conversationHistory.push({ role: 'assistant', content: content });
703
  }
704
 
705
  function showTypingIndicator(show) {
 
732
 
733
  @app.route('/api/chat', methods=['POST'])
734
  def chat():
735
+ """Chat API endpoint with streaming support."""
736
  try:
737
  data = request.get_json()
738
  if not data or 'prompt' not in data or 'model' not in data:
 
742
  model = data['model']
743
  temperature = data.get('temperature', TEMPERATURE)
744
  max_tokens = data.get('max_tokens', MAX_TOKENS)
745
+ stream = data.get('stream', False)
746
+
747
+ result = ollama_manager.generate(model, prompt, stream=stream, temperature=temperature, max_tokens=max_tokens)
748
 
749
+ if stream and isinstance(result, requests.Response):
750
+ def generate_stream():
751
+ for chunk in result.iter_content(chunk_size=None):
752
+ yield chunk
753
+ return Response(generate_stream(), content_type='application/json')
754
+ else:
755
+ return jsonify(result), 200 if result["status"] == "success" else 500
756
  except Exception as e:
757
  logging.error(f"Chat endpoint error: {e}")
758
  return jsonify({"status": "error", "message": str(e)}), 500