d310h commited on
Commit
beec59b
·
verified ·
1 Parent(s): ec08f70

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +653 -493
index.html CHANGED
@@ -1,542 +1,702 @@
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>Ollama Chat Interface</title>
7
- <style>
8
- /*
9
  * GLOBAL VARIABLES & RESET
10
- * Using CSS Variables for consistent theming and easy maintenance.
11
  */
12
- :root {
13
- --bg-body: #121212;
14
- --bg-panel: #1e1e1e;
15
- --bg-input: #2c2c2c;
16
- --text-main: #e0e0e0;
17
- --text-muted: #a0a0a0;
18
- --accent-color: #008080; /* Matches AutoIt Light Blue */
19
- --accent-hover: #009999;
20
- --border-color: #333;
21
- --msg-user-bg: #008080;
22
- --msg-ai-bg: #2c2c2c;
23
- --font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
24
- --shadow-sm: 0 2px 4px rgba(0,0,0,0.3);
25
- --shadow-md: 0 4px 8px rgba(0,0,0,0.4);
26
- }
27
-
28
- * {
29
- box-sizing: border-box;
30
- margin: 0;
31
- padding: 0;
32
- }
33
-
34
- body {
35
- font-family: var(--font-family);
36
- background-color: var(--bg-body);
37
- color: var(--text-main);
38
- height: 100vh;
39
- display: flex;
40
- flex-direction: column;
41
- overflow: hidden; /* Prevent body scroll, handle inside chat */
42
- }
43
-
44
- /*
45
- * HEADER
46
- * Includes the required "Built with anycoder" link.
47
  */
48
- header {
49
- background-color: var(--bg-panel);
50
- padding: 1rem 1.5rem;
51
- border-bottom: 1px solid var(--border-color);
52
- display: flex;
53
- justify-content: space-between;
54
- align-items: center;
55
- box-shadow: var(--shadow-sm);
56
- z-index: 10;
57
- }
58
-
59
- h1 {
60
- font-size: 1.25rem;
61
- font-weight: 600;
62
- display: flex;
63
- align-items: center;
64
- gap: 10px;
65
- }
66
-
67
- .status-badge {
68
- font-size: 0.75rem;
69
- background-color: #2e7d32;
70
- color: white;
71
- padding: 2px 8px;
72
- border-radius: 12px;
73
- text-transform: uppercase;
74
- letter-spacing: 0.5px;
75
- }
76
-
77
- .built-with {
78
- font-size: 0.85rem;
79
- color: var(--text-muted);
80
- text-decoration: none;
81
- transition: color 0.3s ease;
82
- }
83
-
84
- .built-with:hover {
85
- color: var(--accent-color);
86
- }
87
-
88
- /*
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  * MAIN CHAT AREA
90
- * Mimics the ListView behavior but with better UX.
91
  */
92
- #chat-container {
93
- flex: 1;
94
- overflow-y: auto;
95
- padding: 1.5rem;
96
- display: flex;
97
- flex-direction: column;
98
- gap: 1rem;
99
- scroll-behavior: smooth;
100
- }
101
-
102
- /* Scrollbar Styling */
103
- #chat-container::-webkit-scrollbar {
104
- width: 8px;
105
- }
106
- #chat-container::-webkit-scrollbar-track {
107
- background: var(--bg-body);
108
- }
109
- #chat-container::-webkit-scrollbar-thumb {
110
- background: #444;
111
- border-radius: 4px;
112
- }
113
- #chat-container::-webkit-scrollbar-thumb:hover {
114
- background: #555;
115
- }
116
-
117
- /* Message Bubbles */
118
- .message-row {
119
- display: flex;
120
- width: 100%;
121
- opacity: 0;
122
- animation: fadeIn 0.3s forwards;
123
- }
124
-
125
- .message-row.user {
126
- justify-content: flex-end;
127
- }
128
-
129
- .message-row.ai {
130
- justify-content: flex-start;
131
- }
132
-
133
- .message-bubble {
134
- max-width: 80%;
135
- padding: 12px 16px;
136
- border-radius: 12px;
137
- font-size: 1rem;
138
- line-height: 1.5;
139
- position: relative;
140
- word-wrap: break-word;
141
- }
142
-
143
- .message-row.user .message-bubble {
144
- background-color: var(--msg-user-bg);
145
- color: white;
146
- border-bottom-right-radius: 2px;
147
- box-shadow: var(--shadow-sm);
148
- }
149
-
150
- .message-row.ai .message-bubble {
151
- background-color: var(--msg-ai-bg);
152
- color: var(--text-main);
153
- border-bottom-left-radius: 2px;
154
- border: 1px solid var(--border-color);
155
- box-shadow: var(--shadow-sm);
156
- }
157
-
158
- .timestamp {
159
- font-size: 0.7rem;
160
- opacity: 0.7;
161
- margin-bottom: 4px;
162
- display: block;
163
- }
164
-
165
- /* Typing Indicator */
166
- .typing-indicator {
167
- display: flex;
168
- gap: 4px;
169
- padding: 12px 16px;
170
- background-color: var(--msg-ai-bg);
171
- border-radius: 12px;
172
- width: fit-content;
173
- margin-bottom: 1rem;
174
- display: none; /* Hidden by default */
175
- }
176
-
177
- .typing-indicator span {
178
- width: 8px;
179
- height: 8px;
180
- background-color: var(--text-muted);
181
- border-radius: 50%;
182
- animation: bounce 1.4s infinite ease-in-out both;
183
- }
184
-
185
- .typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
186
- .typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
187
-
188
- /*
 
 
 
 
 
 
189
  * INPUT AREA
190
- * Fixed at bottom, mimics the GUICtrlCreateInput/Button combo.
191
  */
