jashdoshi77's picture
added questionnaire
a267a62
/* ═══════════════════════════════════════════════════════════════
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