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();