| 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 = ` |
| <div class="welcome-icon">AI</div> |
| <div> |
| <div class="welcome-title">Your AI Workspace</div> |
| <div class="welcome-sub">Manage tasks, set reminders, capture notes, and search past chats from one clean workspace.</div> |
| </div> |
| <div class="quick-prompts"> |
| <button class="quick-prompt-card" data-prompt="show my tasks" data-send="true"><strong>View Tasks</strong><span>See every pending task</span></button> |
| <button class="quick-prompt-card" data-prompt="show reminders" data-send="true"><strong>Reminders</strong><span>Check upcoming alerts</span></button> |
| <button class="quick-prompt-card" data-prompt="daily summary" data-send="true"><strong>Daily Summary</strong><span>Get a full workspace overview</span></button> |
| <button class="quick-prompt-card" data-prompt="add task " data-send="false"><strong>Add Task</strong><span>Create a new to-do</span></button> |
| </div> |
| `; |
|
|
| 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, "<h3>$1</h3>"); |
| html = html.replace(/^## (.*)$/gm, "<h2>$1</h2>"); |
| html = html.replace(/^# (.*)$/gm, "<h1>$1</h1>"); |
| html = html.replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>"); |
| html = html.replace(/\*(.+?)\*/g, "<em>$1</em>"); |
| html = html.replace(/`([^`]+)`/g, "<code>$1</code>"); |
| html = html.replace(/^- (.*)$/gm, "<li>$1</li>"); |
| html = html.replace(/(<li>.*<\/li>)/gs, "<ul>$1</ul>"); |
| html = html.replace(/\n/g, "<br>"); |
| 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) { |
| |
| } |
| } |
|
|
| 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 = `<strong style="color:var(--text-accent);font-size:12px;">${escapeHtml(title)}</strong><pre style="margin-top:6px;">${escapeHtml(content)}</pre>`; |
| 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 = `<svg width="11" height="11" viewBox="0 0 20 20" fill="currentColor"><path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z"/><path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z"/></svg> Copy`; |
| copyBtn.addEventListener("click", async () => { |
| await navigator.clipboard.writeText(message.content); |
| copyBtn.textContent = "Saved"; |
| setTimeout(() => { |
| copyBtn.innerHTML = `<svg width="11" height="11" viewBox="0 0 20 20" fill="currentColor"><path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z"/><path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z"/></svg> 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 = `<svg width="11" height="11" viewBox="0 0 20 20" fill="currentColor"><path d="M5 4a2 2 0 012-2h6a2 2 0 012 2v14l-5-2.5L5 18V4z"/></svg> 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 = `<div class="typing-dots"><div class="typing-dot"></div><div class="typing-dot"></div><div class="typing-dot"></div></div>`; |
|
|
| 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 = `<strong>${escapeHtml(session.title)}</strong><small>${new Date(session.updated_at).toLocaleDateString()} | ${session.message_count} msgs</small>`; |
| 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 = ` |
| <div class="summary-card"><strong>${summary.pending_tasks}</strong><small>Pending tasks</small></div> |
| <div class="summary-card"><strong>${summary.active_reminders}</strong><small>Reminders</small></div> |
| <div class="summary-card"><strong>${summary.notes}</strong><small>Notes</small></div> |
| <div class="summary-card"><strong>${summary.due_reminders}</strong><small>Due now</small></div> |
| `; |
|
|
| 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 = `<div class="reminder-empty">No reminders yet. Add your first reminder above.</div>`; |
| return; |
| } |
|
|
| reminderList.innerHTML = state.reminders.map(reminder => ` |
| <article class="reminder-item" data-reminder-id="${reminder.id}"> |
| <div class="reminder-main"> |
| <div class="reminder-topline"> |
| <div class="reminder-title">${escapeHtml(reminder.title)}</div> |
| <span class="status-pill ${escapeHtml(reminder.status)}">${escapeHtml(reminder.status)}</span> |
| </div> |
| ${reminder.note ? `<div class="reminder-note">${escapeHtml(reminder.note)}</div>` : ""} |
| <div class="reminder-meta"> |
| <span>${escapeHtml(formatReminderMeta(reminder))}</span> |
| <span>${reminder.completed ? "Completed" : "Active"}</span> |
| </div> |
| </div> |
| <div class="reminder-actions"> |
| ${reminder.completed ? "" : `<button type="button" class="action-chip" data-action="done">Mark as Done</button>`} |
| <button type="button" class="action-chip" data-action="edit">Edit</button> |
| ${reminder.completed ? "" : `<button type="button" class="action-chip" data-action="postpone">Postpone</button>`} |
| <button type="button" class="action-chip danger" data-action="delete">Delete</button> |
| </div> |
| </article> |
| `).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 => ` |
| <button class="search-item" type="button" data-session-id="${escapeHtml(item.session_id)}"> |
| <strong>${escapeHtml(item.session_title)}</strong> |
| <small>${escapeHtml(item.role)}</small> |
| <p>${escapeHtml(item.content.slice(0, 120))}</p> |
| </button>`).join("") |
| : `<div class="search-item"><small>No matches found.</small></div>`; |
|
|
| 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"; |
| }); |
|
|