192
- footer {
193
- background-color: var(--bg-panel);
194
- padding: 1rem;
195
- border-top: 1px solid var(--border-color);
196
- display: flex;
197
- gap: 10px;
198
- align-items: center;
199
- }
200
-
201
- #user-input {
202
- flex: 1;
203
- background-color: var(--bg-input);
204
- border: 1px solid var(--border-color);
205
- color: var(--text-main);
206
- padding: 12px 16px;
207
- border-radius: 8px;
208
- font-size: 1rem;
209
- outline: none;
210
- transition: border-color 0.2s, box-shadow 0.2s;
211
- }
212
-
213
- #user-input:focus {
214
- border-color: var(--accent-color);
215
- box-shadow: 0 0 0 2px rgba(0, 128, 128, 0.2);
216
- }
217
-
218
- #send-btn {
219
- background-color: var(--accent-color);
220
- color: white;
221
- border: none;
222
- padding: 0 24px;
223
- border-radius: 8px;
224
- font-size: 1rem;
225
- font-weight: 600;
226
- cursor: pointer;
227
- transition: background-color 0.2s, transform 0.1s;
228
- display: flex;
229
- align-items: center;
230
- gap: 8px;
231
- }
232
-
233
- #send-btn:hover {
234
- background-color: var(--accent-hover);
235
- }
236
-
237
- #send-btn:active {
238
- transform: scale(0.98);
239
- }
240
-
241
- #send-btn:disabled {
242
- background-color: #444;
243
- cursor: not-allowed;
244
- opacity: 0.7;
245
- }
246
-
247
- /*
 
 
 
 
248
  * ANIMATIONS
249
  */
250
- @keyframes fadeIn {
251
- from { opacity: 0; transform: translateY(10px); }
252
- to { opacity: 1; transform: translateY(0); }
253
- }
254
 
255
- @keyframes bounce {
256
- 0%, 80%, 100% { transform: scale(0); }
257
- 40% { transform: scale(1); }
258
- }
259
 
260
- /*
261
  * MODAL / TOAST
262
  */
263
- .toast {
264
- position: fixed;
265
- top: 20px;
266
- left: 50%;
267
- transform: translateX(-50%) translateY(-100px);
268
- background-color: #333;
269
- color: white;
270
- padding: 10px 20px;
271
- border-radius: 8px;
272
- box-shadow: var(--shadow-md);
273
- opacity: 0;
274
- transition: all 0.4s ease;
275
- z-index: 100;
276
- border-left: 4px solid var(--accent-color);
277
- }
278
- .toast.show {
279
- transform: translateX(-50%) translateY(0);
280
- opacity: 1;
281
- }
282
-
283
- /* Responsive */
284
- @media (max-width: 600px) {
285
- h1 span { display: none; }
286
- .message-bubble { max-width: 90%; }
287
- #send-btn { padding: 0 16px; }
288
- }
289
- </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  </head>
 
291
  <body>
292
 
293
- <header>
294
- <h1>
295
- <span>🤖</span> Ollama Chat Interface
296
- <span class="status-badge" id="connection-status">Connecting...</span>
297
- </h1>
298
- <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="built-with">Built with anycoder</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
299
  </header>
300
 
301
- <main id="chat-container">
302
- <!-- Initial Welcome Message -->
303
- <div class="message-row ai">
304
- <div class="message-bubble">
305
- <span class="timestamp">System</span>
306
- Welcome to Ollama Chat! This is a modern rewrite of your AutoIt script. Please ensure an Ollama instance is running locally on port 11434.
307
- </div>
308
  </div>
 
309
 
310
- <!-- Typing Indicator -->
311
- <div class="typing-indicator" id="typing-indicator">
312
- <span></span>
313
- <span></span>
314
- <span></span>
315
- </div>
316
- </main>
317
 
318
  <footer>
319
- <input type="text" id="user-input" placeholder="Type a message..." autocomplete="off">
320
- <button id="send-btn">
321
- <span>Send</span>
322
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
323
- </button>
324
  </footer>
325
-
326
- <div id="toast" class="toast">Message sent</div>
327
-
328
- <script>
329
- /**
330
- * Ollama Chat Interface Logic
331
- * Replaces AutoIt GUI logic with Vanilla JS DOM manipulation.
332
- */
333
-
334
- // --- Configuration ---
335
- const OLLAMA_API_URL = 'http://localhost:11434/api/chat';
336
- const DEFAULT_MODEL = 'llama3.2'; // A standard, fast model available on most installs
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
 
