const APP_CONFIG = window.APP_CONFIG || {}; const API_BASE = APP_CONFIG.API_BASE_URL || "https://api.corpus.swecha.org/api/v1"; const HF_API_BASE = APP_CONFIG.HF_API_BASE_URL || "https://api-inference.huggingface.co/models"; const HF_ASR_MODEL = APP_CONFIG.HF_ASR_MODEL || "viswamaicoe/swecha-gonthuka-asr"; const HF_TOKEN = APP_CONFIG.HF_TOKEN || ""; const LOCAL_ASR_BASE = (APP_CONFIG.LOCAL_ASR_BASE_URL !== undefined && APP_CONFIG.LOCAL_ASR_BASE_URL !== null && APP_CONFIG.LOCAL_ASR_BASE_URL !== "") ? APP_CONFIG.LOCAL_ASR_BASE_URL : ""; const CORPUS_ASR_ENDPOINT = APP_CONFIG.CORPUS_ASR_ENDPOINT || ""; // Auth elements const loginPage = document.getElementById("loginPage"); const dashboard = document.getElementById("dashboard"); const phoneInput = document.getElementById("phoneInput"); const passwordInput = document.getElementById("passwordInput"); const loginBtn = document.getElementById("loginBtn"); const loginError = document.getElementById("loginError"); const logoutBtn = document.getElementById("logoutBtn"); // Dashboard elements const userSearchInput = document.getElementById("userSearchInput"); const searchBtn = document.getElementById("searchBtn"); const searchStatus = document.getElementById("searchStatus"); const userResults = document.getElementById("userResults"); const dateFilter = document.getElementById("dateFilter"); const standupStatus = document.getElementById("standupStatus"); const standupList = document.getElementById("standupList"); const result = document.getElementById("result"); let selectedUser = null; let currentStandups = []; const standupState = new Map(); // --- Auth logic --- function getAuthToken() { return localStorage.getItem("access_token") || ""; } function setAuthToken(token) { if (token) { localStorage.setItem("access_token", token); } else { localStorage.removeItem("access_token"); } } function checkAuth() { const token = getAuthToken(); if (token) { loginPage.classList.add("hidden"); dashboard.classList.remove("hidden"); } else { loginPage.classList.remove("hidden"); dashboard.classList.add("hidden"); } } async function login() { let phone = phoneInput.value.trim(); const password = passwordInput.value.trim(); if (!phone || !password) { showError(loginError, "Please enter both phone and password."); return; } // Normalise phone: if user typed a plain 10-digit number, prepend +91 if (/^\d{10}$/.test(phone)) { phone = "+91" + phone; } else if (/^91\d{10}$/.test(phone)) { // e.g. 919876543210 → +919876543210 phone = "+" + phone; } setLoading(loginBtn, true, "Logging in..."); hideError(loginError); try { const resp = await fetch(`${API_BASE}/auth/login`, { method: "POST", headers: { "Content-Type": "application/json", accept: "application/json" }, body: JSON.stringify({ phone, password }), }); if (!resp.ok) { const msg = await parseErrorResponse(resp); throw new Error(msg); } const data = await resp.json(); if (data.access_token) { setAuthToken(data.access_token); checkAuth(); } else { throw new Error("Invalid response from server."); } } catch (err) { showError(loginError, `Login failed: ${err.message}`); } finally { setLoading(loginBtn, false, "Login"); } } function logout() { setAuthToken(""); checkAuth(); } // --- API helpers --- function buildHeaders({ json = false } = {}) { const headers = { accept: "application/json" }; const token = getAuthToken(); if (token) headers.Authorization = `Bearer ${token}`; if (json) headers["Content-Type"] = "application/json"; return headers; } async function parseErrorResponse(resp) { try { const body = await resp.json(); if (typeof body?.detail === "string") return body.detail; if (Array.isArray(body?.detail)) { return body.detail.map((e) => e.msg || JSON.stringify(e)).join(", "); } if (typeof body?.message === "string") return body.message; return JSON.stringify(body); } catch (_) { return `${resp.status} ${resp.statusText}`; } } async function fetchJson(path, init = {}) { const resp = await fetch(`${API_BASE}${path}`, { ...init, headers: { ...buildHeaders({ json: init.body && typeof init.body === "string" }), ...(init.headers || {}), }, }); if (resp.status === 401) { logout(); throw new Error("Session expired. Please login again."); } if (!resp.ok) { const msg = await parseErrorResponse(resp); throw new Error(msg); } return resp.json(); } // --- UI helpers --- function setLoading(btn, isLoading, text) { btn.disabled = isLoading; btn.textContent = text; } function showError(el, msg) { el.textContent = msg; el.classList.remove("hidden"); } function hideError(el) { el.textContent = ""; el.classList.add("hidden"); } function formatDate(dateInput) { if (!dateInput) return "--"; const d = new Date(dateInput); if (Number.isNaN(d.getTime())) return String(dateInput).slice(0, 10); return d.toISOString().slice(0, 10); } function userIdFromUser(user) { return user?.id || user?.user_id || user?.uid || ""; } function usernameFromUser(user) { return user?.username || user?.user_name || user?.name || "Unknown"; } function escapeHtml(value) { return String(value ?? "") .replace(/&/g, "&") .replace(//g, ">") .replace(/\"/g, """) .replace(/'/g, "'"); } // --- Business logic --- function renderUserResults(users) { userResults.innerHTML = ""; if (!users.length) { userResults.innerHTML = '

