Priyansh Saxena commited on
Commit
5c00819
·
1 Parent(s): 14cc93b

feat: chat interface upgrades

Browse files
Files changed (3) hide show
  1. static/app.js +290 -331
  2. static/styles.css +554 -669
  3. templates/index.html +100 -80
static/app.js CHANGED
@@ -1,387 +1,346 @@
 
 
 
 
 
1
  let chatHistory = [];
2
- let messageCount = 0;
3
- let useGemini = false; // Track current LLM choice
4
-
5
- // Initialize Gemini toggle
6
- document.addEventListener('DOMContentLoaded', function() {
7
- const geminiToggle = document.getElementById('geminiToggle');
8
- const toggleLabel = document.querySelector('.toggle-label');
9
-
10
- // Load saved preference
11
- useGemini = localStorage.getItem('useGemini') === 'true';
12
- geminiToggle.checked = useGemini;
13
- updateToggleLabel();
14
-
15
- // Handle toggle changes
16
- geminiToggle.addEventListener('change', function() {
17
- useGemini = this.checked;
18
- localStorage.setItem('useGemini', useGemini.toString());
19
- updateToggleLabel();
20
- console.log(`Switched to ${useGemini ? 'Gemini' : 'Ollama'} mode`);
21
-
22
- // Show confirmation
23
- showStatus(`Switched to ${useGemini ? 'Gemini (Cloud AI)' : 'Ollama (Local AI)'} mode`, 'info');
24
-
25
- // Refresh status to reflect changes
26
- checkStatus();
27
- });
28
  });
29
-
30
- function updateToggleLabel() {
31
- const toggleLabel = document.querySelector('.toggle-label');
32
- if (toggleLabel) {
33
- toggleLabel.textContent = `AI Model: ${useGemini ? 'Gemini' : 'Ollama'}`;
34
- }
 
 
 
 
 
 
 
 
 
35
  }
36
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
37
  async function checkStatus() {
 
 
38
  try {
39
- const response = await fetch('/status');
40
- const status = await response.json();
41
-
42
- const statusDiv = document.getElementById('status');
43
-
44
- if (status.enabled && status.gemini_configured) {
45
- statusDiv.className = 'status online';
46
- statusDiv.innerHTML = '<span>Research systems online</span>' +
47
- '<div style="margin-top: 0.5rem; font-size: 0.85rem; opacity: 0.8;">' +
48
- 'Tools: ' + status.tools_available.join(' • ') + '</div>';
49
  } else {
50
- statusDiv.className = 'status offline';
51
- statusDiv.innerHTML = '<span>Limited mode - Configure GEMINI_API_KEY for full functionality</span>' +
52
- '<div style="margin-top: 0.5rem; font-size: 0.85rem; opacity: 0.8;">' +
53
- 'Available: ' + status.tools_available.join(' • ') + '</div>';
54
  }
55
- } catch (error) {
56
- const statusDiv = document.getElementById('status');
57
- statusDiv.className = 'status offline';
58
- statusDiv.innerHTML = '<span>Connection error</span>';
59
  }
60
  }
61
-
62
  async function sendQuery() {
63
- const input = document.getElementById('queryInput');
64
  const sendBtn = document.getElementById('sendBtn');
65
- const loadingIndicator = document.getElementById('loadingIndicator');
66
- const statusIndicator = document.getElementById('statusIndicator');
67
- const statusText = document.getElementById('statusText');
68
- const query = input.value.trim();
69
-
70
- if (!query) {
71
- showStatus('Please enter a research query', 'warning');
72
- return;
73
- }
74
-
75
- console.log('Sending research query');
76
  addMessage('user', query);
77
- input.value = '';
78
-
79
- // Update UI states
 
80
  sendBtn.disabled = true;
81
- sendBtn.innerHTML = '<span class="loading">Processing</span>';
82
- loadingIndicator.classList.add('active');
83
- showStatus('Initializing research...', 'processing');
84
-
85
  try {
86
- console.log('Starting streaming API request...');
87
- const requestStart = Date.now();
88
-
89
- // Create an AbortController for manual timeout control
90
  const controller = new AbortController();
91
- const timeoutId = setTimeout(() => {
92
- console.log('Manual timeout after 5 minutes');
93
- controller.abort();
94
- }, 300000); // 5 minute timeout instead of default browser timeout
95
-
96
- // Use fetch with streaming for POST requests with body
97
- const response = await fetch('/query/stream', {
98
  method: 'POST',
99
- headers: {
100
- 'Content-Type': 'application/json',
101
- 'Accept': 'text/event-stream',
102
- 'Cache-Control': 'no-cache'
103
- },
104
- body: JSON.stringify({
105
- query,
106
- chat_history: chatHistory,
107
- use_gemini: useGemini
108
- }),
109
  signal: controller.signal,
110
- // Disable browser's default timeout behavior
111
- keepalive: true
112
  });
113
-
114
- // Clear the timeout since we got a response
115
- clearTimeout(timeoutId);
116
-
117
- if (!response.ok) {
118
- throw new Error('Request failed with status ' + response.status);
119
- }
120
-
121
- const reader = response.body.getReader();
122
  const decoder = new TextDecoder();
123
- let buffer = '';
124
-
125
- while (true) {
126
  const { done, value } = await reader.read();
127
  if (done) break;
128
-
129
  buffer += decoder.decode(value, { stream: true });
130
  const lines = buffer.split('\n');
131
- buffer = lines.pop(); // Keep incomplete line in buffer
132
-
133
  for (const line of lines) {
134
- if (line.startsWith('data: ')) {
135
- try {
136
- const data = JSON.parse(line.substring(6));
137
-
138
- if (data.type === 'status') {
139
- showStatus(data.message, 'processing');
140
- updateProgress(data.progress);
141
- // Also update the loading text
142
- const loadingText = document.getElementById('loadingText');
143
- if (loadingText) {
144
- loadingText.textContent = data.message;
145
- }
146
- console.log('Progress: ' + data.progress + '% - ' + data.message);
147
- } else if (data.type === 'tools') {
148
- showStatus(data.message, 'processing');
149
- // Update loading text for tools
150
- const loadingText = document.getElementById('loadingText');
151
- if (loadingText) {
152
- loadingText.textContent = data.message;
153
- }
154
- console.log('Tools: ' + data.message);
155
- } else if (data.type === 'result') {
156
- const result = data.data;
157
- const requestTime = Date.now() - requestStart;
158
- console.log('Request completed in ' + requestTime + 'ms');
159
-
160
- if (result.success) {
161
- addMessage('assistant', result.response, result.sources, result.visualizations);
162
- showStatus('Research complete', 'success');
163
- console.log('Analysis completed successfully');
164
- } else {
165
- console.log('Analysis request failed');
166
- addMessage('assistant', result.response || 'Analysis temporarily unavailable. Please try again.', [], []);
167
- showStatus('Request failed', 'error');
168
- }
169
- } else if (data.type === 'complete') {
170
- break;
171
- } else if (data.type === 'error') {
172
- throw new Error(data.message);
173
- }
174
- } catch (parseError) {
175
- console.error('Parse error:', parseError);
176
- }
177
- }
178
  }
179
  }
180
-
181
- } catch (error) {
182
- console.error('Streaming request error:', error);
183
-
184
- // More specific error handling
185
- if (error.name === 'AbortError') {
186
- addMessage('assistant', 'Request timed out after 5 minutes. Ollama may be processing a complex query. Please try a simpler question or wait and try again.');
187
- showStatus('Request timed out', 'error');
188
- } else if (error.message.includes('Failed to fetch') || error.message.includes('network error')) {
189
- addMessage('assistant', 'Network connection error. Please check your internet connection and try again.');
190
- showStatus('Connection error', 'error');
191
- } else if (error.message.includes('ERR_HTTP2_PROTOCOL_ERROR')) {
192
- addMessage('assistant', 'Ollama is still processing your request in the background. Please wait a moment and try again, or try a simpler query.');
193
- showStatus('Processing - please retry', 'warning');
194
  } else {
195
- addMessage('assistant', 'Connection error. Please check your network and try again.');
196
- showStatus('Connection error', 'error');
197
  }
198
  } finally {
199
- // Reset UI states
200
  sendBtn.disabled = false;
201
- sendBtn.innerHTML = 'Research';
202
- loadingIndicator.classList.remove('active');
203
- input.focus();
204
- console.log('Request completed');
205
-
206
- // Hide status after delay
207
- setTimeout(() => hideStatus(), 3000);
208
  }
209
  }
210
-
211
- function addMessage(sender, content, sources = [], visualizations = []) {
212
- const messagesDiv = document.getElementById('chatMessages');
213
-
214
- // Clear welcome message
215
- if (messageCount === 0) {
216
- messagesDiv.innerHTML = '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
217
  }
218
- messageCount++;
219
-
220
- const messageDiv = document.createElement('div');
221
- messageDiv.className = 'message ' + sender;
222
-
223
- let sourcesHtml = '';
224
- if (sources && sources.length > 0) {
225
- sourcesHtml = `
226
- <div class="sources">
227
- Sources: ${sources.map(s => `<span>${s}</span>`).join('')}
 
 
 
 
228
  </div>
229
- `;
230
- }
231
-
232
- let visualizationHtml = '';
233
- if (visualizations && visualizations.length > 0) {
234
- console.log('Processing visualizations:', visualizations.length);
235
- visualizationHtml = visualizations.map((viz, index) => {
236
- console.log(`Visualization ${index}:`, viz.substring(0, 100));
237
- return `<div class="visualization-container" id="viz-${Date.now()}-${index}">${viz}</div>`;
238
- }).join('');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
  }
240
 
241
- // Format content based on sender
242
- let formattedContent = content;
 
 
 
 
 
 
 
 
 
 
 
 
243
  if (sender === 'assistant') {
244
- // Convert markdown to HTML for assistant responses
245
- try {
246
- formattedContent = marked.parse(content);
247
- } catch (error) {
248
- // Fallback to basic formatting if marked.js fails
249
- console.warn('Markdown parsing failed, using fallback:', error);
250
- formattedContent = content
251
- .replace(/\n/g, '<br>')
252
- .replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
253
- .replace(/\*(.*?)\*/g, '<em>$1</em>')
254
- .replace(/`(.*?)`/g, '<code>$1</code>');
255
- }
256
  } else {
257
- // Apply markdown parsing to user messages too
258
- try {
259
- formattedContent = marked.parse(content);
260
- } catch (error) {
261
- formattedContent = content.replace(/\n/g, '<br>');
262
- }
263
  }
264
-
265
- messageDiv.innerHTML = `
266
- <div class="message-content">
267
- ${formattedContent}
268
- ${sourcesHtml}
269
- </div>
270
- ${visualizationHtml}
271
- <div class="message-meta">${new Date().toLocaleTimeString()}</div>
272
- `;
273
-
274
- messagesDiv.appendChild(messageDiv);
275
- messagesDiv.scrollTop = messagesDiv.scrollHeight;
276
-
277
- // Execute any scripts in the visualizations after DOM insertion
 
 
 
 
 
 
 
 
 
 
 
278
  if (visualizations && visualizations.length > 0) {
279
- console.log('Executing visualization scripts...');
280
  setTimeout(() => {
281
- const scripts = messageDiv.querySelectorAll('script');
282
- console.log(`Found ${scripts.length} scripts to execute`);
283
-
284
- scripts.forEach((script, index) => {
285
- console.log(`Executing script ${index}:`, script.textContent.substring(0, 200) + '...');
286
- try {
287
- // Execute script in global context using Function constructor
288
- const scriptFunction = new Function(script.textContent);
289
- scriptFunction.call(window);
290
- console.log(`Script ${index} executed successfully`);
291
- } catch (error) {
292
- console.error(`Script ${index} execution error:`, error);
293
- console.error(`Script content preview:`, script.textContent.substring(0, 500));
294
- }
295
  });
296
- console.log('All visualization scripts executed');
297
- }, 100);
298
  }
299
-
300
  chatHistory.push({ role: sender, content });
301
  if (chatHistory.length > 20) chatHistory = chatHistory.slice(-20);
302
  }