338
- // --- DOM Elements ---
339
- const chatContainer = document.getElementById('chat-container');
340
- const userInput = document.getElementById('user-input');
341
- const sendBtn = document.getElementById('send-btn');
342
- const typingIndicator = document.getElementById('typing-indicator');
343
- const statusBadge = document.getElementById('connection-status');
344
- const toast = document.getElementById('toast');
345
-
346
- // --- State ---
347
- let isGenerating = false;
348
- let messages = []; // In-memory history
349
-
350
- // --- Initialization ---
351
- window.addEventListener('DOMContentLoaded', () => {
352
- checkConnection();
353
- userInput.focus();
354
-
355
- // Auto-resize input (optional modern touch)
356
- userInput.addEventListener('input', function() {
357
- this.style.height = 'auto';
358
- this.style.height = (this.scrollHeight) + 'px';
359
- if(this.value === '') this.style.height = 'auto';
360
- });
361
  });
362
 
363
- // --- Event Listeners ---
364
- sendBtn.addEventListener('click', handleSend);
365
-
366
- userInput.addEventListener('keydown', (e) => {
367
- if (e.key === 'Enter' && !e.shiftKey) {
368
- e.preventDefault();
369
- handleSend();
370
- }
371
  });
372
-
373
- // --- Core Functions ---
374
-
375
- async function checkConnection() {
376
- try {
377
- const response = await fetch('http://localhost:11434/api/tags');
378
- if (response.ok) {
379
- statusBadge.textContent = 'Online';
380
- statusBadge.style.backgroundColor = '#2e7d32';
381
- showToast('Connected to Ollama');
382
- } else {
383
- throw new Error('Not connected');
 
 
 
 
 
 
 
 
 
 
 
 
 
384
  }
385
- } catch (error) {
386
- statusBadge.textContent = 'Offline';
387
- statusBadge.style.backgroundColor = '#c62828';
388
- showToast('Ollama is not running. Please start it via terminal.');
389
  }
 
 
 
390
  }
 
391
 
