Download frontend/script.js from pranit144/PRC_BOT: direct link, hf CLI and curl.
- Browser
- Download file 26.1 kB
-
https://huggingface.co/spaces/pranit144/PRC_BOT/resolve/main/frontend/script.js
- Command line
-
hf download hf://spaces/pranit144/PRC_BOT/frontend/script.js
-
curl -L -o script.js https://huggingface.co/spaces/pranit144/PRC_BOT/resolve/main/frontend/script.js
26.1 kB
| 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) { | |
| // 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 = `<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"; | |
| }); | |