tommytracx commited on
Commit
9ed65ee
·
verified ·
1 Parent(s): 178cdc5

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +324 -110
app.py CHANGED
@@ -1,15 +1,21 @@
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')
 
 
 
 
 
13
 
14
  # Configuration
15
  OLLAMA_BASE_URL = os.getenv('OLLAMA_BASE_URL', 'https://huggingface.co/spaces/tommytracx/ollama-api')
@@ -36,8 +42,14 @@ class OllamaManager:
36
  self.available_models = ALLOWED_MODELS
37
  logging.warning("No allowed models found in API response, using ALLOWED_MODELS")
38
  logging.info(f"Available models: {self.available_models}")
 
 
 
 
 
 
39
  except Exception as e:
40
- logging.error(f"Error refreshing models: {e}")
41
  self.available_models = ALLOWED_MODELS
42
 
43
  def list_models(self) -> List[str]:
@@ -47,6 +59,7 @@ class OllamaManager:
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
 
52
  try:
@@ -64,14 +77,21 @@ class OllamaManager:
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)}
76
 
77
  def health_check(self) -> Dict[str, Any]:
@@ -79,15 +99,22 @@ class OllamaManager:
79
  try:
80
  response = requests.get(f"{self.base_url}/api/tags", timeout=10)
81
  response.raise_for_status()
 
82
  return {"status": "healthy", "available_models": len(self.available_models)}
 
 
 
 
 
 
83
  except Exception as e:
84
- logging.error(f"Health check failed: {e}")
85
  return {"status": "unhealthy", "error": str(e)}
86
 
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">
@@ -107,28 +134,32 @@ HTML_TEMPLATE = '''
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;
@@ -140,55 +171,110 @@ HTML_TEMPLATE = '''
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;
 
 
 
 
 
 
 
 
 
 
 
169
  }
170
  .header h1 {
171
- font-size: 2.5rem;
172
  margin-bottom: 10px;
173
  font-weight: 700;
174
  }
175
  .header p {
176
- font-size: 1.1rem;
177
  opacity: 0.9;
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;
186
  flex-wrap: wrap;
 
187
  }
188
  .control-group {
189
  display: flex;
190
  align-items: center;
191
  gap: 8px;
 
 
192
  }
193
  .control-group label {
194
  font-weight: 600;
@@ -197,7 +283,8 @@ HTML_TEMPLATE = '''
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;
@@ -211,7 +298,7 @@ HTML_TEMPLATE = '''
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);
@@ -246,7 +333,7 @@ HTML_TEMPLATE = '''
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);
@@ -270,6 +357,7 @@ HTML_TEMPLATE = '''
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);
@@ -281,24 +369,25 @@ HTML_TEMPLATE = '''
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;
290
- gap: 15px;
 
291
  }
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
  }
@@ -307,7 +396,7 @@ HTML_TEMPLATE = '''
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;
@@ -316,7 +405,7 @@ HTML_TEMPLATE = '''
316
  font-weight: 600;
317
  cursor: pointer;
318
  transition: transform 0.2s;
319
- min-width: 100px;
320
  }
321
  .send-button:hover {
322
  transform: translateY(-2px);
@@ -345,90 +434,150 @@ HTML_TEMPLATE = '''
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;
356
- align-items: stretch;
357
  }
358
  .control-group {
359
- justify-content: space-between;
 
 
 
 
 
360
  }
361
  .message-content {
362
- max-width: 85%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
363
  }
364
  }
365
  </style>
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>
 
373
  </div>
374
-
375
- <div class="controls">
376
- <div class="control-group">
377
- <label for="model-select">Model:</label>
378
- <select id="model-select">
379
- <option value="">Select a model...</option>
380
- </select>
381
- </div>
382
- <div class="control-group">
383
- <label for="temperature">Temperature:</label>
384
- <input type="range" id="temperature" min="0" max="2" step="0.1" value="0.7">
385
- <span id="temp-value">0.7</span>
386
  </div>