392
- async function handleSend() {
393
- if (isGenerating) return;
394
-
395
- const text = userInput.value.trim();
396
- if (!text) return;
397
-
398
- // 1. UI Updates: Add User Message
399
- addMessageToUI('user', text);
400
- userInput.value = '';
401
- userInput.style.height = 'auto'; // Reset height
402
- isGenerating = true;
403
- toggleLoading(true);
404
-
405
- // 2. Prepare Payload
406
- messages.push({ role: 'user', content: text });
407
-
408
- try {
409
- // 3. API Call
410
- const response = await fetch(OLLAMA_API_URL, {
411
- method: 'POST',
412
- headers: { 'Content-Type': 'application/json' },
413
- body: JSON.stringify({
414
- model: DEFAULT_MODEL,
415
- messages: messages,
416
- stream: true // Enable streaming for real-time feel
417
- })
418
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
419
 
420
- if (!response.ok) throw new Error('API request failed');
421
-
422
- // 4. Handle Streaming Response
423
- const reader = response.body.getReader();
424
- const decoder = new TextDecoder();
425
- let aiResponseText = '';
426
-
427
- // Create a temporary AI message bubble
428
- const aiBubble = addMessageToUI('ai', '', true); // true = placeholder
429
-
430
- while (true) {
431
- const { done, value } = await reader.read();
432
- if (done) break;
433
-
434
- const chunk = decoder.decode(value, { stream: true });
435
- const lines = chunk.split('\n');
436
-
437
- for (const line of lines) {
438
- if (line.startsWith('data: ')) {
439
- const jsonStr = line.slice(6);
440
- if (jsonStr === '[DONE]') break;
441
-
442
- try {
443
- const data = JSON.parse(jsonStr);
444
- const content = data.message?.content || '';
445
- aiResponseText += content;
446
- aiBubble.querySelector('.content-text').innerHTML = escapeHtml(aiResponseText);
447
-
448
- // Auto-scroll to bottom
449
- chatContainer.scrollTop = chatContainer.scrollHeight;
450
- } catch (e) {
451
- console.error('Error parsing stream line', e);
452
- }
453
  }
454
  }
455
  }
456
-
457
- // 5. Finalize
458
- messages.push({ role: 'assistant', content: aiResponseText });
459
- toggleLoading(false);
460
-
461
- } catch (error) {
462
- console.error(error);
463
- addMessageToUI('ai', 'Error: Could not connect to Ollama. Is it running?');
464
- toggleLoading(false);
465
- isGenerating = false;
466
- }
467
- }
468
-
469
- // --- UI Helpers ---
470
-
471
- function addMessageToUI(role, text, isPlaceholder = false) {
472
- const row = document.createElement('div');
473
- row.className = `message-row ${role}`;
474
-
475
- const bubble = document.createElement('div');
476
- bubble.className = 'message-bubble';
477
-
478
- // Add timestamp
479
- const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
480
- const timestamp = document.createElement('span');
481
- timestamp.className = 'timestamp';
482
- timestamp.textContent = time;
483
- bubble.appendChild(timestamp);
484
-
485
- // Add Content
486
- if (isPlaceholder) {
487
- const contentSpan = document.createElement('span');
488
- contentSpan.className = 'content-text';
489
- bubble.appendChild(contentSpan);
490
- } else {
491
- const contentSpan = document.createElement('span');
492
- contentSpan.className = 'content-text';
493
- contentSpan.innerHTML = escapeHtml(text);
494
- bubble.appendChild(contentSpan);
495
  }
496
 
497
- row.appendChild(bubble);
498
-
499
- // Insert before the typing indicator
500
- chatContainer.insertBefore(row, typingIndicator);
501
-
502
- // Scroll to bottom
503
- chatContainer.scrollTop = chatContainer.scrollHeight;
504
 
505
- return bubble;
 
 
 
 
506
  }
 
507
 
508
- function toggleLoading(show) {
509
- if (show) {
510
- typingIndicator.style.display = 'flex';
511
- sendBtn.disabled = true;
512
- sendBtn.innerHTML = '<span>...</span>';
513
- } else {
514
- typingIndicator.style.display = 'none';
515
- sendBtn.disabled = false;
516
- sendBtn.innerHTML = '<span>Send</span><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>';
517
- userInput.focus();
518
- }
519
- }
520
 
521
- function showToast(msg) {
522
- toast.textContent = msg;
523
- toast.classList.add('show');
524
- setTimeout(() => {
525
- toast.classList.remove('show');
526
- }, 3000);
527
- }
528
-
529
- // Security: Prevent XSS
530
- function escapeHtml(text) {
531
- if (!text) return text;
532
- return text
533
- .replace(/&/g, "&amp;")
534
- .replace(/</g, "&lt;")
535
- .replace(/>/g, "&gt;")
536
- .replace(/"/g, "&quot;")
537
- .replace(/'/g, "&#039;");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
538
  }
 
539
 
540
- </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
541
  </body>
542
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
+
4
  <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Ollama Chat Interface</title>
8
+ <style>
9
+ /*
10
  * GLOBAL VARIABLES & RESET
 
11
  */
12
+ :root {
13
+ --bg-body: #121212;
14
+ --bg-panel: #1e1e1e;
15
+ --bg-input: #2c2c2c;
16
+ --text-main: #e0e0e0;
17
+ --text-muted: #a0a0a0;
18
+ --accent-color: #008080;
19
+ --accent-hover: #009999;
20
+ --border-color: #333;
21
+ --msg-user-bg: #008080;
22
+ --msg-ai-bg: #2c2c2c;
23
+ --font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
24
+ --shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.3);
25
+ --shadow-md: 0 4px 8px rgba(0, 0, 0, 0.4);
26
+ --sidebar-width: 280px;
27
+ }
28
+
29
+ * {
30
+ box-sizing: border-box;
31
+ margin: 0;
32
+ padding: 0;
33
+ }
34
+
35
+ body {
36
+ font-family: var(--font-family);
37
+ background-color: var(--bg-body);
38
+ color: var(--text-main);
39
+ height: 100vh;
40
+ display: flex;
41
+ overflow: hidden;
42
+ }
43
+
44
+ /*
45
+ * SIDEBAR
46
+ * Settings and Model Selection
47
  */
48
+ aside {
49
+ width: var(--sidebar-width);
50
+ background-color: var(--bg-panel);
51
+ border-right: 1px solid var(--border-color);
52
+ display: flex;
53
+ flex-direction: column;
54
+ padding: 1.5rem;
55
+ gap: 2rem;
56
+ z-index: 10;
57
+ transition: transform 0.3s ease;
58
+ }
59
+
60
+ aside h2 {
61
+ font-size: 1rem;
62
+ text-transform: uppercase;
63
+ letter-spacing: 1px;
64
+ color: var(--text-muted);
65
+ margin-bottom: 0.5rem;
66
+ }
67
+
68
+ .control-group {
69
+ display: flex;
70
+ flex-direction: column;
71
+ gap: 0.5rem;
72
+ }
73
+
74
+ label {
75
+ font-size: 0.85rem;
76
+ color: var(--text-muted);
77
+ }
78
+
79
+ select, textarea {
80
+ background-color: var(--bg-input);
81
+ border: 1px solid var(--border-color);
82
+ color: var(--text-main);
83
+ padding: 0.75rem;
84
+ border-radius: 6px;
85
+ font-family: inherit;
86
+ font-size: 0.9rem;
87
+ outline: none;
88
+ resize: none;
89
+ }
90
+
91
+ select:focus, textarea:focus {
92
+ border-color: var(--accent-color);
93
+ }
94
+
95
+ .status-indicator {
96
+ display: flex;
97
+ align-items: center;
98
+ gap: 8px;
99
+ font-size: 0.85rem;
100
+ padding: 0.5rem;
101
+ background: rgba(255, 255, 255, 0.05);
102
+ border-radius: 6px;
103
+ }
104
+
105
+ .status-dot {
106
+ width: 8px;
107
+ height: 8px;
108
+ border-radius: 50%;
109
+ background-color: #555;
110
+ transition: background-color 0.3s;
111
+ }
112
+
113
+ .status-dot.online { background-color: #2e7d32; box-shadow: 0 0 5px #2e7d32; }
114
+ .status-dot.offline { background-color: #c62828; }
115
+
116
+ /*
117
  * MAIN CHAT AREA
 
118
  */
119
+ main {
120
+ flex: 1;
121
+ display: flex;
122
+ flex-direction: column;
123
+ position: relative;
124
+ }
125
+
126
+ #chat-container {
127
+ flex: 1;
128
+ overflow-y: auto;
129
+ padding: 2rem;
130
+ display: flex;
131
+ flex-direction: column;
132
+ gap: 1.5rem;
133
+ scroll-behavior: smooth;
134
+ }
135
+
136
+ /* Scrollbar Styling */
137
+ #chat-container::-webkit-scrollbar {
138
+ width: 8px;
139
+ }
140
+
141
+ #chat-container::-webkit-scrollbar-track {
142
+ background: var(--bg-body);
143
+ }
144
+
145
+ #chat-container::-webkit-scrollbar-thumb {
146
+ background: #444;
147
+ border-radius: 4px;
148
+ }
149
+
150
+ /* Message Bubbles */
151
+ .message-row {
152
+ display: flex;
153
+ width: 100%;
154
+ opacity: 0;
155
+ animation: fadeIn 0.3s forwards;
156
+ }
157
+
158
+ .message-row.user {
159
+ justify-content: flex-end;
160
+ }
161
+
162
+ .message-row.ai {
163
+ justify-content: flex-start;
164
+ }
165
+
166
+ .message-bubble {
167
+ max-width: 80%;
168
+ padding: 1rem 1.25rem;
169
+ border-radius: 12px;
170
+ font-size: 1rem;
171
+ line-height: 1.6;
172
+ position: relative;
173
+ word-wrap: break-word;
174
+ }
175
+
176
+ .message-row.user .message-bubble {
177
+ background-color: var(--msg-user-bg);
178
+ color: white;
179
+ border-bottom-right-radius: 2px;
180
+ box-shadow: var(--shadow-sm);
181
+ }
182
+
183
+ .message-row.ai .message-bubble {
184
+ background-color: var(--msg-ai-bg);
185
+ color: var(--text-main);
186
+ border-bottom-left-radius: 2px;
187
+ border: 1px solid var(--border-color);
188
+ box-shadow: var(--shadow-sm);
189
+ }
190
+
191
+ .timestamp {
192
+ font-size: 0.7rem;
193
+ opacity: 0.7;
194
+ margin-bottom: 4px;
195
+ display: block;
196
+ }
197
+
198
+ /* Typing Indicator */
199
+ .typing-indicator {
200
+ display: flex;
201
+ gap: 4px;
202
+ padding: 12px 16px;
203
+ background-color: var(--msg-ai-bg);
204
+ border-radius: 12px;
205
+ width: fit-content;
206
+ margin-left: 1rem;
207
+ display: none;
208
+ }
209
+
210
+ .typing-indicator span {
211
+ width: 8px;
212
+ height: 8px;
213
+ background-color: var(--text-muted);
214
+ border-radius: 50%;
215
+ animation: bounce 1.4s infinite ease-in-out both;
216
+ }
217
+
218
+ .typing-indicator span:nth-child(1) { animation-delay: -0.32s; }
219
+ .typing-indicator span:nth-child(2) { animation-delay: -0.16s; }
220
+
221
+ /*
222
  * INPUT AREA
 
223
  */
224
+ footer {
225
+ background-color: var(--bg-panel);
226
+ padding: 1.5rem;
227
+ border-top: 1px solid var(--border-color);
228
+ display: flex;
229
+ gap: 10px;
230
+ align-items: flex-end;
231
+ }
232
+
233
+ #user-input {
234
+ flex: 1;
235
+ background-color: var(--bg-input);
236
+ border: 1px solid var(--border-color);
237
+ color: var(--text-main);
238
+ padding: 12px 16px;
239
+ border-radius: 8px;
240
+ font-size: 1rem;
241
+ outline: none;
242
+ transition: border-color 0.2s, box-shadow 0.2s;
243
+ min-height: 50px;
244
+ max-height: 150px;
245
+ overflow-y: auto;
246
+ }
247
+
248
+ #user-input:focus {
249
+ border-color: var(--accent-color);
250
+ box-shadow: 0 0 0 2px rgba(0, 128, 128, 0.2);
251
+ }
252
+
253
+ #send-btn {
254
+ background-color: var(--accent-color);
255
+ color: white;
256
+ border: none;
257
+ padding: 0 24px;
258
+ border-radius: 8px;
259
+ font-size: 1rem;
260
+ font-weight: 600;
261
+ cursor: pointer;
262
+ transition: background-color 0.2s, transform 0.1s;
263
+ height: 50px;
264
+ display: flex;
265
+ align-items: center;
266
+ gap: 8px;
267
+ }
268
+
269
+ #send-btn:hover {
270
+ background-color: var(--accent-hover);
271
+ }
272
+
273
+ #send-btn:active {
274
+ transform: scale(0.98);
275
+ }
276
+
277
+ #send-btn:disabled {
278
+ background-color: #444;
279
+ cursor: not-allowed;
280
+ opacity: 0.7;
281
+ }
282
+
283
+ /*
284
  * ANIMATIONS
285
  */
