PRC_BOT / frontend /script.js
pranit144's picture
Upload 55 files
7ddf739 verified
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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;");
}
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";
});