387
- <div class="control-group">
388
- <label for="max-tokens">Max Tokens:</label>
389
- <input type="number" id="max-tokens" min="1" max="4096" value="2048">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
390
  </div>
391
- </div>
392
-
393
- <div class="chat-container" id="chat-container">
394
- <div class="message assistant">
395
- <div class="message-avatar">AI</div>
396
- <div class="message-content">
397
- Hello! I'm your AI assistant powered by Ollama. How can I help you today?
398
  </div>
399
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  </div>
401
-
402
- <div class="typing-indicator" id="typing-indicator">
403
- AI is thinking...
404
- </div>
405
-
406
- <div class="input-container">
407
- <form class="input-form" id="chat-form">
408
- <textarea
409
- class="input-field"
410
- id="message-input"
411
- placeholder="Type your message here..."
412
- rows="1"
413
- ></textarea>
414
- <button type="submit" class="send-button" id="send-button">
415
- Send
416
- </button>
417
- </form>
418
- </div>
419
-
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
  });
@@ -447,6 +596,11 @@ HTML_TEMPLATE = '''
447
  }
448
  }
449
 
 
 
 
 
 
450
  async function loadModels() {
451
  const modelSelect = document.getElementById('model-select');
452
  modelSelect.innerHTML = '<option value="">Loading models...</option>';
@@ -478,7 +632,51 @@ HTML_TEMPLATE = '''
478
  showStatus('Failed to load models: ' + error.message, 'error');
479
  }
480
  }