286
+ @keyframes fadeIn {
287
+ from { opacity: 0; transform: translateY(10px); }
288
+ to { opacity: 1; transform: translateY(0); }
289
+ }
290
 
291
+ @keyframes bounce {
292
+ 0%, 80%, 100% { transform: scale(0); }
293
+ 40% { transform: scale(1); }
294
+ }
295
 
296
+ /*
297
  * MODAL / TOAST
298
  */
299
+ .toast {
300
+ position: fixed;
301
+ top: 20px;
302
+ left: 50%;
303
+ transform: translateX(-50%) translateY(-100px);
304
+ background-color: #333;
305
+ color: white;
306
+ padding: 12px 24px;
307
+ border-radius: 8px;
308
+ box-shadow: var(--shadow-md);
309
+ opacity: 0;
310
+ transition: all 0.4s ease;
311
+ z-index: 100;
312
+ border-left: 4px solid var(--accent-color);
313
+ display: flex;
314
+ align-items: center;
315
+ gap: 10px;
316
+ }
317
+
318
+ .toast.show {
319
+ transform: translateX(-50%) translateY(0);
320
+ opacity: 1;
321
+ }
322
+
323
+ /* Responsive */
324
+ @media (max-width: 768px) {
325
+ aside {
326
+ position: fixed;
327
+ height: 100%;
328
+ left: -280px;
329
+ box-shadow: 2px 0 10px rgba(0,0,0,0.5);
330
+ }
331
+ aside.open {
332
+ left: 0;
333
+ }
334
+ .mobile-menu-btn {
335
+ display: block !important;
336
+ }
337
+ .message-bubble { max-width: 90%; }
338
+ }
339
+
340
+ .mobile-menu-btn {
341
+ display: none;
342
+ position: absolute;
343
+ top: 1rem;
344
+ left: 1rem;
345
+ z-index: 20;
346
+ background: var(--bg-panel);
347
+ border: 1px solid var(--border-color);
348
+ color: var(--text-main);
349
+ padding: 0.5rem;
350
+ border-radius: 4px;
351
+ }
352
+ </style>
353
  </head>