303
-
 
 
 
 
304
  function setQuery(query) {
305
- document.getElementById('queryInput').value = query;
306
- setTimeout(() => sendQuery(), 100);
 
 
 
 
307
  }
308
-
309
- // Status management functions
310
- function showStatus(message, type = 'info') {
311
- const statusIndicator = document.getElementById('statusIndicator');
312
- const statusText = document.getElementById('statusText');
313
-
314
- statusText.textContent = message;
315
- statusIndicator.className = `status-indicator show ${type}`;
316
  }
317
-
318
- function hideStatus() {
319
- const statusIndicator = document.getElementById('statusIndicator');
320
- statusIndicator.classList.remove('show');
 
 
 
321
  }
322
-
323
- function updateProgress(progress) {
324
- // Update progress bar if it exists
325
- const progressBar = document.querySelector('.progress-bar');
326
- if (progressBar) {
327
- progressBar.style.width = `${progress}%`;
328
- }
329
-
330
- // Update loading indicator text with progress
331
- const loadingText = document.getElementById('loadingText');
332
- if (loadingText && progress) {
333
- loadingText.textContent = `Processing ${progress}%...`;
 
 
 
 
 
 
 
 
 
 
 
 
334
  }
335
  }
336
-
337
- // Theme toggle functionality
 
 
 
 
338
  function toggleTheme() {
339
- const currentTheme = document.documentElement.getAttribute('data-theme');
340
- const newTheme = currentTheme === 'light' ? 'dark' : 'light';
341
- const themeIcon = document.querySelector('#themeToggle i');
342
-
343
- document.documentElement.setAttribute('data-theme', newTheme);
344
- localStorage.setItem('theme', newTheme);
345
-
346
- // Update icon
347
- if (newTheme === 'light') {
348
- themeIcon.className = 'fas fa-sun';
349
- } else {
350
- themeIcon.className = 'fas fa-moon';
351
- }
352
  }
353
-
354
- // Initialize theme
355
- function initializeTheme() {
356
- const savedTheme = localStorage.getItem('theme') || 'dark';
357
- const themeIcon = document.querySelector('#themeToggle i');
358
-
359
- document.documentElement.setAttribute('data-theme', savedTheme);
360
-
361
- if (savedTheme === 'light') {
362
- themeIcon.className = 'fas fa-sun';
363
- } else {
364
- themeIcon.className = 'fas fa-moon';
365
- }
366
  }
367
-
368
- // Event listeners
369
- document.getElementById('queryInput').addEventListener('keypress', (e) => {
370
- if (e.key === 'Enter') sendQuery();
371
- });
372
-
373
- document.getElementById('sendBtn').addEventListener('click', (e) => {
374
- console.log('Research button clicked');
375
- e.preventDefault();
376
- sendQuery();
377
- });
378
-
379
  document.getElementById('themeToggle').addEventListener('click', toggleTheme);
380
-
381
- // Initialize
382
- document.addEventListener('DOMContentLoaded', () => {
383
- console.log('Application initialized');
384
- initializeTheme();
385
- checkStatus();
386
- document.getElementById('queryInput').focus();
387
- });
 
1
+ /* ============================================================
2
+ Web3 Research Co-Pilot — app.js
3
+ Optimised streaming chat client with inline tool activity
4
+ ============================================================ */
5
+ // ── State ────────────────────────────────────────────────────
6
  let chatHistory = [];
7
+ let useGemini = false;
8
+ // ── Tool icon map ─────────────────────────────────────────────
9
+ const TOOL_ICONS = {
10
+ coingecko: 'fa-coins',
11
+ defillama: 'fa-droplet',
12
+ cryptocompare: 'fa-chart-bar',
13
+ etherscan: 'fa-magnifying-glass',
14
+ chart: 'fa-chart-line',
15
+ price: 'fa-dollar-sign',
16
+ market: 'fa-chart-area',
17
+ gas: 'fa-gas-pump',
18
+ whale: 'fa-fish',
19
+ 'default': 'fa-gear',
20
+ };
21
+ // ── Marked.js setup ───────────────────────────────────────────
22
+ try {
23
+ marked.use({ breaks: true, gfm: true });
24
+ } catch(e) { /* older marked version — ignore */ }
25
+ // ── Init ─────────────────────────────────────────────────────
26
+ document.addEventListener('DOMContentLoaded', () => {
27
+ initTheme();
28
+ initModel();
29
+ initTextarea();
30
+ checkStatus();
31
+ document.getElementById('queryInput').focus();
 
32
  });
33
+ // ── Textarea auto-grow ───────────────────────────────────────
34
+ function initTextarea() {
35
+ const ta = document.getElementById('queryInput');
36
+ ta.addEventListener('input', () => {
37
+ ta.style.height = 'auto';
38
+ ta.style.height = Math.min(ta.scrollHeight, 130) + 'px';
39
+ document.getElementById('charCount').textContent =
40
+ `${ta.value.length} / 1000`;
41
+ });
42
+ ta.addEventListener('keydown', e => {
43
+ if (e.key === 'Enter' && !e.shiftKey) {
44
+ e.preventDefault();
45
+ sendQuery();
46
+ }
47
+ });
48
  }
49
+ // ── Model selection ──────────────────────────────────────────
50
+ function setModel(model) {
51
+ useGemini = model === 'gemini';
52
+ localStorage.setItem('useGemini', useGemini);
53
+ document.getElementById('btnOllama').classList.toggle('active', !useGemini);
54
+ document.getElementById('btnGemini').classList.toggle('active', useGemini);
55
+ showToast(`Switched to ${useGemini ? 'Gemini (Cloud)' : 'Ollama (Local)'}`, 'info');
56
+ checkStatus();
57
+ }
58
+ function initModel() {
59
+ useGemini = localStorage.getItem('useGemini') === 'true';
60
+ document.getElementById('btnOllama').classList.toggle('active', !useGemini);
61
+ document.getElementById('btnGemini').classList.toggle('active', useGemini);
62
+ }
63
+ // ── Status check ─────────────────────────────────────────────
64
  async function checkStatus() {
65
+ const badge = document.getElementById('statusBadge');
66
+ const text = document.getElementById('statusBadgeText');
67
  try {
68
+ const res = await fetch('/status');
69
+ const data = await res.json();
70
+ if (data.enabled) {
71
+ badge.className = 'status-badge online';
72
+ text.textContent = 'Online';
 
 
 
 
 
73
  } else {
74
+ badge.className = 'status-badge offline';
75
+ text.textContent = 'Limited';
 
 
76
  }
77
+ } catch {
78
+ badge.className = 'status-badge offline';
79
+ text.textContent = 'Offline';
 
80
  }
81
  }
