ada-web

Ada AI is a specialized coding assistant designed for developers and students. Powered by Google Gemini 3.1, it provides real-time, streamin...

git clone https://github.com/notamitgamer/ada-web.git
· rebuilt hourlyOpen on GitHub
commit 7624a386203dde9be7587db9cf6f829782436b1e
parent 4ce573652822b4f4a6a92dd3457910b152519e1b
Author: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Date:   Sat, 17 Jan 2026 16:33:25 +0000

Add frontend improvements: typing animation, better error handling, and markdown styling

Co-authored-by: notamitgamer <199483577+notamitgamer@users.noreply.github.com>

Diffstat:
Mindex.html | 158++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
1 file changed, 121 insertions(+), 37 deletions(-)

diff --git a/index.html b/index.html @@ -85,17 +85,40 @@ /* Markdown Styling */ .prose p { margin-bottom: 0.8rem; line-height: 1.6; } .prose h1, .prose h2, .prose h3 { color: var(--accent); font-weight: bold; margin-top: 1rem; margin-bottom: 0.5rem; } + .prose h1 { font-size: 1.5rem; } + .prose h2 { font-size: 1.25rem; } + .prose h3 { font-size: 1.1rem; } .prose code { color: var(--accent); background: rgba(190, 242, 100, 0.1); padding: 2px 4px; border-radius: 4px; font-family: 'JetBrains Mono'; font-size: 0.85em; } .prose pre { background: #111; padding: 1rem; border-radius: 0.5rem; overflow-x: auto; border: 1px solid var(--border); margin-bottom: 1rem; } .prose pre code { background: transparent; padding: 0; color: #ccc; } .prose ul { list-style-type: disc; padding-left: 1.5rem; margin-bottom: 1rem; } .prose ol { list-style-type: decimal; padding-left: 1.5rem; margin-bottom: 1rem; } + .prose li { margin-bottom: 0.3rem; line-height: 1.5; } + .prose blockquote { border-left: 3px solid var(--accent); padding-left: 1rem; margin: 1rem 0; color: #999; } + .prose a { color: var(--accent); text-decoration: underline; } + .prose strong { color: var(--accent); font-weight: 600; } + .prose em { font-style: italic; color: #aaa; } /* Utilities */ .glass-panel { background: var(--glass); backdrop-filter: blur(12px); border-right: 1px solid var(--border); } .loader { border: 2px solid #333; border-top: 2px solid var(--accent); border-radius: 50%; width: 16px; height: 16px; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } + /* Typing Indicator Animation */ + .typing-dot { + width: 8px; + height: 8px; + background: var(--accent); + border-radius: 50%; + display: inline-block; + animation: typing 1.4s infinite; + opacity: 0.6; + } + @keyframes typing { + 0%, 60%, 100% { transform: translateY(0); opacity: 0.6; } + 30% { transform: translateY(-10px); opacity: 1; } + } + /* Mobile Tabs */ .tab-btn.active { border-bottom: 2px solid var(--accent); color: var(--accent); } </style> @@ -271,6 +294,14 @@ window.addEventListener('resize', () => editor.refresh()); + // Configure marked.js for better markdown rendering + marked.setOptions({ + breaks: true, + gfm: true, + headerIds: false, + mangle: false + }); + // --- AUTH LOGIC --- onAuthStateChanged(auth, (user) => { if (user) { @@ -379,8 +410,8 @@ const loadingId = appendLoading(); try { - // Get fresh token - const token = await currentUser.getIdToken(); + // Get fresh token (force refresh to ensure it's valid) + const token = await currentUser.getIdToken(true); // Render Backend URL const API_URL = 'https://ada-web.onrender.com/api/chat'; @@ -402,44 +433,85 @@ if (!response.ok) { if (response.status === 401) { - throw new Error("401 Unauthorized: Backend rejected the token. Ensure Render Backend uses the correct Firebase Admin SDK credentials for 'ada-ai-aranag'."); + // Try to refresh token and retry once + const freshToken = await currentUser.getIdToken(true); + const retryResponse = await fetch(API_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${freshToken}` + }, + body: JSON.stringify({ + message: text, + history: chatHistory.slice(-10), + codeContext: codeContext, + fileContext: fileContextContent, + sessionId: currentSessionId + }) + }); + + if (!retryResponse.ok) { + throw new Error("Authentication failed. Please sign out and sign in again."); + } + + // Process retry response + await processStreamingResponse(retryResponse, loadingId, text); + return; } - throw new Error(`API Error: ${response.status}`); - } - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - let fullResponse = ""; - let aiMsgDiv = document.getElementById(loadingId); - - aiMsgDiv.innerHTML = '<div class="prose text-sm text-neutral-300 max-w-none"></div>'; - let contentDiv = aiMsgDiv.querySelector('.prose'); - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - const chunk = decoder.decode(value); - fullResponse += chunk; - - const parts = parseResponse(fullResponse); - contentDiv.innerHTML = marked.parse(parts.text); - if (parts.code) { - if (editor.getValue().trim() !== parts.code.trim()) { - editor.setValue(parts.code); + let errorMessage = `API Error: ${response.status}`; + try { + const errorData = await response.json(); + if (errorData.message) { + errorMessage = errorData.message; } + } catch (e) { + // Could not parse error as JSON } - chatContainer.scrollTop = chatContainer.scrollHeight; + throw new Error(errorMessage); } - - chatHistory.push({ user: text, model: fullResponse }); - fileContextContent = ""; - document.getElementById('file-preview').classList.add('hidden'); + + await processStreamingResponse(response, loadingId, text); } catch (e) { - document.getElementById(loadingId).innerText = "Error: " + e.message; + const loadingDiv = document.getElementById(loadingId); + loadingDiv.innerHTML = `<div class="text-sm text-red-400 bg-red-900/20 p-3 rounded-lg border border-red-500/30"> + <strong>Error:</strong> ${e.message} + </div>`; + console.error("Chat error:", e); + } + } + + async function processStreamingResponse(response, loadingId, userText) { + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let fullResponse = ""; + let aiMsgDiv = document.getElementById(loadingId); + + aiMsgDiv.innerHTML = '<div class="prose text-sm text-neutral-300 max-w-none"></div>'; + let contentDiv = aiMsgDiv.querySelector('.prose'); + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + const chunk = decoder.decode(value); + fullResponse += chunk; + + const parts = parseResponse(fullResponse); + contentDiv.innerHTML = marked.parse(parts.text); + + if (parts.code) { + if (editor.getValue().trim() !== parts.code.trim()) { + editor.setValue(parts.code); + } + } + chatContainer.scrollTop = chatContainer.scrollHeight; } + + chatHistory.push({ user: userText, model: fullResponse }); + fileContextContent = ""; + document.getElementById('file-preview').classList.add('hidden'); } function parseResponse(raw) { @@ -447,15 +519,27 @@ const endMarker = "<<<CODE_END>>>"; let text = raw; let code = null; + if (raw.includes(startMarker)) { const parts = raw.split(startMarker); - text = parts[0]; - if (parts[1].includes(endMarker)) { - code = parts[1].split(endMarker)[0].trim(); - } else { - code = parts[1].trim(); + text = parts[0].trim(); + + if (parts[1]) { + if (parts[1].includes(endMarker)) { + // Complete code block + const codeParts = parts[1].split(endMarker); + code = codeParts[0].trim(); + // Append any text after code block back to main text + if (codeParts[1] && codeParts[1].trim()) { + text += "\n\n" + codeParts[1].trim(); + } + } else { + // Incomplete code block (still streaming) + code = parts[1].trim(); + } } } + return { text, code }; }