354
+
355
  <body>
356
 
357
+ <!-- Mobile Menu Toggle -->
358
+ <button class="mobile-menu-btn" id="menu-toggle">⚙️</button>
359
+
360
+ <!-- Sidebar -->
361
+ <aside id="sidebar">
362
+ <div class="status-indicator">
363
+ <div class="status-dot" id="status-dot"></div>
364
+ <span id="status-text">Checking...</span>
365
+ </div>
366
+
367
+ <div class="control-group">
368
+ <h2>Settings</h2>
369
+ <label for="model-select">Ollama Model</label>
370
+ <select id="model-select">
371
+ <option value="llama3.2">llama3.2 (Default)</option>
372
+ <option value="codellama">CodeLlama</option>
373
+ <option value="mistral">Mistral</option>
374
+ <option value="llava">LLaVA (Vision)</option>
375
+ </select>
376
+ </div>
377
+
378
+ <div class="control-group">
379
+ <label for="system-prompt">System Prompt</label>
380
+ <textarea id="system-prompt" rows="4">You are a helpful AI assistant.</textarea>
381
+ </div>
382
+
383
+ <div class="control-group" style="margin-top: auto;">
384
+ <button id="refresh-models-btn" style="background: var(--bg-input); border: 1px solid var(--border-color); color: var(--text-main); padding: 8px; border-radius: 4px; cursor: pointer;">Refresh Models</button>
385
+ </div>
386
+ </aside>
387
+
388
+ <!-- Main Chat -->
389
+ <main>
390
+ <header style="padding: 1rem 1.5rem; border-bottom: 1px solid var(--border-color); display: flex; align-items: center;">
391
+ <h1 style="font-size: 1.2rem; font-weight: 600; display: flex; align-items: center; gap: 10px;">
392
+ <span>🤖</span> Ollama Chat
393
+ </h1>
394
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" style="margin-left: auto; font-size: 0.8rem; color: var(--text-muted); text-decoration: none;">Built with anycoder</a>
395
  </header>
396
 
397
+ <div id="chat-container">
398
+ <!-- Initial Welcome Message -->
399
+ <div class="message-row ai">
400
+ <div class="message-bubble">
401
+ <span class="timestamp">System</span>
402
+ Welcome! Ensure Ollama is running locally on port 11434. I am connected and ready to chat.
 
403
  </div>
404
+ </div>
405
 
406
+ <!-- Typing Indicator -->
407
+ <div class="typing-indicator" id="typing-indicator">
408
+ <span></span>
409
+ <span></span>
410
+ <span></span>
411
+ </div>
412
+ </div>
413
 
414
  <footer>
415
+ <textarea id="user-input" placeholder="Type a message... (Shift+Enter for new line)" rows="1"></textarea>
416
+ <button id="send-btn">
417
+ <span>Send</span>
418
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>
419
+ </button>
420
  </footer>