No matching users found.

'; return; } const frag = document.createDocumentFragment(); users.forEach((user) => { const btn = document.createElement("button"); btn.className = "item-btn"; const username = usernameFromUser(user); btn.textContent = username; btn.addEventListener("click", () => onUserSelected(user)); frag.appendChild(btn); }); userResults.appendChild(frag); } function renderStandups(standups) { standupList.innerHTML = ""; if (!standups.length) { standupList.innerHTML = '

No standups found.

'; return; } const frag = document.createDocumentFragment(); standups.forEach((item) => { const card = document.createElement("article"); card.className = "standup-card"; const hasAudio = Boolean(item.audio_url); const status = standupState.get(item.id) || {}; card.innerHTML = `
Username: ${escapeHtml(item.username)}
Date: ${escapeHtml(formatDate(item.date))}
Status: ${escapeHtml(item.status || "Unknown")}
Record ID: ${escapeHtml(item.id)}
${hasAudio ? `` : '

Audio URL not available.

'} ${status.error ? `

${escapeHtml(status.error)}

` : ""} ${status.telugu ? `
Telugu:
${escapeHtml(status.telugu)}
` : ""} `; const actionBtn = card.querySelector(".transcribe-action"); actionBtn?.addEventListener("click", () => transcribeStandup(item)); frag.appendChild(card); }); standupList.appendChild(frag); } async function searchUsers() { const query = userSearchInput.value.trim(); if (!query) { searchStatus.textContent = "Enter username to search."; userResults.innerHTML = ""; return; } setLoading(searchBtn, true, "Searching..."); searchStatus.textContent = "Searching users..."; try { const users = await fetchJson(`/users/search?query=${encodeURIComponent(query)}`); const filtered = (Array.isArray(users) ? users : []).filter((u) => usernameFromUser(u).toLowerCase().includes(query.toLowerCase()) ); searchStatus.textContent = `Found ${filtered.length} matching user(s).`; renderUserResults(filtered); } catch (err) { searchStatus.textContent = `Search failed: ${err.message}`; userResults.innerHTML = ""; } finally { setLoading(searchBtn, false, "Search"); } } async function fetchAudioUrl(id) { try { const resp = await fetchJson(`/records/${id}/record-url?expires_minutes=60`); return resp?.record_url || null; } catch (_) { return null; } } async function enrichContribution(contribution, username) { const id = contribution?.id || contribution?.record_id || ""; if (!id) return null; const timestamp = contribution?.timestamp || contribution?.created_at || contribution?.date || null; const audioUrl = await fetchAudioUrl(id); return { id, username, title: contribution?.title || "", date: timestamp, status: contribution?.status || "uploaded", audio_url: audioUrl, }; } async function fetchStandupsForUser(user) { const username = usernameFromUser(user); const userId = userIdFromUser(user); const selectedDate = dateFilter.value; // e.g. "2024-03-20" if (!userId && !username) throw new Error("User ID not found"); const identifier = userId || encodeURIComponent(username); standupStatus.textContent = `Loading standups for ${username}...`; standupList.innerHTML = ""; try { const payload = await fetchJson(`/users/${identifier}/contributions/audio`); console.log('Fetched contributions payload:', payload); let rawContributions = Array.isArray(payload?.contributions) ? payload.contributions : Array.isArray(payload) ? payload : []; console.log('Extracted raw contributions count:', rawContributions.length); // Filter by selected date using the contribution's timestamp if (selectedDate) { rawContributions = rawContributions.filter((item) => { const ts = item?.timestamp || item?.created_at || item?.date || ""; return ts.startsWith(selectedDate); }); console.log('After date filter, count:', rawContributions.length); } if (rawContributions.length === 0) { standupStatus.textContent = selectedDate ? `No standups found for ${username} on ${selectedDate}.` : `No standups found for ${username}.`; renderStandups([]); return; } const limited = rawContributions.slice(0, 15); standupStatus.textContent = `Loading audio URLs for ${limited.length} standup(s)...`; const enriched = (await Promise.all( limited.map((item) => enrichContribution(item, username)) )).filter(Boolean); standupStatus.textContent = `Loaded ${enriched.length} standup(s) for ${username}${selectedDate ? " on " + selectedDate : ""}.`; currentStandups = enriched; renderStandups(currentStandups); } catch (err) { standupStatus.textContent = `Failed to load standups: ${err.message}`; renderStandups([]); } } async function onUserSelected(user) { selectedUser = user; standupState.clear(); await fetchStandupsForUser(user); } async function transcribeAudio(audioUrl) { // Send the URL to the local backend which fetches & transcribes server-side. // This avoids CORS errors that occur when the browser tries to fetch // the audio directly from the Corpus CDN. let resp; try { resp = await fetch(`${LOCAL_ASR_BASE}/transcribe-url`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ audio_url: audioUrl }), }); } catch (networkErr) { // fetch() itself threw — backend is not reachable at all throw new Error( `Cannot reach ASR backend at ${LOCAL_ASR_BASE}. Is the backend running? (${networkErr.message})` ); } // Log for debugging console.log("[ASR] response status:", resp.status, resp.statusText); console.log("[ASR] content-type:", resp.headers.get("content-type")); if (resp.status === 404) { throw new Error( `ASR endpoint not found (404). Backend may be on a different port. Check LOCAL_ASR_BASE_URL in config.js (currently: ${LOCAL_ASR_BASE})` ); } if (!resp.ok) { // Try to get a meaningful JSON detail from the backend error let detail = `${resp.status} ${resp.statusText}`; try { const body = await resp.json(); if (body?.detail) { detail = typeof body.detail === "string" ? body.detail : JSON.stringify(body.detail); } } catch (_) { /* non-JSON error body */ } throw new Error(detail); } const data = await resp.json(); return data?.text || null; } async function transcribeStandup(item) { const state = standupState.get(item.id) || {}; standupState.set(item.id, { ...state, loading: true, error: "" }); renderStandups(currentStandups); try { const telugu = await transcribeAudio(item.audio_url); if (!telugu) throw new Error("No transcription returned."); standupState.set(item.id, { loading: false, telugu, error: "", }); result.textContent = `Telugu Transcript (ID ${item.id}):\n${telugu}`; } catch (err) { standupState.set(item.id, { loading: false, error: err.message, }); result.textContent = `Transcription failed: ${err.message}`; } renderStandups(currentStandups); } // --- Listeners --- loginBtn.addEventListener("click", login); logoutBtn.addEventListener("click", logout); searchBtn.addEventListener("click", searchUsers); userSearchInput.addEventListener("keydown", (e) => { if (e.key === "Enter") searchUsers(); }); dateFilter.addEventListener('change', () => { if (selectedUser) { // Re-fetch standups for the currently selected user with new date filter fetchStandupsForUser(selectedUser); } }); // Init checkAuth();