Spaces:
Sleeping
Sleeping
| /* βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| Stacklogix β Chat Client (Vanilla JS) | |
| βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ */ | |
| const API_BASE = window.location.origin; | |
| // βββ State ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let currentSessionId = null; | |
| let isWaiting = false; | |
| // βββ DOM refs βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| const $messages = document.getElementById("messages"); | |
| const $msgContainer = document.getElementById("messagesContainer"); | |
| const $input = document.getElementById("userInput"); | |
| const $btnSend = document.getElementById("btnSend"); | |
| const $btnNewChat = document.getElementById("btnNewChat"); | |
| const $btnToggle = document.getElementById("btnToggleSidebar"); | |
| const $sidebar = document.getElementById("sidebar"); | |
| const $sessionList = document.getElementById("sessionList"); | |
| const $welcomeScreen = document.getElementById("welcomeScreen"); | |
| // βββ UUID generator βββββββββββββββββββββββββββββββββββββββββββ | |
| function uuid() { | |
| return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, c => { | |
| const r = (Math.random() * 16) | 0; | |
| return (c === "x" ? r : (r & 0x3) | 0x8).toString(16); | |
| }); | |
| } | |
| // βββ Simple markdown β HTML βββββββββββββββββββββββββββββββββββ | |
| function renderMarkdown(text) { | |
| if (!text) return ""; | |
| let html = text | |
| // Code blocks | |
| .replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>') | |
| // Inline code | |
| .replace(/`([^`]+)`/g, '<code>$1</code>') | |
| // Bold | |
| .replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>') | |
| // Italic | |
| .replace(/\*(.+?)\*/g, '<em>$1</em>') | |
| // Headers | |
| .replace(/^### (.+)$/gm, '<h3>$1</h3>') | |
| .replace(/^## (.+)$/gm, '<h2>$1</h2>') | |
| .replace(/^# (.+)$/gm, '<h1>$1</h1>') | |
| // Blockquote | |
| .replace(/^> (.+)$/gm, '<blockquote>$1</blockquote>') | |
| // Unordered list items | |
| .replace(/^[-*] (.+)$/gm, '<li>$1</li>') | |
| // Numbered list items | |
| .replace(/^\d+\. (.+)$/gm, '<li>$1</li>'); | |
| // Wrap consecutive <li> in <ul> | |
| html = html.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>'); | |
| // Paragraphs (lines not already wrapped in block elements) | |
| // Allow <strong> and <em> lines to be wrapped in <p> (they're inline, not block) | |
| html = html.replace(/^(?!<[hupbol]|<li|<code|<pre|<block|<ul|<div)(.+)$/gm, '<p>$1</p>'); | |
| // Clean up extra newlines | |
| html = html.replace(/\n{2,}/g, '\n'); | |
| // Highlight questions β wrap <li> or <p> that contain a "?" in a styled box | |
| html = html.replace( | |
| /<li>(.*?\?)\s*<\/li>/g, | |
| '<li class="question-item">$1</li>' | |
| ); | |
| html = html.replace( | |
| /<p>(.*?\?)\s*<\/p>/g, | |
| '<div class="question-highlight"><span class="question-icon">β</span><p>$1</p></div>' | |
| ); | |
| return html; | |
| } | |
| // βββ Add message to chat ββββββββββββββββββββββββββββββββββββββ | |
| function addMessage(role, content) { | |
| // Hide welcome | |
| if ($welcomeScreen) $welcomeScreen.style.display = "none"; | |
| const div = document.createElement("div"); | |
| div.className = `message ${role}-message`; | |
| const avatar = document.createElement("div"); | |
| avatar.className = "message-avatar"; | |
| avatar.textContent = role === "user" ? "U" : "β"; | |
| const bubble = document.createElement("div"); | |
| bubble.className = "message-bubble"; | |
| bubble.innerHTML = role === "assistant" ? renderMarkdown(content) : escapeHtml(content); | |
| div.appendChild(avatar); | |
| div.appendChild(bubble); | |
| $messages.appendChild(div); | |
| scrollToBottom(); | |
| } | |
| function escapeHtml(text) { | |
| const d = document.createElement("div"); | |
| d.textContent = text; | |
| return d.innerHTML.replace(/\n/g, "<br>"); | |
| } | |
| function scrollToBottom() { | |
| requestAnimationFrame(() => { | |
| $msgContainer.scrollTop = $msgContainer.scrollHeight; | |
| }); | |
| } | |
| // βββ Typing indicator βββββββββββββββββββββββββββββββββββββββββ | |
| function showTyping() { | |
| const div = document.createElement("div"); | |
| div.className = "typing-indicator"; | |
| div.id = "typingIndicator"; | |
| div.innerHTML = ` | |
| <div class="message-avatar" style="background:var(--accent-gradient);color:white;width:36px;height:36px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:14px;font-weight:700;">β</div> | |
| <div class="typing-dots"><span></span><span></span><span></span></div> | |
| `; | |
| $messages.appendChild(div); | |
| scrollToBottom(); | |
| } | |
| function hideTyping() { | |
| const el = document.getElementById("typingIndicator"); | |
| if (el) el.remove(); | |
| } | |
| // βββ API calls ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // βββ Create session via API & show greeting ββββββββββββββββββ | |
| async function createNewSession() { | |
| try { | |
| const res = await fetch(`${API_BASE}/sessions/new`, { method: "POST" }); | |
| const data = await res.json(); | |
| currentSessionId = data.session_id; | |
| // Clear and show greeting | |
| $messages.innerHTML = ""; | |
| if ($welcomeScreen) { | |
| $messages.appendChild($welcomeScreen); | |
| $welcomeScreen.style.display = "none"; | |
| } | |
| addMessage("assistant", data.greeting); | |
| loadSessions(); | |
| return data.session_id; | |
| } catch { | |
| // Fallback: generate local id | |
| currentSessionId = uuid(); | |
| return currentSessionId; | |
| } | |
| } | |
| async function sendMessage(message) { | |
| if (!message.trim() || isWaiting) return; | |
| // Auto-create session if none | |
| if (!currentSessionId) { | |
| await createNewSession(); | |
| } | |
| addMessage("user", message); | |
| $input.value = ""; | |
| $input.style.height = "auto"; | |
| $btnSend.disabled = true; | |
| isWaiting = true; | |
| showTyping(); | |
| try { | |
| const res = await fetch(`${API_BASE}/chat`, { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ session_id: currentSessionId, message }), | |
| }); | |
| if (!res.ok) { | |
| const err = await res.json().catch(() => ({})); | |
| throw new Error(err.detail || `HTTP ${res.status}`); | |
| } | |
| const data = await res.json(); | |
| hideTyping(); | |
| addMessage("assistant", data.reply); | |
| loadSessions(); // refresh sidebar | |
| } catch (err) { | |
| hideTyping(); | |
| addMessage("assistant", `β οΈ **Error:** ${err.message}\n\nPlease check that the server is running and try again.`); | |
| } finally { | |
| isWaiting = false; | |
| $btnSend.disabled = !$input.value.trim(); | |
| } | |
| } | |
| async function loadSessions() { | |
| try { | |
| const res = await fetch(`${API_BASE}/sessions`); | |
| const data = await res.json(); | |
| renderSessionList(data.sessions || []); | |
| } catch { /* server might not be ready */ } | |
| } | |
| async function loadSessionHistory(sessionId) { | |
| try { | |
| const res = await fetch(`${API_BASE}/sessions/${sessionId}`); | |
| if (!res.ok) return; | |
| const data = await res.json(); | |
| // Clear messages | |
| $messages.innerHTML = ""; | |
| if ($welcomeScreen) { | |
| $messages.appendChild($welcomeScreen); | |
| $welcomeScreen.style.display = "none"; | |
| } | |
| // Re-render history | |
| (data.messages || []).forEach(m => addMessage(m.role, m.content)); | |
| currentSessionId = sessionId; | |
| highlightActiveSession(); | |
| } catch { /* ignore */ } | |
| } | |
| async function deleteSessionById(sessionId) { | |
| try { | |
| await fetch(`${API_BASE}/sessions/${sessionId}`, { method: "DELETE" }); | |
| if (currentSessionId === sessionId) { | |
| currentSessionId = null; | |
| $messages.innerHTML = ""; | |
| if ($welcomeScreen) { | |
| $messages.appendChild($welcomeScreen); | |
| $welcomeScreen.style.display = ""; | |
| } | |
| } | |
| loadSessions(); | |
| } catch { /* ignore */ } | |
| } | |
| // βββ Session list rendering ββββββββββββββββββββββββββββββββββ | |
| function renderSessionList(sessions) { | |
| $sessionList.innerHTML = ""; | |
| sessions.forEach(s => { | |
| const div = document.createElement("div"); | |
| div.className = "session-item" + (s.session_id === currentSessionId ? " active" : ""); | |
| div.innerHTML = ` | |
| <span class="session-item-label">Session Β· ${s.message_count || 0} msgs</span> | |
| <button class="session-item-delete" title="Delete">Γ</button> | |
| `; | |
| div.querySelector(".session-item-label").addEventListener("click", () => { | |
| loadSessionHistory(s.session_id); | |
| }); | |
| div.querySelector(".session-item-delete").addEventListener("click", e => { | |
| e.stopPropagation(); | |
| deleteSessionById(s.session_id); | |
| }); | |
| $sessionList.appendChild(div); | |
| }); | |
| } | |
| function highlightActiveSession() { | |
| document.querySelectorAll(".session-item").forEach(el => el.classList.remove("active")); | |
| // Not trivial to match back β rely on re-render from loadSessions | |
| } | |
| // βββ New chat βββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function startNewChat() { | |
| currentSessionId = null; | |
| await createNewSession(); | |
| $input.focus(); | |
| } | |
| // βββ Event listeners ββββββββββββββββββββββββββββββββββββββββββ | |
| $btnSend.addEventListener("click", () => sendMessage($input.value)); | |
| $input.addEventListener("input", () => { | |
| $btnSend.disabled = !$input.value.trim() || isWaiting; | |
| // Auto-resize | |
| $input.style.height = "auto"; | |
| $input.style.height = Math.min($input.scrollHeight, 150) + "px"; | |
| }); | |
| $input.addEventListener("keydown", e => { | |
| if (e.key === "Enter" && !e.shiftKey) { | |
| e.preventDefault(); | |
| sendMessage($input.value); | |
| } | |
| }); | |
| $btnNewChat.addEventListener("click", startNewChat); | |
| $btnToggle.addEventListener("click", () => { | |
| $sidebar.classList.toggle("hidden"); | |
| }); | |
| // Suggestion chips | |
| document.querySelectorAll(".suggestion-chip").forEach(chip => { | |
| chip.addEventListener("click", () => { | |
| const msg = chip.getAttribute("data-msg"); | |
| sendMessage(msg); | |
| }); | |
| }); | |
| // βββ Init βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| loadSessions(); | |
| createNewSession(); // auto-start first session with greeting | |