421
+ </main>
422
+
423
+ <div id="toast" class="toast">
424
+ <span id="toast-icon">ℹ️</span>
425
+ <span id="toast-message">Notification</span>
426
+ </div>
427
+
428
+ <script>
429
+ /**
430
+ * OLLAMA CHAT LOGIC
431
+ * Handles API communication, state management, and UI updates.
432
+ */
433
+
434
+ // --- Configuration ---
435
+ const OLLAMA_API_URL = 'http://localhost:11434/api/chat';
436
+ const OLLAMA_TAGS_URL = 'http://localhost:11434/api/tags';
437
+
438
+ // --- DOM Elements ---
439
+ const chatContainer = document.getElementById('chat-container');
440
+ const userInput = document.getElementById('user-input');
441
+ const sendBtn = document.getElementById('send-btn');
442
+ const typingIndicator = document.getElementById('typing-indicator');
443
+ const statusDot = document.getElementById('status-dot');
444
+ const statusText = document.getElementById('status-text');
445
+ const toast = document.getElementById('toast');
446
+ const modelSelect = document.getElementById('model-select');
447
+ const systemPrompt = document.getElementById('system-prompt');
448
+ const refreshModelsBtn = document.getElementById('refresh-models-btn');
449
+ const menuToggle = document.getElementById('menu-toggle');
450
+ const sidebar = document.getElementById('sidebar');
451
+
452
+ // --- State ---
453
+ let isGenerating = false;
454
+ let messages = []; // Conversation history
455
+
456
+ // --- Initialization ---
457
+ window.addEventListener('DOMContentLoaded', () => {
458
+ checkConnection();
459
+ userInput.focus();
460
 
461
+ // Auto-resize textarea
462
+ userInput.addEventListener('input', function() {
463
+ this.style.height = 'auto';
464
+ this.style.height = (this.scrollHeight) + 'px';
465
+ if(this.value === '') this.style.height = 'auto';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
  });
467
 
468
+ // Sidebar toggle for mobile
469
+ menuToggle.addEventListener('click', () => {
470
+ sidebar.classList.toggle('open');
 
 
 
 
 
471
  });
472
+ });
473
+
474
+ // --- Event Listeners ---
475
+ sendBtn.addEventListener('click', handleSend);
476
+
477
+ userInput.addEventListener('keydown', (e) => {
478
+ if (e.key === 'Enter' && !e.shiftKey) {
479
+ e.preventDefault();
480
+ handleSend();
481
+ }
482
+ });
483
+
484
+ refreshModelsBtn.addEventListener('click', fetchModels);
485
+
486
+ // --- Core Functions ---
487
+
488
+ async function checkConnection() {
489
+ try {
490
+ const response = await fetch(OLLAMA_TAGS_URL);
491
+ if (response.ok) {
492
+ updateStatus(true);
493
+ showToast('Connected to Ollama');
494
+ // Only fetch models if we haven't populated them yet
495
+ if(modelSelect.options.length === 1) {
496
+ fetchModels();
497
  }
498
+ } else {
499
+ throw new Error('Not connected');
 
 
500
  }
501
+ } catch (error) {
502
+ updateStatus(false);
503
+ showToast('Ollama is not running.', true);
504
  }
505
+ }
506
 