481
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
  function setupEventListeners() {
483
  document.getElementById('chat-form').addEventListener('submit', handleSubmit);
484
  document.getElementById('temperature').addEventListener('input', function() {
@@ -486,13 +684,14 @@ HTML_TEMPLATE = '''
486
  });
487
  document.getElementById('message-input').addEventListener('input', autoResizeTextarea);
488
  document.getElementById('theme-toggle').addEventListener('click', toggleTheme);
 
489
  loadTheme();
490
  }
491
 
492
  function autoResizeTextarea() {
493
  const textarea = document.getElementById('message-input');
494
  textarea.style.height = 'auto';
495
- textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
496
  }
497
 
498
  async function handleSubmit(e) {
@@ -513,6 +712,12 @@ HTML_TEMPLATE = '''
513
  }
514
 
515
  addMessage(message, 'user');
 
 
 
 
 
 
516
  messageInput.value = '';
517
  autoResizeTextarea();
518
  showTypingIndicator(true);
@@ -559,11 +764,15 @@ HTML_TEMPLATE = '''
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
  }
@@ -571,7 +780,7 @@ HTML_TEMPLATE = '''
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,8 +798,12 @@ HTML_TEMPLATE = '''
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
  }
@@ -614,23 +827,19 @@ HTML_TEMPLATE = '''
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') {
@@ -674,7 +883,7 @@ HTML_TEMPLATE = '''
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}",
@@ -692,14 +901,12 @@ HTML_TEMPLATE = '''
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) {
@@ -736,6 +943,7 @@ def chat():
736
  try:
737
  data = request.get_json()
738
  if not data or 'prompt' not in data or 'model' not in data:
 
739
  return jsonify({"status": "error", "message": "Prompt and model are required"}), 400
740
 
741
  prompt = data['prompt']
@@ -748,10 +956,15 @@ def chat():
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}")
@@ -762,6 +975,7 @@ def get_models():
762
  """Get available models."""
763
  try:
764
  models = ollama_manager.list_models()
 
765
  return jsonify({
766
  "status": "success",
767
  "models": models,
@@ -787,7 +1001,7 @@ def health_check():
787
  "status": "unhealthy",
788
  "error": str(e),
789
  "timestamp": time.time()
790
- }), 500
791
 
792
  if __name__ == '__main__':
793
  app.run(host='0.0.0.0', port=7860, debug=False)
 
1
  # app.py
2
+ from flask import Flask, request, jsonify, Response
3
  import os
4
  import requests
5
  import json
6
  import logging
7
+ from logging.handlers import RotatingFileHandler
8
  from typing import Dict, Any, List
9
  import time
10
  import re
11
 
12
  app = Flask(__name__)
13
+
14
+ # Configure logging with file output
15
+ log_handler = RotatingFileHandler('/home/ollama/openwebui.log', maxBytes=1000000, backupCount=5)
16
+ log_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
17
+ logging.getLogger().addHandler(log_handler)
18
+ logging.getLogger().setLevel(logging.INFO)
19
 
20
  # Configuration
21
  OLLAMA_BASE_URL = os.getenv('OLLAMA_BASE_URL', 'https://huggingface.co/spaces/tommytracx/ollama-api')
 
42
  self.available_models = ALLOWED_MODELS
43
  logging.warning("No allowed models found in API response, using ALLOWED_MODELS")
44
  logging.info(f"Available models: {self.available_models}")
45
+ except requests.exceptions.ConnectionError as e:
46
+ logging.error(f"Connection error refreshing models: {e}")
47
+ self.available_models = ALLOWED_MODELS
48
+ except requests.exceptions.HTTPError as e:
49
+ logging.error(f"HTTP error refreshing models: {e}")
50
+ self.available_models = ALLOWED_MODELS
51
  except Exception as e:
52
+ logging.error(f"Unexpected error refreshing models: {e}")
53
  self.available_models = ALLOWED_MODELS
54
 
55
  def list_models(self) -> List[str]:
 
59
  def generate(self, model_name: str, prompt: str, stream: bool = False, **kwargs) -> Any:
60
  """Generate text using a model, with optional streaming."""
61
  if model_name not in self.available_models:
62
+ logging.warning(f"Attempted to generate with unavailable model: {model_name}")
63
  return {"status": "error", "message": f"Model {model_name} not available"}
64
 
65
  try:
 
77
  response = requests.post(f"{self.base_url}/api/generate", json=payload, timeout=120)
78
  response.raise_for_status()
79
  data = response.json()
80
+ logging.info(f"Generated response with model {model_name}")
81
  return {
82
  "status": "success",
83
  "response": data.get('response', ''),
84
  "model": model_name,
85
  "usage": data.get('usage', {})
86
  }
87
+ except requests.exceptions.ConnectionError as e:
88
+ logging.error(f"Connection error generating response: {e}")
89
+ return {"status": "error", "message": f"Connection error: {str(e)}"}
90
+ except requests.exceptions.HTTPError as e:
91
+ logging.error(f"HTTP error generating response: {e}")
92
+ return {"status": "error", "message": f"HTTP error: {str(e)}"}
93
  except Exception as e:
94
+ logging.error(f"Unexpected error generating response: {e}")
95
  return {"status": "error", "message": str(e)}
96
 
97
  def health_check(self) -> Dict[str, Any]:
 
99
  try:
100
  response = requests.get(f"{self.base_url}/api/tags", timeout=10)
101
  response.raise_for_status()
102
+ logging.info("Health check successful")
103
  return {"status": "healthy", "available_models": len(self.available_models)}
104
+ except requests.exceptions.ConnectionError as e:
105
+ logging.error(f"Health check connection error: {e}")
106
+ return {"status": "unhealthy", "error": f"Connection error: {str(e)}"}
107
+ except requests.exceptions.HTTPError as e:
108
+ logging.error(f"Health check HTTP error: {e}")
109
+ return {"status": "unhealthy", "error": f"HTTP error: {str(e)}"}
110
  except Exception as e:
111
+ logging.error(f"Health check unexpected error: {e}")
112
  return {"status": "unhealthy", "error": str(e)}
113
 
114
  # Initialize Ollama manager
115
  ollama_manager = OllamaManager(OLLAMA_BASE_URL)
116
 
117
+ # HTML template for the chat interface with comprehensive, mobile-optimized UI
118
  HTML_TEMPLATE = '''
119
  <!DOCTYPE html>
120
  <html lang="en">
 
134
  </script>
135
  <style>
136
  :root {
137
+ --primary-color: #5a4bff;
138
+ --secondary-color: #7b3fe4;
139
  --text-color: #333;
140
+ --bg-color: #f8fafc;
141
  --message-bg-user: var(--primary-color);
142
  --message-bg-assistant: white;
143
  --avatar-user: var(--primary-color);
144
+ --avatar-assistant: #2ea44f;
145
+ --border-color: #e2e8f0;
146
  --input-bg: white;
147
+ --sidebar-bg: #ffffff;
148
+ --sidebar-border: #e2e8f0;
149
  }
150
  .dark-mode {
151
  --primary-color: #3b4a8c;
152
  --secondary-color: #4a2e6b;
153
+ --text-color: #e2e8f0;
154
+ --bg-color: #1a202c;
155
  --message-bg-user: var(--primary-color);
156
+ --message-bg-assistant: #2d3748;
157
  --avatar-user: var(--primary-color);
158
+ --avatar-assistant: #276749;
159
+ --border-color: #4a5568;
160
+ --input-bg: #2d3748;
161
+ --sidebar-bg: #2d3748;
162
+ --sidebar-border: #4a5568;
163
  }
164
  * {
165
  margin: 0;
 
171
  background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
172
  color: var(--text-color);
173
  min-height: 100vh;
174
+ overflow-x: hidden;
175
  }
176
  .container {
177
+ display: flex;
178
+ max-width: 100%;
179
+ min-height: 100vh;
180
  background: var(--bg-color);
 
 
 
 
181
  }
182
+ .sidebar {
183
+ width: 250px;
184
+ background: var(--sidebar-bg);
185
+ border-right: 1px solid var(--sidebar-border);
186
+ padding: 20px;
187
+ position: fixed;
188
+ height: 100%;
189
+ transform: translateX(-100%);
190
+ transition: transform 0.3s ease;
191
+ z-index: 1000;
192
+ }
193
+ .sidebar.open {
194
+ transform: translateX(0);
195
+ }
196
+ .sidebar-toggle {
197
+ position: fixed;
198
  top: 10px;
199
+ left: 10px;
200
+ background: var(--primary-color);
201
+ color: white;
202
  border: none;
203
+ padding: 10px;
204
+ border-radius: 8px;
205
+ cursor: pointer;
206
+ z-index: 1100;
207
+ }
208
+ .sidebar h2 {
209
+ font-size: 1.5rem;
210
+ margin-bottom: 20px;
211
+ }
212
+ .chat-history {
213
+ list-style: none;
214
+ overflow-y: auto;
215
+ max-height: calc(100vh - 100px);
216
+ }
217
+ .chat-history-item {
218
+ padding: 10px;
219
+ border-radius: 8px;
220
+ margin-bottom: 10px;
221
  cursor: pointer;
222
+ transition: background 0.2s;
223
+ }
224
+ .chat-history-item:hover {
225
+ background: var(--border-color);
226
+ }
227
+ .chat-history-item.active {
228
+ background: var(--primary-color);
229
  color: white;
230
  }
231
+ .main-content {
232
+ flex: 1;
233
+ display: flex;
234
+ flex-direction: column;
235
+ min-height: 100vh;
236
+ }
237
  .header {
238
  background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
239
  color: white;
240
+ padding: 20px;
241
  text-align: center;
242
+ position: relative;
243
+ }
244
+ .theme-toggle {
245
+ position: absolute;
246
+ top: 20px;
247
+ right: 20px;
248
+ background: none;
249
+ border: none;
250
+ cursor: pointer;
251
+ font-size: 1.5rem;
252
+ color: white;
253
  }
254
  .header h1 {
255
+ font-size: 2rem;
256
  margin-bottom: 10px;
257
  font-weight: 700;
258
  }
259
  .header p {
260
+ font-size: 1rem;
261
  opacity: 0.9;
262
  }
263
  .controls {
264
+ padding: 15px 20px;
265
  background: var(--bg-color);
266
  border-bottom: 1px solid var(--border-color);
267
  display: flex;
268
+ gap: 10px;
 
269
  flex-wrap: wrap;
270
+ justify-content: center;
271
  }
272
  .control-group {
273
  display: flex;
274
  align-items: center;
275
  gap: 8px;
276
+ flex: 1;
277
+ min-width: 200px;
278
  }
279
  .control-group label {
280
  font-weight: 600;
 
283
  }
284
  .control-group select,
285
  .control-group input {
286
+ flex: 1;
287
+ padding: 10px;
288
  border: 2px solid var(--border-color);
289
  border-radius: 8px;
290
  font-size: 14px;
 
298
  border-color: var(--primary-color);
299
  }
300
  .chat-container {
301
+ flex: 1;
302
  overflow-y: auto;
303
  padding: 20px;
304
  background: var(--bg-color);
 
333
  background: var(--message-bg-assistant);
334
  padding: 15px 20px;
335
  border-radius: 18px;
336
+ max-width: 80%;
337
  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
338
  line-height: 1.5;
339
  color: var(--text-color);
 
357
  cursor: pointer;
358
  font-size: 12px;
359
  color: var(--text-color);
360
+ transition: background 0.2s;
361
  }
362
  .code-button:hover {
363
  background: rgba(0,0,0,0.2);
 
369
  overflow: hidden;
370
  }
371
  .input-container {
372
+ padding: 15px 20px;
373
  background: var(--bg-color);
374
  border-top: 1px solid var(--border-color);
375
  }
376
  .input-form {
377
  display: flex;
378
+ gap: 10px;
379
+ align-items: center;
380
  }
381
  .input-field {
382
  flex: 1;
383
+ padding: 12px 15px;
384
  border: 2px solid var(--border-color);
385
  border-radius: 25px;
386
  font-size: 16px;
387
  transition: border-color 0.3s;
388
  resize: none;
389
  min-height: 50px;
390
+ max-height: 150px;
391
  background: var(--input-bg);
392
  color: var(--text-color);
393
  }
 
396
  border-color: var(--primary-color);
397
  }
398
  .send-button {
399
+ padding: 12px 20px;
400
  background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
401
  color: white;
402
  border: none;
 
405
  font-weight: 600;
406
  cursor: pointer;
407
  transition: transform 0.2s;
408
+ min-width: 80px;
409
  }
410
  .send-button:hover {
411
  transform: translateY(-2px);
 
434
  border-radius: 18px;
435
  color: #6c757d;
436
  font-style: italic;
437
+ margin: 20px;
438
  }
439
  @keyframes fadeIn {
440
  from { opacity: 0; transform: translateY(10px); }
441
  to { opacity: 1; transform: translateY(0); }
442
  }
443
  @media (max-width: 768px) {
444
+ .container {
445
+ flex-direction: column;
446
+ }
447
+ .sidebar {
448
+ width: 100%;
449
+ height: auto;
450
+ max-height: 80vh;
451
+ position: fixed;
452
+ top: 0;
453
+ left: 0;
454
+ transform: translateY(-100%);
455
+ border-right: none;
456
+ border-bottom: 1px solid var(--sidebar-border);
457
+ }
458
+ .sidebar.open {
459
+ transform: translateY(0);
460
+ }
461
+ .sidebar-toggle {
462
+ top: 10px;
463
+ left: 10px;
464
+ z-index: 1100;
465
+ }
466
+ .main-content {
467
+ margin-top: 60px;
468
+ }
469
  .controls {
470
  flex-direction: column;
471
+ gap: 15px;
472
  }
473
  .control-group {
474
+ flex-direction: column;
475
+ align-items: stretch;
476
+ }
477
+ .control-group select,
478
+ .control-group input {
479
+ width: 100%;
480
  }
481
  .message-content {
482
+ max-width: 90%;
483
+ }
484
+ .header {
485
+ padding: 15px;
486
+ }
487
+ .header h1 {
488
+ font-size: 1.8rem;
489
+ }
490
+ .header p {
491
+ font-size: 0.9rem;
492
+ }
493
+ .input-container {
494
+ padding: 10px 15px;
495
+ }
496
+ .send-button {
497
+ padding: 10px 15px;
498
+ min-width: 60px;
499
  }
500
  }
501
  </style>
502
  </head>
503
  <body>
504
+ <button class="sidebar-toggle" id="sidebar-toggle">☰</button>
505
  <div class="container">
506
+ <div class="sidebar" id="sidebar">
507
+ <h2>Chat History</h2>
508
+ <ul class="chat-history" id="chat-history">
509
+ <!-- Chat history items will be populated here -->
510
+ </ul>
511
  </div>
512
+ <div class="main-content">
513
+ <div class="header">
514
+ <button class="theme-toggle" id="theme-toggle">🌙</button>
515
+ <h1>🤖 OpenWebUI</h1>
516
+ <p>Chat with AI models powered by Ollama on Hugging Face Spaces</p>
 
 
 
 
 
 
 
517
  </div>
518
+
519
+ <div class="controls">
520
+ <div class="control-group">
521
+ <label for="model-select">Model:</label>
522
+ <select id="model-select">
523
+ <option value="">Select a model...</option>
524
+ </select>
525
+ </div>
526
+ <div class="control-group">
527
+ <label for="temperature">Temperature:</label>
528
+ <div style="flex: 1; display: flex; align-items: center; gap: 8px;">
529
+ <input type="range" id="temperature" min="0" max="2" step="0.1" value="0.7">
530
+ <span id="temp-value">0.7</span>
531
+ </div>
532
+ </div>
533
+ <div class="control-group">
534
+ <label for="max-tokens">Max Tokens:</label>
535
+ <input type="number" id="max-tokens" min="1" max="4096" value="2048">
536
+ </div>
537
  </div>
538
+
539
+ <div class="chat-container" id="chat-container">
540
+ <div class="message assistant">
541
+ <div class="message-avatar">AI</div>
542
+ <div class="message-content">
543
+ Hello! I'm your AI assistant powered by Ollama. How can I help you today?
544
+ </div>
545
  </div>
546
  </div>
547
+
548
+ <div class="typing-indicator" id="typing-indicator">
549
+ AI is thinking...
550
+ </div>
551
+
552
+ <div class="input-container">
553
+ <form class="input-form" id="chat-form">
554
+ <textarea
555
+ class="input-field"
556
+ id="message-input"
557
+ placeholder="Type your message here..."
558
+ rows="1"
559
+ ></textarea>
560
+ <button type="submit" class="send-button" id="send-button">
561
+ Send
562
+ </button>
563
+ </form>
564
+ </div>
565
+
566
+ <div class="status" id="status"></div>
567
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
  </div>
569
 
570
  <script type="module">
571
  import { Sandpack } from 'https://esm.sh/@codesandbox/sandpack-react@latest';
572
 
573
+ let conversationHistory = JSON.parse(localStorage.getItem('chatHistory')) || [];
574
+ let currentConversationId = null;
575
  let currentMessageDiv = null;
576
  let currentCodeBlocks = [];
577
 
578
  document.addEventListener('DOMContentLoaded', function() {
579
  loadModels();
580
+ loadChatHistory();
581
  setupEventListeners();
582
  autoResizeTextarea();
583
  });
 
596
  }
597
  }
598
 
599
+ function toggleSidebar() {
600
+ const sidebar = document.getElementById('sidebar');
601
+ sidebar.classList.toggle('open');
602
+ }
603
+
604
  async function loadModels() {
605
  const modelSelect = document.getElementById('model-select');
606
  modelSelect.innerHTML = '<option value="">Loading models...</option>';
 
632
  showStatus('Failed to load models: ' + error.message, 'error');
633
  }
634
  }
635
+
636
+ function loadChatHistory() {
637
+ const chatHistoryList = document.getElementById('chat-history');
638
+ chatHistoryList.innerHTML = '';
639
+ conversationHistory.forEach((conv, index) => {
640
+ const li = document.createElement('li');
641
+ li.className = 'chat-history-item';
642
+ li.textContent = `Chat ${index + 1} - ${new Date(conv.timestamp).toLocaleString()}`;
643
+ li.dataset.convId = index;
644
+ li.addEventListener('click', () => loadConversation(index));
645
+ chatHistoryList.appendChild(li);
646
+ });
647
+ if (conversationHistory.length > 0) {
648
+ loadConversation(conversationHistory.length - 1);
649
+ }
650
+ }
651
+
652
+ function loadConversation(convId) {
653
+ currentConversationId = convId;
654
+ const chatContainer = document.getElementById('chat-container');
655
+ chatContainer.innerHTML = '';
656
+ const conversation = conversationHistory[convId];
657
+ conversation.messages.forEach(msg => {
658
+ const messageDiv = addMessage(msg.content, msg.role, false, false);
659
+ if (msg.role === 'assistant') {
660
+ processCodeBlocks(messageDiv, msg.content);
661
+ }
662
+ });
663
+ const historyItems = document.querySelectorAll('.chat-history-item');
664
+ historyItems.forEach(item => item.classList.remove('active'));
665
+ historyItems[convId].classList.add('active');
666
+ }
667
+
668
+ function saveConversation() {
669
+ if (!currentConversationId && currentConversationId !== 0) {
670
+ conversationHistory.push({
671
+ timestamp: Date.now(),
672
+ messages: []
673
+ });
674
+ currentConversationId = conversationHistory.length - 1;
675
+ }
676
+ localStorage.setItem('chatHistory', JSON.stringify(conversationHistory));
677
+ loadChatHistory();
678
+ }
679
+
680
  function setupEventListeners() {
681
  document.getElementById('chat-form').addEventListener('submit', handleSubmit);
682
  document.getElementById('temperature').addEventListener('input', function() {
 
684
  });
685
  document.getElementById('message-input').addEventListener('input', autoResizeTextarea);
686
  document.getElementById('theme-toggle').addEventListener('click', toggleTheme);
687
+ document.getElementById('sidebar-toggle').addEventListener('click', toggleSidebar);
688
  loadTheme();
689
  }
690
 
691
  function autoResizeTextarea() {
692
  const textarea = document.getElementById('message-input');
693
  textarea.style.height = 'auto';
694
+ textarea.style.height = Math.min(textarea.scrollHeight, 150) + 'px';
695
  }
696
 
697
  async function handleSubmit(e) {
 
712
  }
713
 
714
  addMessage(message, 'user');
715
+ if (currentConversationId === null) {
716
+ saveConversation();
717
+ }
718
+ conversationHistory[currentConversationId].messages.push({ role: 'user', content: message });
719
+ localStorage.setItem('chatHistory', JSON.stringify(conversationHistory));
720
+
721
  messageInput.value = '';
722
  autoResizeTextarea();
723
  showTypingIndicator(true);
 
764
  }
765
 
766
  processCodeBlocks(currentMessageDiv, accumulatedResponse);
767
+ conversationHistory[currentConversationId].messages.push({ role: 'assistant', content: accumulatedResponse });
768
+ localStorage.setItem('chatHistory', JSON.stringify(conversationHistory));
769
  showStatus(`Response generated using ${model}`, 'success');
770
  } catch (error) {
771
  showTypingIndicator(false);
772
  if (currentMessageDiv) {
773
  updateMessage(currentMessageDiv, 'Sorry, I encountered a network error.');
774
+ conversationHistory[currentConversationId].messages.push({ role: 'assistant', content: 'Sorry, I encountered a network error.' });
775
+ localStorage.setItem('chatHistory', JSON.stringify(conversationHistory));
776
  } else {
777
  addMessage('Sorry, I encountered a network error.', 'assistant');
778
  }
 
780
  }
781
  }
782
 
783
+ function addMessage(content, sender, isStreaming = false, save = true) {
784
  const chatContainer = document.getElementById('chat-container');
785
  const messageDiv = document.createElement('div');
786
  messageDiv.className = `message ${sender}`;
 
798
  chatContainer.appendChild(messageDiv);
799
  chatContainer.scrollTop = chatContainer.scrollHeight;
800
 
801
+ if (!isStreaming && save) {
802
+ if (currentConversationId === null) {
803
+ saveConversation();
804
+ }
805
+ conversationHistory[currentConversationId].messages.push({ role: sender, content: content });
806
+ localStorage.setItem('chatHistory', JSON.stringify(conversationHistory));
807
  }
808
  return messageDiv;
809
  }
 
827
  const code = match[2].trim();
828
  const startIndex = match.index;
829
 
 
830
  if (startIndex > lastIndex) {
831
  fragments.push({ type: 'text', content: content.slice(lastIndex, startIndex) });
832
  }
833
 
 
834
  fragments.push({ type: 'code', language, content: code });
835
  currentCodeBlocks.push({ language, content: code });
836
  lastIndex = codeBlockRegex.lastIndex;
837
  }
838
 
 
839
  if (lastIndex < content.length) {
840
  fragments.push({ type: 'text', content: content.slice(lastIndex) });
841
  }
842
 
 
843
  messageContent.innerHTML = '';
844
  fragments.forEach((fragment, index) => {
845
  if (fragment.type === 'text') {
 
883
  script.textContent = `
884
  import { Sandpack } from '@codesandbox/sandpack-react';
885
  import { createRoot } from 'react-dom';
886
+ const root = createRoot(document.getElementById('sandpack-${currentConversationId}-${index}'));
887
  root.render(
888
  React.createElement(Sandpack, {
889
  template: "${fragment.language === 'javascript' ? 'react' : fragment.language}",
 
901
  `;
902
 
903
  const sandboxDiv = document.createElement('div');
904
+ sandboxDiv.id = `sandpack-${currentConversationId}-${index}`;
905
  codeContainer.appendChild(sandboxDiv);
906
  codeContainer.appendChild(script);
907
  messageContent.appendChild(codeContainer);
908
  }
909
  });
 
 
910
  }
911
 
912
  function showTypingIndicator(show) {
 
943
  try:
944
  data = request.get_json()
945
  if not data or 'prompt' not in data or 'model' not in data:
946
+ logging.warning("Chat request missing 'prompt' or 'model' field")
947
  return jsonify({"status": "error", "message": "Prompt and model are required"}), 400
948
 
949
  prompt = data['prompt']
 
956
 
957
  if stream and isinstance(result, requests.Response):
958
  def generate_stream():
959
+ try:
960
+ for chunk in result.iter_content(chunk_size=None):
961
+ yield chunk
962
+ except Exception as e:
963
+ logging.error(f"Streaming error: {e}")
964
+ yield json.dumps({"status": "error", "message": str(e)}).encode()
965
  return Response(generate_stream(), content_type='application/json')
966
  else:
967
+ logging.info(f"Non-streaming chat response generated with model {model}")
968
  return jsonify(result), 200 if result["status"] == "success" else 500
969
  except Exception as e:
970
  logging.error(f"Chat endpoint error: {e}")
 
975
  """Get available models."""
976
  try:
977
  models = ollama_manager.list_models()
978
+ logging.info(f"Returning models: {models}")
979
  return jsonify({
980
  "status": "success",
981
  "models": models,
 
1001
  "status": "unhealthy",
1002
  "error": str(e),
1003
  "timestamp": time.time()
1004
+ }), 503
1005
 
1006
  if __name__ == '__main__':
1007
  app.run(host='0.0.0.0', port=7860, debug=False)