82
+ // ── Send query ───────────────────────────────────────────────
83
  async function sendQuery() {
84
+ const ta = document.getElementById('queryInput');
85
  const sendBtn = document.getElementById('sendBtn');
86
+ const query = ta.value.trim();
87
+ if (!query) return;
 
 
 
 
 
 
 
 
 
88
  addMessage('user', query);
89
+ ta.value = '';
90
+ ta.style.height = 'auto';
91
+ document.getElementById('charCount').textContent = '0 / 1000';
92
+ const thinkingId = showThinking();
93
  sendBtn.disabled = true;
94
+ sendBtn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i>';
 
 
 
95
  try {
 
 
 
 
96
  const controller = new AbortController();
97
+ const timer = setTimeout(() => controller.abort(), 300000);
98
+ const res = await fetch('/query/stream', {
 
 
 
 
 
99
  method: 'POST',
100
+ headers: { 'Content-Type': 'application/json', 'Accept': 'text/event-stream' },
101
+ body: JSON.stringify({ query, chat_history: chatHistory, use_gemini: useGemini }),
 
 
 
 
 
 
 
 
102
  signal: controller.signal,
 
 
103
  });
104
+ clearTimeout(timer);
105
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
106
+ const reader = res.body.getReader();
 
 
 
 
 
 
107
  const decoder = new TextDecoder();
108
+ let buffer = '';
109
+ outer: while (true) {
 
110
  const { done, value } = await reader.read();
111
  if (done) break;
 
112
  buffer += decoder.decode(value, { stream: true });
113
  const lines = buffer.split('\n');
114
+ buffer = lines.pop(); // keep incomplete line
 
115
  for (const line of lines) {
116
+ if (!line.startsWith('data: ')) continue;
117
+ try {
118
+ const evt = JSON.parse(line.slice(6));
119
+ if (handleStreamEvent(evt, thinkingId) === 'done') break outer;
120
+ } catch { /* skip malformed JSON */ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
  }
122
  }
123
+ } catch (err) {
124
+ removeThinking(thinkingId);
125
+ if (err.name === 'AbortError') {
126
+ addMessage('assistant', 'Request timed out after 5 minutes. Try a shorter or simpler query.');
127
+ } else if (err.message.includes('Failed to fetch')) {
128
+ addMessage('assistant', 'Network error please check your connection.');
 
 
 
 
 
 
 
 
129
  } else {
130
+ addMessage('assistant', 'An unexpected error occurred. Please try again.');
 
131
  }
132
  } finally {
 
133
  sendBtn.disabled = false;
134
+ sendBtn.innerHTML = '<i class="fas fa-paper-plane"></i>';
135
+ ta.focus();
 
 
 
 
 
136
  }
137
  }
138
+ // ── Handle SSE events ────────────────────────────────────────
139
+ function handleStreamEvent(data, thinkingId) {
140
+ switch (data.type) {
141
+ case 'status':
142
+ updateThinking(thinkingId, data.message, data.progress);
143
+ break;
144
+ case 'tools':
145
+ addToolStep(thinkingId, data.message);
146
+ break;
147
+ case 'result':
148
+ removeThinking(thinkingId);
149
+ if (data.data && data.data.success) {
150
+ addMessage('assistant', data.data.response, data.data.sources, data.data.visualizations);
151
+ } else {
152
+ const msg = (data.data && data.data.response) || 'Analysis temporarily unavailable.';
153
+ addMessage('assistant', msg);
154
+ }
155
+ return 'done';
156
+ case 'complete':
157
+ return 'done';
158
+ case 'error':
159
+ removeThinking(thinkingId);
160
+ addMessage('assistant', data.message || 'An error occurred.');
161
+ return 'done';
162
  }
163
+ }
164
+ // ── Thinking bubble ──────────────────────────────────────────
165
+ function showThinking() {
166
+ const id = 'thinking-' + Date.now();
167
+ const msgs = document.getElementById('chatMessages');
168
+ clearWelcome();
169
+ const div = document.createElement('div');
170
+ div.className = 'message assistant';
171
+ div.id = id;
172
+ div.innerHTML = `
173
+ <div class="thinking-bubble">
174
+ <div class="thinking-header">
175
+ <div class="thinking-spinner"></div>
176
+ <span class="thinking-label" id="${id}-label">Analyzing query…</span>
177
  </div>
178
+ <div class="tool-steps" id="${id}-steps"></div>
179
+ <div class="progress-strip"><div class="progress-fill" id="${id}-bar"></div></div>
180
+ </div>
181
+ <div class="message-time">${fmtTime()}</div>`;
182
+ msgs.appendChild(div);
183
+ msgs.scrollTop = msgs.scrollHeight;
184
+ return id;
185
+ }
186
+ function updateThinking(id, message, progress) {
187
+ const label = document.getElementById(`${id}-label`);
188
+ if (label) label.textContent = message;
189
+ const bar = document.getElementById(`${id}-bar`);
190
+ if (bar && progress != null) bar.style.width = `${progress}%`;
191
+ }
192
+ function addToolStep(id, message) {
193
+ const steps = document.getElementById(`${id}-steps`);
194
+ if (!steps) return;
195
+
196
+ // Parse "Available tools: ['CoinGecko', 'DeFiLlama', ...]" into individual rows
197
+ const listMatch = message.match(/\[(.+)\]/);
198
+ const toolNames = listMatch
199
+ ? [...listMatch[1].matchAll(/'([^']+)'/g)].map(m => m[1])
200
+ : null;
201
+
202
+ if (toolNames && toolNames.length > 0) {
203
+ toolNames.forEach(name => {
204
+ const step = document.createElement('div');
205
+ step.className = 'tool-step';
206
+ step.innerHTML = `<i class="fas ${getToolIcon(name)}"></i><span>${escapeHtml(name)}</span>`;
207
+ steps.appendChild(step);
208
+ });
209
+ } else {
210
+ const step = document.createElement('div');
211
+ step.className = 'tool-step';
212
+ step.innerHTML = `<i class="fas ${getToolIcon(message)}"></i><span>${escapeHtml(message)}</span>`;
213
+ steps.appendChild(step);
214
  }
215
 
216
+ document.getElementById('chatMessages').scrollTop = 9999;
217
+ }
218
+ function removeThinking(id) {
219
+ const el = document.getElementById(id);
220
+ if (el) el.remove();
221
+ }
222
+ // ── Add message ──────────────────────────────────────────────
223
+ function addMessage(sender, content, sources = [], visualizations = []) {
224
+ clearWelcome();
225
+ const msgs = document.getElementById('chatMessages');
226
+ const div = document.createElement('div');
227
+ div.className = `message ${sender}`;
228
+ // Render content
229
+ let bodyHtml = '';
230
  if (sender === 'assistant') {
231
+ try { bodyHtml = `<div class="md-content">${marked.parse(String(content))}</div>`; }
232
+ catch { bodyHtml = `<div class="md-content">${escapeHtml(String(content)).replace(/\n/g, '<br>')}</div>`; }
 
 
 
 
 
 
 
 
 
 
233
  } else {
234
+ bodyHtml = `<div class="md-content">${escapeHtml(String(content))}</div>`;
 
 
 
 
 
235
  }
236
+ // Sources
237
+ let srcHtml = '';
238
+ if (sources && sources.length > 0) {
239
+ const tags = sources.map(s => `<span class="source-tag">${escapeHtml(s)}</span>`).join('');
240
+ srcHtml = `<div class="sources-list"><span class="sources-label">Sources</span>${tags}</div>`;
241
+ }
242
+ // Visualizations
243
+ const vizHtml = (visualizations || []).map((v, i) =>
244
+ `<div class="viz-container" id="viz-${Date.now()}-${i}">${v}</div>`
245
+ ).join('');
246
+ div.innerHTML = `
247
+ <div class="message-bubble">${bodyHtml}${srcHtml}</div>
248
+ ${vizHtml}
249
+ <div class="message-time">${fmtTime()}</div>`;
250
+ msgs.appendChild(div);
251
+ msgs.scrollTop = msgs.scrollHeight;
252
+ // Apply syntax highlighting to code blocks
253
+ if (sender === 'assistant') {
254
+ requestAnimationFrame(() => {
255
+ if (typeof hljs !== 'undefined') {
256
+ div.querySelectorAll('pre code').forEach(b => hljs.highlightElement(b));
257
+ }
258
+ });
259
+ }
260
+ // Execute embedded Plotly scripts
261
  if (visualizations && visualizations.length > 0) {
 
262
  setTimeout(() => {
263
+ div.querySelectorAll('script').forEach(s => {
264
+ try { new Function(s.textContent).call(window); }
265
+ catch (e) { console.warn('Viz script error:', e); }
 
 
 
 
 
 
 
 
 
 
 
266
  });
267
+ }, 120);
 
268
  }
269
+ // Maintain rolling chat history (last 20 turns)
270
  chatHistory.push({ role: sender, content });
271
  if (chatHistory.length > 20) chatHistory = chatHistory.slice(-20);
272
  }
273
+ // ── Helpers ──────────────────────────────────────────────────
274
+ function clearWelcome() {
275
+ const w = document.querySelector('.welcome-screen');
276
+ if (w) w.remove();
277
+ }
278
  function setQuery(query) {
279
+ const ta = document.getElementById('queryInput');
280
+ ta.value = query;
281
+ ta.style.height = 'auto';
282
+ ta.style.height = Math.min(ta.scrollHeight, 130) + 'px';
283
+ document.getElementById('charCount').textContent = `${query.length} / 1000`;
284
+ setTimeout(sendQuery, 50);
285
  }
286
+ function getToolIcon(message) {
287
+ const m = message.toLowerCase();
288
+ for (const [key, icon] of Object.entries(TOOL_ICONS)) {
289
+ if (key !== 'default' && m.includes(key)) return icon;
290
+ }
291
+ return TOOL_ICONS.default;
 
 
292
  }
293
+ function escapeHtml(str) {
294
+ return str
295
+ .replace(/&/g, '&amp;')
296
+ .replace(/</g, '&lt;')
297
+ .replace(/>/g, '&gt;')
298
+ .replace(/"/g, '&quot;')
299
+ .replace(/'/g, '&#039;');
300
  }
301
+ function fmtTime() {
302
+ return new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
303
+ }
304
+ // ── Toast ─────────────────────────────────────────────────────
305
+ let toastTimer = null;
306
+ function showToast(message, type = 'info') {
307
+ const toast = document.getElementById('statusIndicator');
308
+ const text = document.getElementById('statusText');
309
+ const icon = toast.querySelector('.toast-icon');
310
+ const icons = {
311
+ info: 'fa-circle-info',
312
+ processing: 'fa-circle-notch fa-spin',
313
+ success: 'fa-circle-check',
314
+ error: 'fa-circle-xmark',
315
+ warning: 'fa-triangle-exclamation',
316
+ };
317
+ text.textContent = message;
318
+ toast.className = `toast show ${type}`;
319
+ if (icon) icon.className = `fas ${icons[type] || icons.info} toast-icon`;
320
+ clearTimeout(toastTimer);
321
+ if (type !== 'processing') {
322
+ toastTimer = setTimeout(() => {
323
+ toast.classList.remove('show');
324
+ }, 3000);
325
  }
326
  }
327
+ // ── Theme ─────────────────────────────────────────────────────
328
+ function initTheme() {
329
+ const theme = localStorage.getItem('theme') || 'dark';
330
+ document.documentElement.setAttribute('data-theme', theme);
331
+ syncThemeIcon(theme);
332
+ }
333
  function toggleTheme() {
334
+ const cur = document.documentElement.getAttribute('data-theme');
335
+ const next = cur === 'light' ? 'dark' : 'light';
336
+ document.documentElement.setAttribute('data-theme', next);
337
+ localStorage.setItem('theme', next);
338
+ syncThemeIcon(next);
 
 
 
 
 
 
 
 
339
  }
340
+ function syncThemeIcon(theme) {
341
+ const icon = document.querySelector('#themeToggle i');
342
+ if (icon) icon.className = theme === 'light' ? 'fas fa-moon' : 'fas fa-sun';
 
 
 
 
 
 
 
 
 
 
343
  }
344
+ // ── Event listeners ───────────────────────────────────────────
345
+ document.getElementById('sendBtn').addEventListener('click', sendQuery);
 
 
 
 
 
 
 
 
 
 
346
  document.getElementById('themeToggle').addEventListener('click', toggleTheme);
 
 
 
 
 
 
 
 
static/styles.css CHANGED
@@ -1,767 +1,652 @@
 
 
 
 
 
1
  :root {
2
- --primary: #0066ff;
3
- --primary-dark: #0052cc;
4
- --accent: #00d4aa;
5
- --background: #000000;
6
- --surface: #111111;
7
- --surface-elevated: #1a1a1a;
8
- --text: #ffffff;
9
- --text-secondary: #a0a0a0;
10
- --text-muted: #666666;
11
- --border: rgba(255, 255, 255, 0.08);
12
- --border-focus: rgba(0, 102, 255, 0.3);
13
- --shadow: rgba(0, 0, 0, 0.4);
14
- --success: #00d4aa;
15
- --warning: #ffa726;
16
- --error: #f44336;
17
- }
18
-
 
 
 
 
 
 
19
  [data-theme="light"] {
20
- --background: #ffffff;
21
- --surface: #f8f9fa;
22
- --surface-elevated: #ffffff;
23
- --text: #1a1a1a;
24
- --text-secondary: #4a5568;
25
- --text-muted: #718096;
26
- --border: rgba(0, 0, 0, 0.08);
27
- --border-focus: rgba(0, 102, 255, 0.3);
28
- --shadow: rgba(0, 0, 0, 0.1);
29
- }
30
-
31
- * {
32
- margin: 0;
33
- padding: 0;
34
- box-sizing: border-box;
35
- transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease;
36
  }
37
-
38
  body {
39
- font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', system-ui, sans-serif;
40
- background: var(--background);
41
  color: var(--text);
42
- line-height: 1.5;
43
- min-height: 100vh;
44
- font-weight: 400;
45
  -webkit-font-smoothing: antialiased;
46
  -moz-osx-font-smoothing: grayscale;
 
47
  }
48
-
49
- .container {
50
- max-width: 1000px;
 
 
 
51
  margin: 0 auto;
52
- padding: 2rem 1.5rem;
 
53
  }
54
-
55
  .header {
56
- text-align: center;
57
- margin-bottom: 2.5rem;
58
- }
59
- .header-content {
60
  display: flex;
 
61
  justify-content: space-between;
 
 
 
 
 
 
 
 
 
 
62
  align-items: center;
63
- max-width: 100%;
 
64
  }
65
- .header-text {
66
- flex: 1;
67
- text-align: center;
 
 
 
 
 
 
 
 
 
 
 
68
  }
69
- .header-controls {
70
  display: flex;
71
  align-items: center;
72
- gap: 1rem;
 
73
  }
74
-
75
- /* LLM Toggle Switch Styles */
76
- .llm-toggle {
77
  display: flex;
78
  align-items: center;
79
- gap: 0.5rem;
80
- }
81
- .toggle-label {
82
- font-size: 0.875rem;
83
- color: var(--text-secondary);
84
  font-weight: 500;
85
- }
86
-
87
- .switch {
88
- position: relative;
89
- display: inline-block;
90
- width: 80px;
91
- height: 32px;
92
- }
93
-
94
- .switch input {
95
- opacity: 0;
96
- width: 0;
97
- height: 0;
98
- }
99
-
100
- .slider {
101
- position: absolute;
102
- cursor: pointer;
103
- top: 0;
104
- left: 0;
105
- right: 0;
106
- bottom: 0;
107
- background-color: var(--surface);
108
  border: 1px solid var(--border);
109
- transition: .4s;
110
- overflow: hidden;
 
111
  }
112
-
113
- .slider:before {
114
- position: absolute;
115
- content: "";
116
- height: 24px;
117
- width: 24px;
118
- left: 3px;
119
- bottom: 3px;
120
- background-color: var(--primary);
121
- transition: .4s;
122
  border-radius: 50%;
123
- z-index: 2;
124
- }
125
-
126
- .slider-text-off, .slider-text-on {
127
- position: absolute;
128
- color: var(--text-secondary);
129
- font-size: 0.7rem;
130
- font-weight: 500;
131
- top: 50%;
132
- transform: translateY(-50%);
133
- transition: .4s;
134
- pointer-events: none;
135
- z-index: 1;
136
- }
137
-
138
- .slider-text-off {
139
- left: 8px;
140
- }
141
-
142
- .slider-text-on {
143
- right: 8px;
144
- opacity: 0;
145
  }
146
-
147
- input:checked + .slider {
148
- background-color: var(--accent);
149
- border-color: var(--accent);
 
 
 
 
150
  }
151
-
152
- input:checked + .slider .slider-text-off {
153
- opacity: 0;
 
 
 
 
 
 
 
 
 
 
 
 
154
  }
155
-
156
- input:checked + .slider .slider-text-on {
157
- opacity: 1;
 
158
  }
159
-
160
- input:checked + .slider:before {
161
- transform: translateX(48px);
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  }
163
-
164
- .slider.round {
165
- border-radius: 20px;
 
 
 
 
166
  }
167
-
168
- .slider.round:before {
169
- border-radius: 50%;
 
 
170
  }
171
-
172
- .theme-toggle {
173
- background: var(--surface);
174
- border: 1px solid var(--border);
175
- border-radius: 8px;
176
- padding: 0.75rem;
177
- color: var(--text);
178
- cursor: pointer;
179
- transition: all 0.2s ease;
180
- font-size: 1.1rem;
181
- min-width: 44px;
182
- height: 44px;
 
 
 
 
 
183
  display: flex;
 
184
  align-items: center;
185
  justify-content: center;
 
 
 
 
186
  }
187
- .theme-toggle:hover {
188
- background: var(--surface-elevated);
189
- border-color: var(--primary);
190
- transform: translateY(-1px);
191
- }
192
-
193
- .header h1 {
194
- font-size: 2.25rem;
195
  font-weight: 600;
 
196
  color: var(--text);
197
- margin-bottom: 0.5rem;
198
- letter-spacing: -0.025em;
199
  }
200
-
201
- .header .brand {
202
- color: var(--primary);
203
- }
204
-
205
- .header p {
206
- color: var(--text-secondary);
207
- font-size: 1rem;
208
- font-weight: 400;
209
  }
210
-
211
- .status {
212
- background: var(--surface);
213
- border: 1px solid var(--border);
214
- border-radius: 12px;
215
- padding: 1rem 1.5rem;
216
- margin-bottom: 2rem;
217
- text-align: center;
218
- transition: all 0.2s ease;
219
- }
220
-
221
- .status.online {
222
- border-color: var(--success);
223
- background: linear-gradient(135deg, rgba(0, 212, 170, 0.05), rgba(0, 212, 170, 0.02));
224
- }
225
-
226
- .status.offline {
227
- border-color: var(--error);
228
- background: linear-gradient(135deg, rgba(244, 67, 54, 0.05), rgba(244, 67, 54, 0.02));
229
- }
230
-
231
- .status.checking {
232
- border-color: var(--warning);
233
- background: linear-gradient(135deg, rgba(255, 167, 38, 0.05), rgba(255, 167, 38, 0.02));
234
- animation: pulse 2s infinite;
235
- }
236
-
237
- @keyframes pulse {
238
- 0%, 100% { opacity: 1; }
239
- 50% { opacity: 0.8; }
240
  }
241
-
242
- .chat-interface {
 
 
 
243
  background: var(--surface);
244
- border: 1px solid var(--border);
245
- border-radius: 16px;
246
- overflow: hidden;
247
- margin-bottom: 2rem;
248
- backdrop-filter: blur(20px);
249
- }
250
-
251
- .chat-messages {
252
- height: 480px;
253
- overflow-y: auto;
254
- padding: 2rem;
255
- background: linear-gradient(180deg, var(--background), var(--surface));
256
- }
257
-
258
- .chat-messages::-webkit-scrollbar {
259
- width: 3px;
260
- }
261
-
262
- .chat-messages::-webkit-scrollbar-track {
263
- background: transparent;
264
  }
265
-
266
- .chat-messages::-webkit-scrollbar-thumb {
267
- background: var(--border);
268
- border-radius: 2px;
 
269
  }
270
-
 
271
  .message {
272
- margin-bottom: 2rem;
273
- opacity: 0;
274
- animation: messageSlide 0.4s cubic-bezier(0.2, 0, 0.2, 1) forwards;
275
- }
276
-
277
- @keyframes messageSlide {
278
- from {
279
- opacity: 0;
280
- transform: translateY(20px) scale(0.98);
281
- }
282
- to {
283
- opacity: 1;
284
- transform: translateY(0) scale(1);
285
- }
286
- }
287
-
288
- .message.user {
289
- text-align: right;
290
- }
291
-
292
- .message.assistant {
293
- text-align: left;
 
294
  }
295
-
296
- .message-content {
297
- display: inline-block;
298
- max-width: 75%;
299
- padding: 1.25rem 1.5rem;
300
- border-radius: 24px;
301
- font-size: 0.95rem;
302
- line-height: 1.6;
303
- position: relative;
304
- }
305
-
306
- .message.user .message-content {
307
- background: linear-gradient(135deg, var(--primary), var(--primary-dark));
308
- color: #ffffff;
309
- border-bottom-right-radius: 8px;
310
- box-shadow: 0 4px 12px rgba(0, 102, 255, 0.2);
311
- }
312
-
313
- .message.assistant .message-content {
314
- background: var(--surface-elevated);
315
- color: var(--text);
316
- border-bottom-left-radius: 8px;
317
- border: 1px solid var(--border);
318
  }
319
- .message-content h1, .message-content h2, .message-content h3, .message-content h4 {
320
- color: var(--accent);
321
- margin: 1.25rem 0 0.5rem 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
322
  font-weight: 600;
323
  line-height: 1.3;
324
- text-shadow: 0 1px 2px rgba(0, 212, 170, 0.1);
325
- }
326
- .message-content h1 {
327
- font-size: 1.35rem;
328
- background: linear-gradient(135deg, var(--accent), #00b894);
329
- -webkit-background-clip: text;
330
- -webkit-text-fill-color: transparent;
331
- background-clip: text;
332
- }
333
- .message-content h2 {
334
- font-size: 1.2rem;
335
- color: #00b894;
336
- }
337
- .message-content h3 {
338
- font-size: 1.05rem;
 
 
 
 
 
 
 
 
339
  color: var(--accent);
340
  }
341
- .message-content h4 {
342
- font-size: 0.95rem;
343
- color: #74b9ff;
344
- }
345
- .message-content p {
346
- margin: 0.75rem 0;
347
- line-height: 1.65;
348
- color: var(--text);
349
- }
350
- .message-content ul, .message-content ol {
351
  margin: 0.75rem 0;
352
- padding-left: 1.5rem;
353
- line-height: 1.6;
354
- }
355
- .message-content li {
356
- margin: 0.3rem 0;
357
- line-height: 1.6;
358
- position: relative;
359
- }
360
- .message-content ul li::marker {
361
- color: var(--accent);
362
  }
363
- .message-content ol li::marker {
364
- color: var(--accent);
365
- font-weight: 600;
 
 
 
366
  }
367
- .message-content table {
 
368
  width: 100%;
369
  border-collapse: collapse;
370
- margin: 1rem 0;
371
- font-size: 0.9rem;
372
- }
373
- .message-content th, .message-content td {
374
  border: 1px solid var(--border);
 
 
 
 
 
 
 
 
 
 
375
  padding: 0.5rem 0.75rem;
376
  text-align: left;
 
377
  }
378
- .message-content th {
379
- background: var(--surface);
380
- font-weight: 600;
381
- color: var(--accent);
382
  }
383
- .message-content strong {
384
- background: linear-gradient(135deg, var(--accent), #74b9ff);
385
- -webkit-background-clip: text;
386
- -webkit-text-fill-color: transparent;
387
- background-clip: text;
388
- font-weight: 700;
389
- text-shadow: 0 1px 2px rgba(0, 212, 170, 0.2);
390
- }
391
- .message-content em {
392
- color: #a29bfe;
393
- font-style: italic;
394
- background: rgba(162, 155, 254, 0.1);
395
- padding: 0.1rem 0.2rem;
396
- border-radius: 3px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  }
398
- .message-content code {
399
- background: linear-gradient(135deg, rgba(116, 185, 255, 0.15), rgba(0, 212, 170, 0.1));
400
- border: 1px solid rgba(116, 185, 255, 0.3);
401
- padding: 0.2rem 0.5rem;
402
- border-radius: 6px;
403
- font-family: 'SF Mono', Consolas, 'Courier New', monospace;
404
- font-size: 0.85rem;
405
- color: #74b9ff;
406
  font-weight: 600;
407
- text-shadow: 0 1px 2px rgba(116, 185, 255, 0.2);
 
408
  }
409
- .message.user .message-content strong,
410
- .message.user .message-content code,
411
- .message.user .message-content em {
412
- color: rgba(255, 255, 255, 0.95);
413
- background: rgba(255, 255, 255, 0.1);
414
- -webkit-text-fill-color: rgba(255, 255, 255, 0.95);
 
 
415
  }
416
- .message-content pre {
417
- background: var(--background);
 
418
  border: 1px solid var(--border);
419
- border-radius: 8px;
420
- padding: 1rem;
421
- margin: 1rem 0;
422
- overflow-x: auto;
423
- font-family: 'SF Mono', Consolas, 'Courier New', monospace;
424
- font-size: 0.85rem;
425
- line-height: 1.5;
426
- }
427
- .message-content pre code {
428
- background: none;
429
- border: none;
430
- padding: 0;
431
- font-size: inherit;
432
- }
433
- .message-content blockquote {
434
- border-left: 3px solid var(--accent);
435
- padding-left: 1rem;
436
- margin: 1rem 0;
437
- color: var(--text-secondary);
438
- font-style: italic;
439
- background: rgba(0, 212, 170, 0.05);
440
- padding: 0.75rem 0 0.75rem 1rem;
441
- border-radius: 0 4px 4px 0;
442
- }
443
- .message-content a {
444
- color: var(--accent);
445
- text-decoration: none;
446
- border-bottom: 1px solid transparent;
447
- transition: border-color 0.2s ease;
448
  }
449
- .message-content a:hover {
450
- border-bottom-color: var(--accent);
 
 
 
 
 
 
 
 
451
  }
452
- .message.user .message-content {
453
- word-wrap: break-word;
454
- white-space: pre-wrap;
 
 
 
455
  }
456
- .message.user .message-content strong,
457
- .message.user .message-content code {
458
- color: rgba(255, 255, 255, 0.9);
 
 
 
 
 
459
  }
460
-
461
- .message-meta {
 
 
 
 
 
462
  font-size: 0.75rem;
463
- color: var(--text-muted);
464
- margin-top: 0.5rem;
465
- font-weight: 500;
466
- }
467
-
468
- .sources {
469
- margin-top: 1rem;
470
- padding-top: 1rem;
471
- border-top: 1px solid var(--border);
472
- font-size: 0.8rem;
473
- color: var(--text-secondary);
 
474
  }
475
-
476
- .sources span {
477
- display: inline-block;
478
- background: rgba(0, 102, 255, 0.1);
479
- border: 1px solid rgba(0, 102, 255, 0.2);
480
- padding: 0.25rem 0.75rem;
481
- border-radius: 6px;
482
- margin: 0.25rem 0.5rem 0.25rem 0;
483
- font-weight: 500;
484
- font-size: 0.75rem;
485
  }
486
-
487
  .input-area {
488
- padding: 2rem;
489
- background: linear-gradient(180deg, var(--surface), var(--surface-elevated));
490
  border-top: 1px solid var(--border);
 
 
491
  }
492
-
493
- .input-container {
494
  display: flex;
495
- gap: 1rem;
496
- align-items: stretch;
 
 
 
 
 
 
 
 
 
497
  }
498
-
499
  .input-field {
500
  flex: 1;
501
- padding: 1rem 1.5rem;
502
- background: var(--background);
503
- border: 2px solid var(--border);
504
- border-radius: 28px;
505
- color: var(--text);
506
- font-size: 0.95rem;
507
- outline: none;
508
- transition: all 0.2s cubic-bezier(0.2, 0, 0.2, 1);
509
- font-weight: 400;
510
- }
511
-
512
- .input-field:focus {
513
- border-color: var(--primary);
514
- box-shadow: 0 0 0 4px var(--border-focus);
515
- background: var(--surface);
516
- }
517
-
518
- .input-field::placeholder {
519
- color: var(--text-muted);
520
- font-weight: 400;
521
- }
522
-
523
- .send-button {
524
- padding: 1rem 2rem;
525
- background: linear-gradient(135deg, var(--primary), var(--primary-dark));
526
- color: #ffffff;
527
  border: none;
528
- border-radius: 28px;
529
- font-weight: 600;
530
- cursor: pointer;
531
- transition: all 0.2s cubic-bezier(0.2, 0, 0.2, 1);
532
- font-size: 0.95rem;
533
- box-shadow: 0 4px 12px rgba(0, 102, 255, 0.2);
534
- }
535
-
536
- .send-button:hover:not(:disabled) {
537
- transform: translateY(-2px);
538
- box-shadow: 0 8px 24px rgba(0, 102, 255, 0.3);
539
- }
540
-
541
- .send-button:active {
542
- transform: translateY(0);
543
- }
544
-
545
- .send-button:disabled {
546
- opacity: 0.6;
547
- cursor: not-allowed;
548
- transform: none;
549
- box-shadow: 0 4px 12px rgba(0, 102, 255, 0.1);
550
- }
551
-
552
- .examples {
553
- display: grid;
554
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
555
- gap: 1rem;
556
- margin-top: 1rem;
557
- }
558
-
559
- .example {
560
- background: linear-gradient(135deg, var(--surface), var(--surface-elevated));
561
- border: 1px solid var(--border);
562
- border-radius: 12px;
563
- padding: 1.5rem;
564
- cursor: pointer;
565
- transition: all 0.3s cubic-bezier(0.2, 0, 0.2, 1);
566
- position: relative;
567
- overflow: hidden;
568
- }
569
-
570
- .example::before {
571
- content: '';
572
- position: absolute;
573
- top: 0;
574
- left: -100%;
575
- width: 100%;
576
- height: 100%;
577
- background: linear-gradient(90deg, transparent, rgba(0, 102, 255, 0.05), transparent);
578
- transition: left 0.5s ease;
579
- }
580
-
581
- .example:hover::before {
582
- left: 100%;
583
- }
584
-
585
- .example:hover {
586
- border-color: var(--primary);
587
- transform: translateY(-4px);
588
- box-shadow: 0 12px 32px rgba(0, 0, 0, 0.2);
589
- background: linear-gradient(135deg, var(--surface-elevated), var(--surface));
590
- }
591
-
592
- .example-title {
593
- font-weight: 600;
594
  color: var(--text);
595
- margin-bottom: 0.5rem;
596
- font-size: 0.95rem;
 
 
 
 
 
 
 
 
 
597
  display: flex;
598
  align-items: center;
599
  gap: 0.5rem;
 
600
  }
601
- .example-title i {
602
- color: var(--primary);
603
- font-size: 1rem;
604
- width: 20px;
605
- text-align: center;
606
- }
607
-
608
- .example-desc {
609
- font-size: 0.85rem;
610
- color: var(--text-secondary);
611
- font-weight: 400;
612
  }
613
-
614
- .loading {
615
- display: inline-flex;
616
  align-items: center;
617
- gap: 0.5rem;
618
- color: var(--text-secondary);
619
- font-weight: 500;
620
- }
621
-
622
- .loading::after {
623
- content: '';
624
- width: 14px;
625
- height: 14px;
626
- border: 2px solid currentColor;
627
- border-top-color: transparent;
628
- border-radius: 50%;
629
- animation: spin 1s linear infinite;
630
- }
631
-
632
- @keyframes spin {
633
- to { transform: rotate(360deg); }
634
  }
635
- .loading-indicator {
636
- display: none;
637
- background: var(--surface-elevated);
638
- border: 1px solid var(--border);
639
- border-radius: 12px;
640
- padding: 1.5rem;
641
- margin: 1rem 0;
642
- text-align: center;
643
- color: var(--text-secondary);
644
  }
645
- .loading-indicator.active {
646
- display: block;
 
 
 
 
 
647
  }
648
- .loading-spinner {
649
  display: inline-block;
650
- width: 20px;
651
- height: 20px;
652
- border: 2px solid var(--border);
653
- border-top-color: var(--primary);
654
- border-radius: 50%;
655
- animation: spin 1s linear infinite;
656
- margin-right: 0.5rem;
657
- }
658
- .progress-container {
659
- width: 100%;
660
- height: 4px;
661
- background: var(--border);
662
- border-radius: 2px;
663
- overflow: hidden;
664
- margin: 10px 0 0 0;
665
- }
666
- .progress-bar {
667
- height: 100%;
668
- background: linear-gradient(90deg, var(--primary), var(--accent));
669
- border-radius: 2px;
670
- transition: width 0.3s ease;
671
- width: 0%;
672
  }
673
- .status-indicator {
 
674
  position: fixed;
675
- top: 20px;
676
- right: 20px;
677
  background: var(--surface);
678
- border: 1px solid var(--border);
679
- border-radius: 8px;
680
- padding: 0.75rem 1rem;
681
- font-size: 0.85rem;
682
- color: var(--text-secondary);
 
 
 
683
  opacity: 0;
684
- transform: translateY(-10px);
685
- transition: all 0.3s ease;
686
  z-index: 1000;
687
- }
688
- .status-indicator.show {
689
- opacity: 1;
690
- transform: translateY(0);
691
- }
692
- .status-indicator.processing {
693
- border-color: var(--primary);
694
- background: linear-gradient(135deg, rgba(0, 102, 255, 0.05), rgba(0, 102, 255, 0.02));
695
- }
696
-
697
- .visualization-container {
698
- margin: 1.5rem 0;
699
- background: var(--surface-elevated);
700
- border-radius: 12px;
701
- padding: 1.5rem;
702
- border: 1px solid var(--border);
703
- }
704
-
705
- .welcome {
706
- text-align: center;
707
- padding: 4rem 2rem;
708
- color: var(--text-secondary);
709
- }
710
-
711
- .welcome h3 {
712
- font-size: 1.25rem;
713
- font-weight: 600;
714
- margin-bottom: 0.5rem;
715
- color: var(--text);
716
- }
717
-
718
- .welcome p {
719
- font-size: 0.95rem;
720
- font-weight: 400;
721
- }
722
-
723
- @media (max-width: 768px) {
724
- .container {
725
- padding: 1rem;
726
- }
727
-
728
- .header-content {
729
- flex-direction: column;
730
- gap: 1rem;
731
- }
732
-
733
- .header-text {
734
- text-align: center;
735
- }
736
-
737
- .header h1 {
738
- font-size: 1.75rem;
739
- }
740
-
741
- .chat-messages {
742
- height: 400px;
743
- padding: 1.5rem;
744
- }
745
-
746
- .message-content {
747
- max-width: 85%;
748
- padding: 1rem 1.25rem;
749
- }
750
-
751
- .input-area {
752
- padding: 1.5rem;
753
- }
754
-
755
- .input-container {
756
- flex-direction: column;
757
- gap: 0.75rem;
758
- }
759
-
760
- .send-button {
761
- align-self: stretch;
762
- }
763
-
764
- .examples {
765
- grid-template-columns: 1fr;
766
- }
767
  }
 
1
+ /* ============================================================
2
+ Web3 Research Co-Pilot — Styles
3
+ Minimalist professional dark UI with Inter font
4
+ ============================================================ */
5
+ /* CSS Variables */
6
  :root {
7
+ --primary: #6366f1;
8
+ --primary-dark: #4f46e5;
9
+ --primary-glow: rgba(99,102,241,0.15);
10
+ --accent: #22d3ee;
11
+ --bg: #0d0d14;
12
+ --surface: #13131f;
13
+ --surface-2: #1c1c2e;
14
+ --surface-3: #26263a;
15
+ --text: #f1f5f9;
16
+ --text-2: #94a3b8;
17
+ --text-3: #64748b;
18
+ --border: rgba(255,255,255,0.07);
19
+ --border-2: rgba(255,255,255,0.11);
20
+ --success: #10b981;
21
+ --warning: #f59e0b;
22
+ --error: #ef4444;
23
+ --shadow-sm: 0 2px 8px rgba(0,0,0,0.3);
24
+ --shadow: 0 4px 20px rgba(0,0,0,0.45);
25
+ --radius-sm: 6px;
26
+ --radius: 10px;
27
+ --radius-lg: 16px;
28
+ --radius-xl: 20px;
29
+ }
30
  [data-theme="light"] {
31
+ --bg: #f6f7fb;
32
+ --surface: #ffffff;
33
+ --surface-2: #f1f3f9;
34
+ --surface-3: #e5e8f0;
35
+ --text: #0f172a;
36
+ --text-2: #475569;
37
+ --text-3: #94a3b8;
38
+ --border: rgba(0,0,0,0.06);
39
+ --border-2: rgba(0,0,0,0.10);
40
+ --shadow-sm: 0 2px 8px rgba(0,0,0,0.06);
41
+ --shadow: 0 4px 20px rgba(0,0,0,0.10);
42
+ }
43
+ *, *::before, *::after {
44
+ margin: 0; padding: 0; box-sizing: border-box;
 
 
45
  }
 
46
  body {
47
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
48
+ background: var(--bg);
49
  color: var(--text);
50
+ line-height: 1.6;
51
+ height: 100vh;
52
+ overflow: hidden;
53
  -webkit-font-smoothing: antialiased;
54
  -moz-osx-font-smoothing: grayscale;
55
+ transition: background-color 0.25s, color 0.25s;
56
  }
57
+ /* App Shell */
58
+ .app {
59
+ display: flex;
60
+ flex-direction: column;
61
+ height: 100vh;
62
+ max-width: 860px;
63
  margin: 0 auto;
64
+ border-left: 1px solid var(--border);
65
+ border-right: 1px solid var(--border);
66
  }
67
+ /* HEADER */
68
  .header {
 
 
 
 
69
  display: flex;
70
+ align-items: center;
71
  justify-content: space-between;
72
+ padding: 0 1.25rem;
73
+ height: 52px;
74
+ border-bottom: 1px solid var(--border);
75
+ background: var(--surface);
76
+ flex-shrink: 0;
77
+ gap: 0.75rem;
78
+ z-index: 100;
79
+ }
80
+ .header-left {
81
+ display: flex;
82
  align-items: center;
83
+ gap: 0.625rem;
84
+ min-width: 0;
85
  }
86
+ .logo-icon { flex-shrink: 0; }
87
+ .header-title {
88
+ font-size: 0.9rem;
89
+ font-weight: 600;
90
+ color: var(--text);
91
+ white-space: nowrap;
92
+ overflow: hidden;
93
+ text-overflow: ellipsis;
94
+ }
95
+ .brand {
96
+ background: linear-gradient(135deg, var(--primary), var(--accent));
97
+ -webkit-background-clip: text;
98
+ -webkit-text-fill-color: transparent;
99
+ background-clip: text;
100
  }
101
+ .header-right {
102
  display: flex;
103
  align-items: center;
104
+ gap: 0.625rem;
105
+ flex-shrink: 0;
106
  }
107
+ /* Status Badge */
108
+ .status-badge {
 
109
  display: flex;
110
  align-items: center;
111
+ gap: 0.375rem;
112
+ padding: 0.25rem 0.625rem;
113
+ border-radius: 20px;
114
+ font-size: 0.72rem;
 
115
  font-weight: 500;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  border: 1px solid var(--border);
117
+ background: var(--surface-2);
118
+ color: var(--text-3);
119
+ user-select: none;
120
  }
121
+ .status-dot {
122
+ width: 6px;
123
+ height: 6px;
 
 
 
 
 
 
 
124
  border-radius: 50%;
125
+ background: var(--text-3);
126
+ flex-shrink: 0;
127
+ transition: background 0.3s, box-shadow 0.3s;
128
+ }
129
+ .status-badge.online .status-dot { background: var(--success); box-shadow: 0 0 0 2px rgba(16,185,129,0.2); }
130
+ .status-badge.offline .status-dot { background: var(--error); }
131
+ .status-badge.checking .status-dot { background: var(--warning); animation: pulse-dot 1.4s ease-in-out infinite; }
132
+ .status-badge.online { color: var(--text-2); }
133
+ @keyframes pulse-dot {
134
+ 0%, 100% { opacity: 1; }
135
+ 50% { opacity: 0.35; }
 
 
 
 
 
 
 
 
 
 
 
136
  }
137
+ /* Model Toggle */
138
+ .model-select {
139
+ display: flex;
140
+ background: var(--surface-2);
141
+ border: 1px solid var(--border);
142
+ border-radius: var(--radius-sm);
143
+ padding: 2px;
144
+ gap: 2px;
145
  }
146
+ .model-btn {
147
+ display: flex;
148
+ align-items: center;
149
+ gap: 0.3rem;
150
+ padding: 0.28rem 0.625rem;
151
+ border: none;
152
+ border-radius: 4px;
153
+ font-size: 0.72rem;
154
+ font-weight: 500;
155
+ color: var(--text-3);
156
+ background: transparent;
157
+ cursor: pointer;
158
+ transition: all 0.18s;
159
+ white-space: nowrap;
160
+ font-family: inherit;
161
  }
162
+ .model-btn.active {
163
+ background: var(--surface);
164
+ color: var(--text);
165
+ box-shadow: var(--shadow-sm);
166
  }
167
+ .model-btn:hover:not(.active) { color: var(--text-2); }
168
+ /* Icon Button */
169
+ .icon-btn {
170
+ display: flex;
171
+ align-items: center;
172
+ justify-content: center;
173
+ width: 32px;
174
+ height: 32px;
175
+ border: 1px solid var(--border);
176
+ border-radius: var(--radius-sm);
177
+ background: var(--surface-2);
178
+ color: var(--text-3);
179
+ cursor: pointer;
180
+ font-size: 0.8rem;
181
+ transition: all 0.18s;
182
+ flex-shrink: 0;
183
  }
184
+ .icon-btn:hover { background: var(--surface-3); color: var(--text); }
185
+ /* MAIN LAYOUT */
186
+ .main {
187
+ flex: 1;
188
+ overflow: hidden;
189
+ display: flex;
190
+ flex-direction: column;
191
  }
192
+ .chat-wrap {
193
+ display: flex;
194
+ flex-direction: column;
195
+ height: 100%;
196
+ overflow: hidden;
197
  }
198
+ /* CHAT MESSAGES */
199
+ .chat-messages {
200
+ flex: 1;
201
+ overflow-y: auto;
202
+ padding: 1.5rem 1.25rem;
203
+ display: flex;
204
+ flex-direction: column;
205
+ gap: 1rem;
206
+ scroll-behavior: smooth;
207
+ background: var(--bg);
208
+ }
209
+ .chat-messages::-webkit-scrollbar { width: 4px; }
210
+ .chat-messages::-webkit-scrollbar-track { background: transparent; }
211
+ .chat-messages::-webkit-scrollbar-thumb { background: var(--border-2); border-radius: 2px; }
212
+ .chat-messages::-webkit-scrollbar-thumb:hover { background: var(--text-3); }
213
+ /* Welcome Screen */
214
+ .welcome-screen {
215
  display: flex;
216
+ flex-direction: column;
217
  align-items: center;
218
  justify-content: center;
219
+ text-align: center;
220
+ padding: 2rem 1rem 1.5rem;
221
+ flex: 1;
222
+ animation: fadeIn 0.4s ease;
223
  }
224
+ .welcome-icon { margin-bottom: 1.25rem; opacity: 0.85; }
225
+ .welcome-screen h2 {
226
+ font-size: 1.35rem;
 
 
 
 
 
227
  font-weight: 600;
228
+ margin-bottom: 0.4rem;
229
  color: var(--text);
230
+ letter-spacing: -0.02em;
 
231
  }
232
+ .welcome-screen p {
233
+ color: var(--text-3);
234
+ font-size: 0.875rem;
235
+ margin-bottom: 1.75rem;
 
 
 
 
 
236
  }
237
+ .example-chips {
238
+ display: flex;
239
+ flex-wrap: wrap;
240
+ gap: 0.5rem;
241
+ justify-content: center;
242
+ max-width: 580px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  }
244
+ .chip {
245
+ display: inline-flex;
246
+ align-items: center;
247
+ gap: 0.375rem;
248
+ padding: 0.425rem 0.875rem;
249
  background: var(--surface);
250
+ border: 1px solid var(--border-2);
251
+ border-radius: 20px;
252
+ color: var(--text-2);
253
+ font-size: 0.78rem;
254
+ font-weight: 500;
255
+ cursor: pointer;
256
+ transition: all 0.18s;
257
+ font-family: inherit;
 
 
 
 
 
 
 
 
 
 
 
 
258
  }
259
+ .chip:hover {
260
+ background: var(--surface-2);
261
+ border-color: var(--primary);
262
+ color: var(--text);
263
+ transform: translateY(-1px);
264
  }
265
+ .chip i { color: var(--primary); font-size: 0.72rem; }
266
+ /* MESSAGE BUBBLES */
267
  .message {
268
+ display: flex;
269
+ flex-direction: column;
270
+ gap: 0.2rem;
271
+ animation: msgSlide 0.28s ease;
272
+ }
273
+ @keyframes msgSlide {
274
+ from { opacity: 0; transform: translateY(6px); }
275
+ to { opacity: 1; transform: translateY(0); }
276
+ }
277
+ @keyframes fadeIn {
278
+ from { opacity: 0; }
279
+ to { opacity: 1; }
280
+ }
281
+ .message.user { align-items: flex-end; }
282
+ .message.assistant { align-items: flex-start; }
283
+ .message-bubble {
284
+ max-width: 82%;
285
+ padding: 0.75rem 1rem;
286
+ border-radius: var(--radius-lg);
287
+ font-size: 0.875rem;
288
+ line-height: 1.65;
289
+ word-break: break-word;
290
+ overflow-wrap: anywhere;
291
  }
292
+ .message.user .message-bubble {
293
+ background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
294
+ color: #fff;
295
+ border-bottom-right-radius: 4px;
296
+ box-shadow: 0 2px 10px rgba(99,102,241,0.25);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
297
  }
298
+ .message.assistant .message-bubble {
299
+ background: var(--surface);
300
+ border: 1px solid var(--border);
301
+ color: var(--text);
302
+ border-bottom-left-radius: 4px;
303
+ max-width: 94%;
304
+ }
305
+ .message-time {
306
+ font-size: 0.67rem;
307
+ color: var(--text-3);
308
+ padding: 0 0.25rem;
309
+ font-variant-numeric: tabular-nums;
310
+ }
311
+ /* MARKDOWN CONTENT */
312
+ .md-content { line-height: 1.7; }
313
+ .md-content > *:first-child { margin-top: 0 !important; }
314
+ .md-content > *:last-child { margin-bottom: 0 !important; }
315
+ .md-content h1, .md-content h2,
316
+ .md-content h3, .md-content h4,
317
+ .md-content h5, .md-content h6 {
318
  font-weight: 600;
319
  line-height: 1.3;
320
+ margin: 1rem 0 0.4rem;
321
+ }
322
+ .md-content h1 { font-size: 1.2rem; color: var(--accent); }
323
+ .md-content h2 { font-size: 1.05rem; color: var(--accent); border-bottom: 1px solid var(--border); padding-bottom: 0.3rem; }
324
+ .md-content h3 { font-size: 0.95rem; color: var(--primary); }
325
+ .md-content h4 { font-size: 0.875rem; color: var(--text-2); }
326
+ .md-content p { margin: 0.5rem 0; }
327
+ .md-content ul, .md-content ol {
328
+ margin: 0.5rem 0;
329
+ padding-left: 1.4rem;
330
+ }
331
+ .md-content li { margin: 0.2rem 0; }
332
+ .md-content ul li::marker { color: var(--accent); }
333
+ .md-content ol li::marker { color: var(--accent); font-weight: 600; }
334
+ .md-content strong { font-weight: 600; color: var(--text); }
335
+ .md-content em { color: var(--text-2); font-style: italic; }
336
+ .md-content code {
337
+ font-family: 'JetBrains Mono', 'SF Mono', Consolas, 'Courier New', monospace;
338
+ font-size: 0.8rem;
339
+ background: var(--surface-2);
340
+ border: 1px solid var(--border-2);
341
+ padding: 0.12rem 0.4rem;
342
+ border-radius: 4px;
343
  color: var(--accent);
344
  }
345
+ .md-content pre {
346
+ background: var(--surface-2);
347
+ border: 1px solid var(--border);
348
+ border-radius: var(--radius);
349
+ padding: 0.875rem 1rem;
350
+ overflow-x: auto;
 
 
 
 
351
  margin: 0.75rem 0;
 
 
 
 
 
 
 
 
 
 
352
  }
353
+ .md-content pre code {
354
+ background: none;
355
+ border: none;
356
+ padding: 0;
357
+ color: var(--text);
358
+ font-size: 0.8rem;
359
  }
360
+ .hljs { background: transparent !important; padding: 0 !important; }
361
+ .md-content table {
362
  width: 100%;
363
  border-collapse: collapse;
364
+ margin: 0.75rem 0;
365
+ font-size: 0.82rem;
 
 
366
  border: 1px solid var(--border);
367
+ border-radius: var(--radius);
368
+ overflow: hidden;
369
+ }
370
+ .md-content th {
371
+ background: var(--surface-2);
372
+ color: var(--text-2);
373
+ font-weight: 600;
374
+ font-size: 0.75rem;
375
+ text-transform: uppercase;
376
+ letter-spacing: 0.04em;
377
  padding: 0.5rem 0.75rem;
378
  text-align: left;
379
+ border-bottom: 1px solid var(--border);
380
  }
381
+ .md-content td {
382
+ padding: 0.45rem 0.75rem;
383
+ border-top: 1px solid var(--border);
384
+ color: var(--text);
385
  }
386
+ .md-content tr:hover td { background: rgba(255,255,255,0.015); }
387
+ .md-content blockquote {
388
+ border-left: 3px solid var(--primary);
389
+ background: var(--primary-glow);
390
+ padding: 0.6rem 0.875rem;
391
+ margin: 0.75rem 0;
392
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
393
+ color: var(--text-2);
394
+ }
395
+ .md-content a { color: var(--accent); text-decoration: none; border-bottom: 1px solid transparent; transition: border-color 0.15s; }
396
+ .md-content a:hover { border-bottom-color: var(--accent); }
397
+ .md-content hr { border: none; border-top: 1px solid var(--border); margin: 1rem 0; }
398
+ /* User bubble overrides */
399
+ .message.user .md-content h1,
400
+ .message.user .md-content h2,
401
+ .message.user .md-content h3,
402
+ .message.user .md-content h4 { color: rgba(255,255,255,0.9); border-color: rgba(255,255,255,0.2); }
403
+ .message.user .md-content strong { color: #fff; }
404
+ .message.user .md-content em { color: rgba(255,255,255,0.8); }
405
+ .message.user .md-content code {
406
+ background: rgba(255,255,255,0.15);
407
+ border-color: rgba(255,255,255,0.2);
408
+ color: #fff;
409
+ }
410
+ .message.user .md-content ul li::marker,
411
+ .message.user .md-content ol li::marker { color: rgba(255,255,255,0.7); }
412
+ /* Sources */
413
+ .sources-list {
414
+ display: flex;
415
+ flex-wrap: wrap;
416
+ align-items: center;
417
+ gap: 0.35rem;
418
+ margin-top: 0.75rem;
419
+ padding-top: 0.75rem;
420
+ border-top: 1px solid var(--border);
421
  }
422
+ .sources-label {
423
+ font-size: 0.68rem;
424
+ color: var(--text-3);
 
 
 
 
 
425
  font-weight: 600;
426
+ text-transform: uppercase;
427
+ letter-spacing: 0.05em;
428
  }
429
+ .source-tag {
430
+ font-size: 0.7rem;
431
+ padding: 0.18rem 0.55rem;
432
+ background: rgba(99,102,241,0.08);
433
+ border: 1px solid rgba(99,102,241,0.2);
434
+ border-radius: 20px;
435
+ color: var(--primary);
436
+ font-weight: 500;
437
  }
438
+ .viz-container {
439
+ margin-top: 0.875rem;
440
+ border-radius: var(--radius);
441
  border: 1px solid var(--border);
442
+ overflow: hidden;
443
+ width: 100%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
  }
445
+ /* THINKING / TOOL STEPS */
446
+ .thinking-bubble {
447
+ background: var(--surface);
448
+ border: 1px solid var(--border);
449
+ border-radius: var(--radius-lg);
450
+ border-bottom-left-radius: 4px;
451
+ padding: 0.75rem 1rem;
452
+ font-size: 0.82rem;
453
+ max-width: 90%;
454
+ min-width: 220px;
455
  }
456
+ .thinking-header {
457
+ display: flex;
458
+ align-items: center;
459
+ gap: 0.5rem;
460
+ color: var(--text-2);
461
+ margin-bottom: 0.5rem;
462
  }
463
+ .thinking-spinner {
464
+ width: 13px;
465
+ height: 13px;
466
+ border: 2px solid var(--border-2);
467
+ border-top-color: var(--primary);
468
+ border-radius: 50%;
469
+ animation: spin 0.75s linear infinite;
470
+ flex-shrink: 0;
471
  }
472
+ @keyframes spin { to { transform: rotate(360deg); } }
473
+ .thinking-label { font-weight: 500; font-size: 0.8rem; }
474
+ .tool-steps { display: flex; flex-direction: column; gap: 0.2rem; }
475
+ .tool-step {
476
+ display: flex;
477
+ align-items: center;
478
+ gap: 0.5rem;
479
  font-size: 0.75rem;
480
+ color: var(--text-3);
481
+ padding: 0.15rem 0;
482
+ animation: msgSlide 0.2s ease;
483
+ }
484
+ .tool-step i { color: var(--primary); width: 13px; text-align: center; font-size: 0.7rem; }
485
+ .tool-step.done i { color: var(--success); }
486
+ .progress-strip {
487
+ margin-top: 0.625rem;
488
+ height: 2px;
489
+ background: var(--border);
490
+ border-radius: 1px;
491
+ overflow: hidden;
492
  }
493
+ .progress-fill {
494
+ height: 100%;
495
+ background: linear-gradient(90deg, var(--primary), var(--accent));
496
+ border-radius: 1px;
497
+ transition: width 0.4s ease;
498
+ width: 0%;
 
 
 
 
499
  }
500
+ /* INPUT AREA */
501
  .input-area {
502
+ padding: 0.875rem 1.25rem 1rem;
 
503
  border-top: 1px solid var(--border);
504
+ background: var(--surface);
505
+ flex-shrink: 0;
506
  }
507
+ .input-box {
 
508
  display: flex;
509
+ align-items: flex-end;
510
+ gap: 0.625rem;
511
+ background: var(--surface-2);
512
+ border: 1.5px solid var(--border-2);
513
+ border-radius: var(--radius-xl);
514
+ padding: 0.55rem 0.625rem 0.55rem 1rem;
515
+ transition: border-color 0.2s, box-shadow 0.2s;
516
+ }
517
+ .input-box:focus-within {
518
+ border-color: var(--primary);
519
+ box-shadow: 0 0 0 3px var(--primary-glow);
520
  }
 
521
  .input-field {
522
  flex: 1;
523
+ background: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
  border: none;
525
+ outline: none;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
526
  color: var(--text);
527
+ font-size: 0.875rem;
528
+ font-family: 'Inter', sans-serif;
529
+ line-height: 1.55;
530
+ resize: none;
531
+ overflow-y: hidden;
532
+ max-height: 130px;
533
+ padding: 0;
534
+ min-height: 22px;
535
+ }
536
+ .input-field::placeholder { color: var(--text-3); }
537
+ .input-actions {
538
  display: flex;
539
  align-items: center;
540
  gap: 0.5rem;
541
+ flex-shrink: 0;
542
  }
543
+ .char-count {
544
+ font-size: 0.65rem;
545
+ color: var(--text-3);
546
+ font-variant-numeric: tabular-nums;
547
+ white-space: nowrap;
 
 
 
 
 
 
548
  }
549
+ .send-btn {
550
+ display: flex;
 
551
  align-items: center;
552
+ justify-content: center;
553
+ width: 30px;
554
+ height: 30px;
555
+ background: var(--primary);
556
+ border: none;
557
+ border-radius: var(--radius-sm);
558
+ color: #fff;
559
+ cursor: pointer;
560
+ font-size: 0.75rem;
561
+ transition: all 0.18s;
562
+ flex-shrink: 0;
563
+ font-family: inherit;
 
 
 
 
 
564
  }
565
+ .send-btn:hover:not(:disabled) {
566
+ background: var(--primary-dark);
567
+ transform: translateY(-1px);
568
+ box-shadow: 0 3px 10px rgba(99,102,241,0.35);
 
 
 
 
 
569
  }
570
+ .send-btn:active { transform: translateY(0); }
571
+ .send-btn:disabled { opacity: 0.45; cursor: not-allowed; transform: none; box-shadow: none; }
572
+ .input-hint {
573
+ font-size: 0.65rem;
574
+ color: var(--text-3);
575
+ margin-top: 0.4rem;
576
+ padding-left: 0.25rem;
577
  }
578
+ .input-hint kbd {
579
  display: inline-block;
580
+ font-family: 'JetBrains Mono', monospace;
581
+ font-size: 0.6rem;
582
+ background: var(--surface-3);
583
+ border: 1px solid var(--border-2);
584
+ border-radius: 3px;
585
+ padding: 0.05rem 0.3rem;
586
+ color: var(--text-2);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
587
  }
588
+ /* TOAST NOTIFICATION */
589
+ .toast {
590
  position: fixed;
591
+ top: 0.875rem;
592
+ right: 0.875rem;
593
  background: var(--surface);
594
+ border: 1px solid var(--border-2);
595
+ border-radius: var(--radius);
596
+ padding: 0.55rem 0.875rem;
597
+ font-size: 0.78rem;
598
+ color: var(--text-2);
599
+ display: flex;
600
+ align-items: center;
601
+ gap: 0.5rem;
602
  opacity: 0;
603
+ transform: translateY(-6px) scale(0.97);
604
+ transition: all 0.2s cubic-bezier(0.2, 0, 0.2, 1);
605
  z-index: 1000;
606
+ box-shadow: var(--shadow);
607
+ pointer-events: none;
608
+ max-width: 300px;
609
+ }
610
+ .toast.show { opacity: 1; transform: translateY(0) scale(1); }
611
+ .toast-icon { font-size: 0.75rem; flex-shrink: 0; }
612
+ .toast.processing { border-color: rgba(99,102,241,0.4); }
613
+ .toast.processing .toast-icon { color: var(--primary); }
614
+ .toast.success { border-color: rgba(16,185,129,0.4); }
615
+ .toast.success .toast-icon { color: var(--success); }
616
+ .toast.error { border-color: rgba(239,68,68,0.4); }
617
+ .toast.error .toast-icon { color: var(--error); }
618
+ .toast.warning { border-color: rgba(245,158,11,0.4); }
619
+ .toast.warning .toast-icon { color: var(--warning); }
620
+ /* LIGHT THEME OVERRIDES */
621
+ [data-theme="light"] .md-content pre {
622
+ background: #f6f8fa;
623
+ border-color: #d0d7de;
624
+ }
625
+ [data-theme="light"] .md-content code {
626
+ background: rgba(175,184,193,0.2);
627
+ border-color: rgba(175,184,193,0.4);
628
+ color: #0550ae;
629
+ }
630
+ [data-theme="light"] .message.assistant .message-bubble {
631
+ background: #fff;
632
+ border-color: rgba(0,0,0,0.08);
633
+ box-shadow: 0 1px 4px rgba(0,0,0,0.05);
634
+ }
635
+ [data-theme="light"] .thinking-bubble {
636
+ background: #fff;
637
+ border-color: rgba(0,0,0,0.08);
638
+ }
639
+ [data-theme="light"] .chat-messages { background: var(--bg); }
640
+ /* RESPONSIVE */
641
+ @media (max-width: 640px) {
642
+ .app { max-width: 100%; border: none; }
643
+ .header { padding: 0 1rem; height: 48px; }
644
+ .header-title { font-size: 0.82rem; }
645
+ .status-badge { display: none; }
646
+ .chat-messages { padding: 1rem; }
647
+ .message-bubble { max-width: 92%; font-size: 0.85rem; }
648
+ .message.assistant .message-bubble { max-width: 98%; }
649
+ .input-area { padding: 0.75rem 0.875rem 0.875rem; }
650
+ .example-chips { gap: 0.375rem; }
651
+ .chip { font-size: 0.75rem; padding: 0.375rem 0.75rem; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
652
  }
templates/index.html CHANGED
@@ -4,99 +4,119 @@
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Web3 Research Co-Pilot</title>
7
- <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22><path fill=%22%2300d4aa%22 d=%22M12 2L2 7v10c0 5.5 3.8 7.7 9 9 5.2-1.3 9-3.5 9-9V7l-10-5z%22/></svg>">
8
- <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
9
- <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
 
 
 
 
 
 
 
10
  <link rel="stylesheet" href="/static/styles.css">
11
  </head>
12
  <body>
13
- <div id="statusIndicator" class="status-indicator">
 
 
14
  <span id="statusText">Ready</span>
15
  </div>
16
-
17
- <div class="container">
18
- <div class="header">
19
- <div class="header-content">
20
- <div class="header-text">
21
- <h1><span class="brand">Web3</span> Research Co-Pilot</h1>
22
- <p>Professional cryptocurrency analysis and market intelligence</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  </div>
24
- <div class="header-controls">
25
- <div class="llm-toggle">
26
- <span class="toggle-label">AI Model:</span>
27
- <label class="switch">
28
- <input type="checkbox" id="geminiToggle" title="Switch between Ollama (Local) and Gemini (Cloud)">
29
- <span class="slider round">
30
- <span class="slider-text-off">Ollama</span>
31
- <span class="slider-text-on">Gemini</span>
32
- </span>
33
- </label>
34
- </div>
35
- <button id="themeToggle" class="theme-toggle" title="Toggle theme">
36
- <i class="fas fa-moon"></i>
37
  </button>
38
  </div>
 
 
 
39
  </div>
40
- </div>
41
-
42
- <div id="status" class="status checking">
43
- <span>Initializing research systems...</span>
44
- </div>
45
 
46
- <div class="chat-interface">
47
- <div id="chatMessages" class="chat-messages">
48
- <div class="welcome">
49
- <h3>Welcome to Web3 Research Co-Pilot</h3>
50
- <p>Ask about market trends, DeFi protocols, or blockchain analytics</p>
51
- </div>
52
- </div>
53
- <div id="loadingIndicator" class="loading-indicator">
54
- <div class="loading-spinner"></div>
55
- <span id="loadingText">Processing your research query...</span>
56
- <div class="progress-container">
57
- <div class="progress-bar" style="width: 0%;"></div>
58
- </div>
59
- </div>
60
- <div class="input-area">
61
- <div class="input-container">
62
- <input
63
- type="text"
64
- id="queryInput"
65
- class="input-field"
66
- placeholder="Research Bitcoin trends, analyze DeFi yields, compare protocols..."
67
- maxlength="500"
68
- >
69
- <button id="sendBtn" class="send-button">Research</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  </div>
71
- </div>
72
- </div>
73
 
74
- <div class="examples">
75
- <div class="example" onclick="setQuery('Analyze Bitcoin price trends and institutional adoption patterns')">
76
- <div class="example-title"><i class="fas fa-chart-line"></i> Market Analysis</div>
77
- <div class="example-desc">Bitcoin trends, institutional flows, and market sentiment analysis</div>
78
- </div>
79
- <div class="example" onclick="setQuery('Compare top DeFi protocols by TVL, yield, and risk metrics across chains')">
80
- <div class="example-title"><i class="fas fa-coins"></i> DeFi Intelligence</div>
81
- <div class="example-desc">Protocol comparison, yield analysis, and cross-chain opportunities</div>
82
- </div>
83
- <div class="example" onclick="setQuery('Evaluate Ethereum Layer 2 scaling solutions and adoption metrics')">
84
- <div class="example-title"><i class="fas fa-layer-group"></i> Layer 2 Research</div>
85
- <div class="example-desc">Scaling solutions, transaction costs, and ecosystem growth</div>
86
- </div>
87
- <div class="example" onclick="setQuery('Find optimal yield farming strategies with risk assessment')">
88
- <div class="example-title"><i class="fas fa-seedling"></i> Yield Optimization</div>
89
- <div class="example-desc">Cross-chain opportunities, APY tracking, and risk analysis</div>
90
- </div>
91
- <div class="example" onclick="setQuery('Track whale movements and large Bitcoin transactions today')">
92
- <div class="example-title"><i class="fas fa-fish"></i> Whale Tracking</div>
93
- <div class="example-desc">Large transactions, wallet analysis, and market impact</div>
94
- </div>
95
- <div class="example" onclick="setQuery('Analyze gas fees and network congestion across blockchains')">
96
- <div class="example-title"><i class="fas fa-tachometer-alt"></i> Network Analytics</div>
97
- <div class="example-desc">Gas prices, network utilization, and cost comparisons</div>
98
  </div>
99
- </div>
100
  </div>
101
 
102
  <script src="/static/app.js"></script>
 
4
  <meta charset="UTF-8">
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>Web3 Research Co-Pilot</title>
7
+ <!-- Prevent flash of unstyled content -->
8
+ <script>(function(){var t=localStorage.getItem('theme')||'dark';document.documentElement.setAttribute('data-theme',t);})()</script>
9
+ <link rel="preconnect" href="https://fonts.googleapis.com">
10
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
11
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
12
+ <link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 24 24%22><path fill=%22%236366f1%22 d=%22M12 2L2 7v10c0 5.5 3.8 7.7 9 9 5.2-1.3 9-3.5 9-9V7l-10-5z%22/></svg>">
13
+ <script src="https://cdn.jsdelivr.net/npm/marked@9/marked.min.js"></script>
14
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css" id="hljs-theme">
15
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
16
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" rel="stylesheet">
17
  <link rel="stylesheet" href="/static/styles.css">
18
  </head>
19
  <body>
20
+ <!-- Toast notification -->
21
+ <div id="statusIndicator" class="toast">
22
+ <i class="fas fa-circle-info toast-icon"></i>
23
  <span id="statusText">Ready</span>
24
  </div>
25
+
26
+ <div class="app">
27
+ <!-- Header -->
28
+ <header class="header">
29
+ <div class="header-left">
30
+ <svg class="logo-icon" width="26" height="26" viewBox="0 0 24 24" fill="none">
31
+ <path d="M12 2L2 7v10c0 5.5 3.8 7.7 9 9 5.2-1.3 9-3.5 9-9V7L12 2z" fill="url(#logoGrad)"/>
32
+ <defs>
33
+ <linearGradient id="logoGrad" x1="0%" y1="0%" x2="100%" y2="100%">
34
+ <stop offset="0%" style="stop-color:#6366f1"/>
35
+ <stop offset="100%" style="stop-color:#22d3ee"/>
36
+ </linearGradient>
37
+ </defs>
38
+ </svg>
39
+ <span class="header-title"><span class="brand">Web3</span> Research Co-Pilot</span>
40
+ </div>
41
+ <div class="header-right">
42
+ <div id="statusBadge" class="status-badge checking">
43
+ <span class="status-dot"></span>
44
+ <span id="statusBadgeText">Connecting</span>
45
  </div>
46
+ <div class="model-select">
47
+ <button class="model-btn active" id="btnOllama" onclick="setModel('ollama')">
48
+ <i class="fas fa-microchip"></i> Local
49
+ </button>
50
+ <button class="model-btn" id="btnGemini" onclick="setModel('gemini')">
51
+ <i class="fas fa-cloud"></i> Gemini
 
 
 
 
 
 
 
52
  </button>
53
  </div>
54
+ <button id="themeToggle" class="icon-btn" title="Toggle theme">
55
+ <i class="fas fa-sun"></i>
56
+ </button>
57
  </div>
58
+ </header>
 
 
 
 
59
 
60
+ <!-- Main -->
61
+ <main class="main">
62
+ <div class="chat-wrap">
63
+ <div id="chatMessages" class="chat-messages">
64
+ <div class="welcome-screen">
65
+ <svg class="welcome-icon" width="52" height="52" viewBox="0 0 24 24" fill="none">
66
+ <path d="M12 2L2 7v10c0 5.5 3.8 7.7 9 9 5.2-1.3 9-3.5 9-9V7L12 2z" fill="url(#welcomeGrad)"/>
67
+ <defs>
68
+ <linearGradient id="welcomeGrad" x1="0%" y1="0%" x2="100%" y2="100%">
69
+ <stop offset="0%" style="stop-color:#6366f1"/>
70
+ <stop offset="100%" style="stop-color:#22d3ee"/>
71
+ </linearGradient>
72
+ </defs>
73
+ </svg>
74
+ <h2>Web3 Research Co-Pilot</h2>
75
+ <p>Professional cryptocurrency analysis and market intelligence</p>
76
+ <div class="example-chips">
77
+ <button class="chip" onclick="setQuery('Analyze Bitcoin price trends and institutional adoption patterns')">
78
+ <i class="fas fa-chart-line"></i> BTC Analysis
79
+ </button>
80
+ <button class="chip" onclick="setQuery('Compare top DeFi protocols by TVL, yield, and risk metrics across chains')">
81
+ <i class="fas fa-coins"></i> DeFi Compare
82
+ </button>
83
+ <button class="chip" onclick="setQuery('Evaluate Ethereum Layer 2 scaling solutions and adoption metrics')">
84
+ <i class="fas fa-layer-group"></i> L2 Research
85
+ </button>
86
+ <button class="chip" onclick="setQuery('Find optimal yield farming strategies with risk assessment')">
87
+ <i class="fas fa-seedling"></i> Yield Farming
88
+ </button>
89
+ <button class="chip" onclick="setQuery('Track whale movements and large Bitcoin transactions today')">
90
+ <i class="fas fa-fish"></i> Whale Tracker
91
+ </button>
92
+ <button class="chip" onclick="setQuery('Analyze gas fees and network congestion across blockchains')">
93
+ <i class="fas fa-gauge-high"></i> Gas Analytics
94
+ </button>
95
+ </div>
96
+ </div>
97
  </div>
 
 
98
 
99
+ <!-- Input -->
100
+ <div class="input-area">
101
+ <div class="input-box">
102
+ <textarea
103
+ id="queryInput"
104
+ class="input-field"
105
+ placeholder="Ask about crypto markets, DeFi protocols, blockchain analytics..."
106
+ maxlength="1000"
107
+ rows="1"
108
+ ></textarea>
109
+ <div class="input-actions">
110
+ <span class="char-count" id="charCount">0 / 1000</span>
111
+ <button id="sendBtn" class="send-btn" title="Send (Enter)">
112
+ <i class="fas fa-paper-plane"></i>
113
+ </button>
114
+ </div>
115
+ </div>
116
+ <p class="input-hint"><kbd>Enter</kbd> to send &nbsp;·&nbsp; <kbd>Shift+Enter</kbd> for new line</p>
117
+ </div>
 
 
 
 
 
118
  </div>
119
+ </main>
120
  </div>
121
 
122
  <script src="/static/app.js"></script>