507
+ async function fetchModels() {
508
+ try {
509
+ const response = await fetch(OLLAMA_TAGS_URL);
510
+ const data = await response.json();
511
+
512
+ // Clear existing options except default
513
+ modelSelect.innerHTML = '<option value="">Loading...</option>';
514
+
515
+ if (data.models && data.models.length > 0) {
516
+ modelSelect.innerHTML = '';
517
+ data.models.forEach(model => {
518
+ const option = document.createElement('option');
519
+ option.value = model.name;
520
+ option.textContent = model.name;
521
+ modelSelect.appendChild(option);
 
 
 
 
 
 
 
 
 
 
 
522
  });
523
+ showToast(`Loaded ${data.models.length} models`);
524
+ } else {
525
+ modelSelect.innerHTML = '<option value="">No models found</option>';
526
+ }
527
+ } catch (error) {
528
+ console.error(error);
529
+ showToast('Failed to fetch models', true);
530
+ }
531
+ }
532
+
533
+ async function handleSend() {
534
+ if (isGenerating) return;
535
+
536
+ const text = userInput.value.trim();
537
+ if (!text) return;
538
+
539
+ // 1. UI Updates
540
+ addMessageToUI('user', text);
541
+ userInput.value = '';
542
+ userInput.style.height = 'auto'; // Reset height
543
+ isGenerating = true;
544
+ toggleLoading(true);
545
+
546
+ // 2. Prepare Payload
547
+ // We prepend the system prompt to the history for context
548
+ const history = [
549
+ { role: 'system', content: systemPrompt.value },
550
+ ...messages
551
+ ];
552
+
553
+ try {
554
+ const response = await fetch(OLLAMA_API_URL, {
555
+ method: 'POST',
556
+ headers: { 'Content-Type': 'application/json' },
557
+ body: JSON.stringify({
558
+ model: modelSelect.value || 'llama3.2',
559
+ messages: history,
560
+ stream: true
561
+ })
562
+ });
563
 
564
+ if (!response.ok) throw new Error('API request failed');
565
+
566
+ // 3. Handle Streaming
567
+ const reader = response.body.getReader();
568
+ const decoder = new TextDecoder();
569
+ let aiResponseText = '';
570
+
571
+ // Create a placeholder AI message
572
+ const aiBubble = addMessageToUI('ai', '', true);
573
+
574
+ while (true) {
575
+ const { done, value } = await reader.read();
576
+ if (done) break;
577
+
578
+ const chunk = decoder.decode(value, { stream: true });
579
+ const lines = chunk.split('\n');
580
+
581
+ for (const line of lines) {
582
+ if (line.startsWith('data: ')) {
583
+ const jsonStr = line.slice(6);
584
+ if (jsonStr === '[DONE]') break;
585
+
586
+ try {
587
+ const data = JSON.parse(jsonStr);
588
+ const content = data.message?.content || '';
589
+ aiResponseText += content;
590
+ aiBubble.querySelector('.content-text').innerHTML = escapeHtml(aiResponseText);
591
+ chatContainer.scrollTop = chatContainer.scrollHeight;
592
+ } catch (e) {
593
+ console.error('Error parsing stream', e);
 
 
 
594
  }
595
  }
596
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
597
  }
598
 
599
+ // 4. Save to history
600
+ messages.push({ role: 'user', content: text });
601
+ messages.push({ role: 'assistant', content: aiResponseText });
 
 
 
 
602
 
603
+ } catch (error) {
604
+ console.error(error);
605
+ addMessageToUI('ai', 'Error: Could not connect to Ollama. Is it running on port 11434?');
606
+ toggleLoading(false);
607
+ isGenerating = false;
608
  }
609
+ }
610
 
611
+ // --- UI Helpers ---
 
 
 
 
 
 
 
 
 
 
 
612
 
613
+ function addMessageToUI(role, text, isPlaceholder = false) {
614
+ const row = document.createElement('div');
615
+ row.className = `message-row ${role}`;
616
+
617
+ const bubble = document.createElement('div');
618
+ bubble.className = 'message-bubble';
619
+
620
+ const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
621
+ const timestamp = document.createElement('span');
622
+ timestamp.className = 'timestamp';
623
+ timestamp.textContent = time;
624
+ bubble.appendChild(timestamp);
625
+
626
+ if (isPlaceholder) {
627
+ const contentSpan = document.createElement('span');
628
+ contentSpan.className = 'content-text';
629
+ bubble.appendChild(contentSpan);
630
+ } else {
631
+ const contentSpan = document.createElement('span');
632
+ contentSpan.className = 'content-text';
633
+ contentSpan.innerHTML = escapeHtml(text);
634
+ bubble.appendChild(contentSpan);
635
+ }
636
+
637
+ row.appendChild(bubble);
638
+
639
+ // Insert before the typing indicator
640
+ chatContainer.insertBefore(row, typingIndicator);
641
+
642
+ chatContainer.scrollTop = chatContainer.scrollHeight;
643
+
644
+ return bubble;
645
+ }
646
+
647
+ function toggleLoading(show) {
648
+ if (show) {
649
+ typingIndicator.style.display = 'flex';
650
+ sendBtn.disabled = true;
651
+ sendBtn.innerHTML = '<span>...</span>';
652
+ sidebar.style.opacity = '0.5';
653
+ sidebar.style.pointerEvents = 'none';
654
+ } else {
655
+ typingIndicator.style.display = 'none';
656
+ sendBtn.disabled = false;
657
+ sendBtn.innerHTML = '<span>Send</span><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"></line><polygon points="22 2 15 22 11 13 2 9 22 2"></polygon></svg>';
658
+ userInput.focus();
659
+ sidebar.style.opacity = '1';
660
+ sidebar.style.pointerEvents = 'auto';
661
  }
662
+ }
663
 
664
+ function showToast(msg, isError = false) {
665
+ const toastEl = document.getElementById('toast');
666
+ const toastMsg = document.getElementById('toast-message');
667
+ const toastIcon = document.getElementById('toast-icon');
668
+
669
+ toastMsg.textContent = msg;
670
+ toastIcon.textContent = isError ? '⚠️' : '✅';
671
+ toastEl.style.borderLeftColor = isError ? '#c62828' : 'var(--accent-color)';
672
+
673
+ toastEl.classList.add('show');
674
+ setTimeout(() => {
675
+ toastEl.classList.remove('show');
676
+ }, 3000);
677
+ }
678
+
679
+ function updateStatus(isOnline) {
680
+ if (isOnline) {
681
+ statusDot.className = 'status-dot online';
682
+ statusText.textContent = 'Online';
683
+ statusText.style.color = '#2e7d32';
684
+ } else {
685
+ statusDot.className = 'status-dot offline';
686
+ statusText.textContent = 'Offline';
687
+ statusText.style.color = '#c62828';
688
+ }
689
+ }
690
+
691
+ function escapeHtml(text) {
692
+ if (!text) return text;
693
+ return text
694
+ .replace(/&/g, "&amp;")
695
+ .replace(/</g, "&lt;")
696
+ .replace(/>/g, "&gt;")
697
+ .replace(/"/g, "&quot;")
698
+ .replace(/'/g, "&#039;");
699
+ }
700
+ </script>
701
  </body>
702
  </html>