const state = { sessionId: null, sessions: [], messages: [], reminders: [], editingReminderId: null, }; const DEFAULT_API_BASE = "http://127.0.0.1:8000"; let apiBase = window.location.protocol === "file:" ? DEFAULT_API_BASE : ""; const WELCOME_HTML = `
AI
Your AI Workspace
Manage tasks, set reminders, capture notes, and search past chats from one clean workspace.
`; const chatMessages = document.getElementById("chatMessages"); const chatForm = document.getElementById("chatForm"); const messageInput = document.getElementById("messageInput"); const sessionList = document.getElementById("sessionList"); const mobileSessionList = document.getElementById("mobileSessionList"); const summaryCards = document.getElementById("summaryCards"); const searchInput = document.getElementById("searchInput"); const searchResults = document.getElementById("searchResults"); const helperPanel = document.getElementById("helperPanel"); const sideHelperContent = document.getElementById("sideHelperContent"); const messageTemplate = document.getElementById("messageTemplate"); const pageOverlay = document.getElementById("pageOverlay"); const reminderModal = document.getElementById("reminderModal"); const reminderForm = document.getElementById("reminderForm"); const reminderTitleInput = document.getElementById("reminderTitle"); const reminderNoteInput = document.getElementById("reminderNote"); const reminderDateInput = document.getElementById("reminderDate"); const reminderTimeInput = document.getElementById("reminderTime"); const reminderFormMessage = document.getElementById("reminderFormMessage"); const reminderList = document.getElementById("reminderList"); const saveReminderBtn = document.getElementById("saveReminderBtn"); const reminderModalTitle = document.getElementById("reminderModalTitle"); function updateOverlay() { const open = document.body.classList.contains("left-drawer-open") || document.body.classList.contains("right-drawer-open"); pageOverlay.classList.toggle("hidden", !open); } function openDrawer(side) { document.body.classList.add(`${side}-drawer-open`); updateOverlay(); } function closeDrawers() { document.body.classList.remove("left-drawer-open", "right-drawer-open"); updateOverlay(); } document.getElementById("openLeftDrawerBtn")?.addEventListener("click", () => openDrawer("left")); document.getElementById("closeLeftDrawerBtn")?.addEventListener("click", closeDrawers); document.getElementById("openRightDrawerBtn")?.addEventListener("click", () => openDrawer("right")); document.getElementById("closeRightDrawerBtn")?.addEventListener("click", closeDrawers); pageOverlay?.addEventListener("click", closeDrawers); const tabDefs = { actions: "tab-actions", guide: "tab-guide", focus: "tab-focus" }; function setActiveTab(tabKey) { document.querySelectorAll(".panel-tab").forEach(button => { button.classList.toggle("active", button.dataset.tab === tabKey); }); Object.entries(tabDefs).forEach(([key, id]) => { const element = document.getElementById(id); if (!element) return; element.style.display = key === tabKey ? (key === "actions" ? "grid" : "flex") : "none"; }); } document.querySelectorAll(".panel-tab").forEach(button => { button.addEventListener("click", () => setActiveTab(button.dataset.tab)); }); setActiveTab("actions"); function escapeHtml(text) { return String(text) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">"); } function renderMarkdown(text) { let html = escapeHtml(text); html = html.replace(/^### (.*)$/gm, "

$1

"); html = html.replace(/^## (.*)$/gm, "

$1

"); html = html.replace(/^# (.*)$/gm, "

$1

"); html = html.replace(/\*\*(.+?)\*\*/g, "$1"); html = html.replace(/\*(.+?)\*/g, "$1"); html = html.replace(/`([^`]+)`/g, "$1"); html = html.replace(/^- (.*)$/gm, "
  • $1
  • "); html = html.replace(/(
  • .*<\/li>)/gs, ""); html = html.replace(/\n/g, "
    "); return html; } async function rawRequest(base, url, options = {}) { const mergedOptions = { ...options }; const method = (mergedOptions.method || "GET").toUpperCase(); const headers = new Headers(mergedOptions.headers || {}); if (mergedOptions.body && !headers.has("Content-Type")) { headers.set("Content-Type", "application/json"); } if (!mergedOptions.body && method === "GET" && headers.has("Content-Type")) { headers.delete("Content-Type"); } mergedOptions.headers = headers; return fetch(`${base}${url}`, mergedOptions); } async function resolveApiBase() { const candidates = []; if (window.location.protocol !== "file:") { candidates.push(""); } if (!candidates.includes(DEFAULT_API_BASE)) { candidates.push(DEFAULT_API_BASE); } for (const candidate of candidates) { try { const response = await rawRequest(candidate, "/health"); if (response.ok) { apiBase = candidate; return; } } catch (error) { continue; } } throw new Error(`Could not reach the backend. Start the app with run.bat and open ${DEFAULT_API_BASE} .`); } async function api(url, options = {}) { if (apiBase === "" && window.location.protocol !== "file:" && window.location.port !== "8000") { await resolveApiBase(); } let response; try { response = await rawRequest(apiBase, url, options); } catch (error) { if (apiBase !== DEFAULT_API_BASE) { apiBase = DEFAULT_API_BASE; response = await rawRequest(apiBase, url, options).catch(() => null); } if (!response) { throw new Error(`Could not reach the backend. Start the app with run.bat and open ${DEFAULT_API_BASE} .`); } } if (!response.ok && apiBase !== DEFAULT_API_BASE && window.location.port !== "8000") { try { const fallback = await rawRequest(DEFAULT_API_BASE, url, options); if (fallback.ok) { apiBase = DEFAULT_API_BASE; return fallback.json(); } } catch (error) { // Ignore and surface the original response below. } } if (!response.ok) { const error = await response.json().catch(() => null); const detail = error?.detail || `${response.status} ${response.statusText}` || "Request failed"; throw new Error(detail); } return response.json(); } function autoResize() { messageInput.style.height = "auto"; messageInput.style.height = `${Math.min(messageInput.scrollHeight, 120)}px`; } function scrollToBottom() { chatMessages.scrollTop = chatMessages.scrollHeight; } function setHelperContent(title, content) { helperPanel.classList.remove("hidden"); helperPanel.innerHTML = `${escapeHtml(title)}
    ${escapeHtml(content)}
    `; if (sideHelperContent) { sideHelperContent.textContent = `${title}\n\n${content}`; } setActiveTab("focus"); } function placePrompt(prompt, autoSend = false) { messageInput.value = prompt; autoResize(); messageInput.focus(); messageInput.setSelectionRange(prompt.length, prompt.length); if (autoSend && prompt.trim()) chatForm.requestSubmit(); } function ensureWelcomeState() { const existing = document.getElementById("welcomeState"); if (existing) return existing; const node = document.createElement("div"); node.id = "welcomeState"; node.className = "welcome-state"; node.innerHTML = WELCOME_HTML; chatMessages.appendChild(node); bindPromptButtons(); return node; } function addMessage(message, saveable = true) { const currentWelcome = document.getElementById("welcomeState"); if (currentWelcome) currentWelcome.style.display = "none"; const node = messageTemplate.content.firstElementChild.cloneNode(true); node.classList.add(message.role); const avatar = node.querySelector(".msg-avatar"); avatar.classList.add(message.role === "user" ? "user" : "ai"); avatar.textContent = message.role === "user" ? "U" : "AI"; const bubble = node.querySelector(".bubble"); bubble.innerHTML = renderMarkdown(message.content); const actions = node.querySelector(".message-actions"); if (message.role === "assistant") { const copyBtn = document.createElement("button"); copyBtn.className = "msg-action-btn"; copyBtn.innerHTML = ` Copy`; copyBtn.addEventListener("click", async () => { await navigator.clipboard.writeText(message.content); copyBtn.textContent = "Saved"; setTimeout(() => { copyBtn.innerHTML = ` Copy`; }, 1500); }); actions.appendChild(copyBtn); } if (saveable && Number.isInteger(message.id) && state.sessionId) { const saveBtn = document.createElement("button"); saveBtn.className = "msg-action-btn"; saveBtn.innerHTML = ` Save`; saveBtn.addEventListener("click", async () => { await api(`/api/history/sessions/${state.sessionId}/messages/${message.id}/save`, { method: "POST" }); saveBtn.textContent = "Saved"; }); actions.appendChild(saveBtn); } chatMessages.appendChild(node); scrollToBottom(); } function renderMessages(messages) { chatMessages.innerHTML = ""; if (!messages.length) { ensureWelcomeState().style.display = "flex"; return; } messages.forEach(message => addMessage(message)); } function typingBubble() { const currentWelcome = document.getElementById("welcomeState"); if (currentWelcome) currentWelcome.style.display = "none"; const node = messageTemplate.content.firstElementChild.cloneNode(true); node.classList.add("assistant", "typing"); const avatar = node.querySelector(".msg-avatar"); avatar.classList.add("ai"); avatar.textContent = "AI"; const bubble = node.querySelector(".bubble"); bubble.innerHTML = `
    `; chatMessages.appendChild(node); scrollToBottom(); return node; } function renderSessions() { [sessionList, mobileSessionList].forEach(list => { if (!list) return; list.innerHTML = ""; state.sessions.forEach(session => { const item = document.createElement("button"); item.className = `session-item${session.id === state.sessionId ? " active" : ""}`; item.innerHTML = `${escapeHtml(session.title)}${new Date(session.updated_at).toLocaleDateString()} | ${session.message_count} msgs`; item.addEventListener("click", () => { loadSession(session.id); closeDrawers(); }); list.appendChild(item); }); }); } async function loadSessions() { state.sessions = await api("/api/history/sessions"); renderSessions(); } async function loadSession(sessionId) { try { const session = await api(`/api/history/sessions/${sessionId}`); state.sessionId = session.id; state.messages = session.messages || []; renderMessages(state.messages); renderSessions(); } catch (error) { if (!String(error.message).includes("Session not found")) throw error; await loadSessions(); if (state.sessions.length) { await loadSession(state.sessions[0].id); return; } state.sessionId = null; state.messages = []; renderMessages([]); } } async function createNewSession() { const session = await api("/api/history/sessions", { method: "POST" }); state.sessionId = session.id; state.messages = []; renderMessages([]); await loadSessions(); } async function refreshSummary() { const summary = await api("/api/history/summary"); summaryCards.innerHTML = `
    ${summary.pending_tasks}Pending tasks
    ${summary.active_reminders}Reminders
    ${summary.notes}Notes
    ${summary.due_reminders}Due now
    `; const setText = (id, value) => { const element = document.getElementById(id); if (element) element.textContent = value; }; setText("statTasks", summary.pending_tasks); setText("statReminders", summary.active_reminders); setText("statNotes", summary.notes); setText("statDue", summary.due_reminders); const setBadge = (id, value) => { const element = document.getElementById(id); if (!element) return; element.textContent = value; element.style.display = value > 0 ? "" : "none"; }; setBadge("taskBadge", summary.pending_tasks); setBadge("reminderBadge", summary.active_reminders); setBadge("notesBadge", summary.notes); setBadge("mobileTaskBadge", summary.pending_tasks); } function showReminderMessage(message, type = "success") { reminderFormMessage.textContent = message; reminderFormMessage.className = `form-status ${type}`; } function clearReminderMessage() { reminderFormMessage.textContent = ""; reminderFormMessage.className = "form-status hidden"; } function resetReminderForm() { state.editingReminderId = null; reminderForm.reset(); reminderModalTitle.textContent = "Create reminder"; saveReminderBtn.textContent = "Save Reminder"; clearReminderMessage(); } function populateReminderForm(reminder) { state.editingReminderId = reminder.id; reminderTitleInput.value = reminder.title || ""; reminderNoteInput.value = reminder.note || ""; reminderDateInput.value = reminder.date || ""; reminderTimeInput.value = reminder.time || ""; reminderModalTitle.textContent = "Edit reminder"; saveReminderBtn.textContent = "Update Reminder"; clearReminderMessage(); } function openReminderModal(reminder = null) { if (reminder) { populateReminderForm(reminder); } else if (!state.editingReminderId) { resetReminderForm(); } reminderModal.classList.remove("hidden"); reminderModal.setAttribute("aria-hidden", "false"); document.body.style.overflow = "hidden"; reminderTitleInput.focus(); } function closeReminderModal() { reminderModal.classList.add("hidden"); reminderModal.setAttribute("aria-hidden", "true"); document.body.style.overflow = ""; resetReminderForm(); } function formatReminderMeta(reminder) { return `${reminder.display_date} at ${reminder.display_time}`; } function renderReminderList() { if (!state.reminders.length) { reminderList.innerHTML = `
    No reminders yet. Add your first reminder above.
    `; return; } reminderList.innerHTML = state.reminders.map(reminder => `
    ${escapeHtml(reminder.title)}
    ${escapeHtml(reminder.status)}
    ${reminder.note ? `
    ${escapeHtml(reminder.note)}
    ` : ""}
    ${escapeHtml(formatReminderMeta(reminder))} ${reminder.completed ? "Completed" : "Active"}
    ${reminder.completed ? "" : ``} ${reminder.completed ? "" : ``}
    `).join(""); } async function loadReminders() { state.reminders = await api("/api/reminders"); renderReminderList(); } async function saveReminder(event) { event.preventDefault(); clearReminderMessage(); const title = reminderTitleInput.value.trim(); const note = reminderNoteInput.value.trim(); const date = reminderDateInput.value; const time = reminderTimeInput.value; if (!title) { showReminderMessage("Task title is required.", "error"); return; } if (!date || !time) { showReminderMessage("Please select both date and time.", "error"); return; } saveReminderBtn.disabled = true; saveReminderBtn.textContent = state.editingReminderId ? "Updating..." : "Saving..."; try { const payload = { title, note, date, time }; if (state.editingReminderId) { await api(`/api/reminders/${state.editingReminderId}`, { method: "PUT", body: JSON.stringify(payload), }); showReminderMessage("Reminder updated successfully", "success"); } else { await api("/api/reminders", { method: "POST", body: JSON.stringify(payload), }); showReminderMessage("Reminder added successfully", "success"); } await loadReminders(); await refreshSummary(); if (!state.editingReminderId) { reminderForm.reset(); } else { state.editingReminderId = null; reminderModalTitle.textContent = "Create reminder"; saveReminderBtn.textContent = "Save Reminder"; } } catch (error) { showReminderMessage(error.message, "error"); } finally { saveReminderBtn.disabled = false; saveReminderBtn.textContent = state.editingReminderId ? "Update Reminder" : "Save Reminder"; } } async function handleReminderAction(event) { const actionButton = event.target.closest("[data-action]"); if (!actionButton) return; const reminderNode = actionButton.closest("[data-reminder-id]"); if (!reminderNode) return; const reminderId = Number(reminderNode.dataset.reminderId); const reminder = state.reminders.find(item => item.id === reminderId); if (!reminder) return; try { if (actionButton.dataset.action === "edit") { openReminderModal(reminder); return; } if (actionButton.dataset.action === "done") { await api(`/api/reminders/${reminderId}/complete`, { method: "PATCH" }); showReminderMessage("Reminder marked as done", "success"); } else if (actionButton.dataset.action === "delete") { await api(`/api/reminders/${reminderId}`, { method: "DELETE" }); showReminderMessage("Reminder deleted successfully", "success"); } else if (actionButton.dataset.action === "postpone") { await api(`/api/reminders/${reminderId}/postpone`, { method: "PATCH" }); showReminderMessage("Reminder postponed by 1 day", "success"); } await loadReminders(); await refreshSummary(); } catch (error) { showReminderMessage(error.message, "error"); } } async function submitMessage(event) { event.preventDefault(); const text = messageInput.value.trim(); if (!text) return; if (!state.sessionId) await createNewSession(); addMessage({ role: "user", content: text }, false); messageInput.value = ""; autoResize(); const loader = typingBubble(); try { const response = await api("/api/chat", { method: "POST", body: JSON.stringify({ session_id: state.sessionId, message: text }), }); loader.remove(); state.sessionId = response.session_id; await loadSession(state.sessionId); await loadSessions(); await loadReminders(); await refreshSummary(); } catch (error) { loader.remove(); addMessage({ role: "assistant", content: `[!] ${error.message}` }, false); } } async function clearCurrentChat() { if (!state.sessionId) { renderMessages([]); return; } await api(`/api/history/sessions/${state.sessionId}`, { method: "DELETE" }); state.sessionId = null; state.messages = []; renderMessages([]); await loadSessions(); await refreshSummary(); } async function searchChats() { const query = searchInput.value.trim(); if (!query) { searchResults.style.display = "none"; searchResults.innerHTML = ""; return; } const results = await api(`/api/history/search?query=${encodeURIComponent(query)}`); searchResults.style.display = "block"; searchResults.innerHTML = results.length ? results.map(item => ` `).join("") : `
    No matches found.
    `; searchResults.querySelectorAll("[data-session-id]").forEach(button => { button.addEventListener("click", async () => { await loadSession(button.dataset.sessionId); searchResults.style.display = "none"; }); }); if (sideHelperContent) { sideHelperContent.textContent = results.length ? results.map(item => `${item.session_title}: ${item.content}`).join("\n\n") : "No matching chat history found."; } } document.addEventListener("click", event => { if (!searchInput.contains(event.target) && !searchResults.contains(event.target)) { searchResults.style.display = "none"; } }); async function showSavedMessages() { const saved = await api("/api/history/saved"); if (!saved.length) { setHelperContent("Saved messages", "No saved messages yet."); return; } const content = saved.slice(0, 12).map(item => `[${item.role}] ${item.content}`).join("\n\n"); setHelperContent("Saved messages", content); } function showCommands() { const content = [ "add task complete DSA homework", "show my tasks", "complete task 2", "delete task 2", "remind me tomorrow 7 PM to revise CN", "show reminders", "add note remember to apply for internship", "save this as note", "show notes", "search chats internship", "daily summary", ].join("\n"); setHelperContent("Example commands", content); } function bindPromptButtons() { document.querySelectorAll("[data-prompt]").forEach(button => { if (button.dataset.promptBound === "true") return; button.dataset.promptBound = "true"; button.addEventListener("click", () => { const prompt = button.dataset.prompt || ""; const sendSetting = button.dataset.send; const autoSend = sendSetting === "true" || (!prompt.endsWith(" ") && sendSetting !== "false"); placePrompt(prompt, autoSend); closeDrawers(); }); }); } document.addEventListener("keydown", event => { if (event.key === "Escape") { if (!reminderModal.classList.contains("hidden")) { closeReminderModal(); return; } closeDrawers(); } }); messageInput.addEventListener("input", autoResize); chatForm.addEventListener("submit", submitMessage); reminderForm.addEventListener("submit", saveReminder); reminderList.addEventListener("click", handleReminderAction); document.getElementById("newChatBtn")?.addEventListener("click", createNewSession); document.getElementById("mobileNewChatBtn")?.addEventListener("click", async () => { await createNewSession(); closeDrawers(); }); document.getElementById("clearChatBtn")?.addEventListener("click", clearCurrentChat); document.getElementById("refreshSummaryBtn")?.addEventListener("click", event => { event.stopPropagation(); refreshSummary(); }); document.getElementById("showSavedBtn")?.addEventListener("click", showSavedMessages); document.getElementById("showCommandsBtn")?.addEventListener("click", showCommands); document.getElementById("openReminderModalBtn")?.addEventListener("click", async () => { await loadReminders(); openReminderModal(); }); document.getElementById("closeReminderModalBtn")?.addEventListener("click", closeReminderModal); document.getElementById("cancelReminderBtn")?.addEventListener("click", closeReminderModal); reminderModal.querySelectorAll("[data-reminder-close]").forEach(node => { node.addEventListener("click", closeReminderModal); }); searchInput.addEventListener("input", () => { if (!searchInput.value.trim()) { searchResults.style.display = "none"; searchResults.innerHTML = ""; return; } searchChats(); }); searchInput.addEventListener("keydown", event => { if (event.key === "Enter") { event.preventDefault(); searchChats(); } }); async function bootstrap() { bindPromptButtons(); ensureWelcomeState(); autoResize(); await resolveApiBase(); await loadSessions(); await loadReminders(); await refreshSummary(); if (state.sessions.length) { await loadSession(state.sessions[0].id); } } bootstrap().catch(error => { setHelperContent("Startup issue", error.message); ensureWelcomeState().style.display = "flex"; });