// Enhanced logging utility class Logger { constructor() { this.logs = []; this.maxLogs = 1000; } log(level, message, data = null) { const timestamp = new Date().toISOString(); const logEntry = { timestamp, level, message, data, url: window.location.href }; this.logs.push(logEntry); if (this.logs.length > this.maxLogs) { this.logs.shift(); } const consoleMethod = level === 'error' ? 'error' : level === 'warn' ? 'warn' : 'log'; console[consoleMethod](`[${timestamp}] ${level.toUpperCase()}: ${message}`, data || ''); // Save to localStorage for persistence try { localStorage.setItem('devden_logs', JSON.stringify(this.logs)); } catch (e) { console.warn('Failed to save logs to localStorage:', e); } } info(message, data = null) { this.log('info', message, data); } warn(message, data = null) { this.log('warn', message, data); } error(message, data = null) { this.log('error', message, data); } exportLogs() { const blob = new Blob([JSON.stringify(this.logs, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `devden_logs_${new Date().toISOString().split('T')[0]}.json`; a.click(); URL.revokeObjectURL(url); } clearLogs() { this.logs = []; localStorage.removeItem('devden_logs'); } } const logger = new Logger(); // Load existing logs from localStorage try { const savedLogs = localStorage.getItem('devden_logs'); if (savedLogs) { logger.logs = JSON.parse(savedLogs); logger.info('Loaded existing logs from localStorage', { count: logger.logs.length }); } } catch (e) { logger.warn('Failed to load existing logs from localStorage:', e); } const loginScreen = document.getElementById("loginScreen"); const welcomeScreen = document.getElementById("welcomeScreen"); const chatScreen = document.getElementById("chatScreen"); const chatMessages = document.getElementById("chatMessages"); const welcomeInput = document.getElementById("welcomeInput"); const chatInput = document.getElementById("chatInput"); const loginBtn = document.getElementById("loginBtn"); // API URL is same as frontend (nginx proxies /api and /auth to backend) const API_URL = window.location.origin; let isInChat = false; // Auth functions function getToken() { try { const token = localStorage.getItem("devden_token"); logger.info('Retrieved token from localStorage', { hasToken: !!token }); return token; } catch (error) { logger.error('Failed to retrieve token from localStorage', error); return null; } } function setToken(token) { try { localStorage.setItem("devden_token", token); logger.info('Token saved to localStorage'); } catch (error) { logger.error('Failed to save token to localStorage', error); } } function clearToken() { try { localStorage.removeItem("devden_token"); logger.info('Token cleared from localStorage'); } catch (error) { logger.error('Failed to clear token from localStorage', error); } } function showLoginScreen() { try { loginScreen.classList.remove("hidden"); welcomeScreen.classList.add("hidden"); chatScreen.classList.add("hidden"); logger.info('Switched to login screen'); } catch (error) { logger.error('Failed to show login screen', error); } } function showWelcomeScreen() { try { loginScreen.classList.add("hidden"); welcomeScreen.classList.remove("hidden"); chatScreen.classList.add("hidden"); welcomeInput.focus(); logger.info('Switched to welcome screen'); } catch (error) { logger.error('Failed to show welcome screen', error); } } function switchToChat() { try { loginScreen.classList.add("hidden"); welcomeScreen.classList.add("hidden"); chatScreen.classList.remove("hidden"); chatInput.focus(); isInChat = true; logger.info('Switched to chat screen'); } catch (error) { logger.error('Failed to switch to chat screen', error); } } async function checkAuth() { logger.info('Starting auth check'); const token = getToken(); if (!token) { logger.info('No token found, showing login screen'); showLoginScreen(); return; } try { logger.info('Making auth check request to /api/auth/me'); const response = await fetch(`${API_URL}/api/auth/me`, { headers: { Authorization: `Bearer ${token}` }, }); logger.info('Auth check response received', { status: response.status, statusText: response.statusText, ok: response.ok }); if (response.ok) { const userData = await response.json(); logger.info('Auth check successful', { user: userData }); showWelcomeScreen(); } else { const errorText = await response.text(); logger.warn('Auth check failed', { status: response.status, statusText: response.statusText, response: errorText }); clearToken(); showLoginScreen(); } } catch (error) { logger.error('Auth check request failed', { error: error.message, stack: error.stack }); showLoginScreen(); } } async function handleLogin() { logger.info('Login button clicked, starting login process'); try { loginBtn.disabled = true; loginBtn.textContent = "Checking auth config..."; logger.info('Disabled login button and updated text'); // Check if auth is configured logger.info('Checking auth configuration via /api/auth/status'); const statusResponse = await fetch(`${API_URL}/api/auth/status`); const statusData = await statusResponse.json(); logger.info('Auth status response', { status: statusResponse.status, configured: statusData.configured }); if (!statusData.configured) { const errorMsg = "Authentication not configured. Please set ENTRA_TENANT_ID, ENTRA_CLIENT_ID, and ENTRA_CLIENT_SECRET in your .env file."; logger.error('Auth not configured', { response: statusData }); alert(errorMsg); loginBtn.disabled = false; loginBtn.textContent = "Sign in with Microsoft"; return; } loginBtn.textContent = "Getting auth URL..."; logger.info('Auth configured, requesting login URL from /api/auth/login'); // Get auth URL and redirect const response = await fetch(`${API_URL}/api/auth/login`); const data = await response.json(); logger.info('Login URL response', { status: response.status, hasAuthUrl: !!data.auth_url, authUrl: data.auth_url ? data.auth_url.substring(0, 100) + '...' : null }); if (data.auth_url) { loginBtn.textContent = "Redirecting..."; logger.info('Redirecting to Microsoft OAuth URL'); window.location.href = data.auth_url; } else { throw new Error("No auth URL returned"); } } catch (error) { logger.error('Login process failed', { error: error.message, stack: error.stack }); alert("Login failed: " + error.message); loginBtn.disabled = false; loginBtn.textContent = "Sign in with Microsoft"; } } async function handleCallback() { logger.info('Starting OAuth callback processing', { url: window.location.href }); const params = new URLSearchParams(window.location.search); const code = params.get("code"); const sessionState = params.get("session_state"); const error = params.get("error"); const errorDescription = params.get("error_description"); logger.info('Parsed URL parameters', { hasCode: !!code, hasSessionState: !!sessionState, hasError: !!error, codeLength: code ? code.length : 0, sessionState: sessionState, error: error, errorDescription: errorDescription }); // Check for OAuth errors if (error) { logger.error('OAuth error in callback URL', { error: error, errorDescription: errorDescription }); alert(`Authentication failed: ${error} - ${errorDescription || 'Unknown error'}`); return false; } if (!code) { logger.warn('No authorization code found in callback URL'); return false; } try { logger.info('Sending authorization code to backend /api/auth/callback'); const response = await fetch(`${API_URL}/api/auth/callback`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ code }), }); logger.info('Callback response received', { status: response.status, statusText: response.statusText, ok: response.ok }); if (!response.ok) { const errorData = await response.json(); logger.error('Callback request failed', { status: response.status, error: errorData }); throw new Error(errorData.detail || "Callback failed"); } const data = await response.json(); logger.info('Callback successful', { hasToken: !!data.token, hasUser: !!data.user, user: data.user }); setToken(data.token); // Clean up URL logger.info('Cleaning up URL (removing query parameters)'); window.history.replaceState({}, "", "/"); return true; } catch (error) { logger.error('Callback processing failed', { error: error.message, stack: error.stack }); alert("Authentication failed: " + error.message); return false; } } // Chat functions function addMessage(text, type = "user") { const msg = document.createElement("div"); msg.className = `message ${type}`; // Render markdown for bot messages, escape HTML for user messages const content = type === "assistant" ? renderMarkdown(text) : escapeHtml(text); msg.innerHTML = `
`; chatMessages.appendChild(msg); // Add copy buttons to code blocks for assistant messages if (type === "assistant") { addCopyButtonsToCodeBlocks(msg); } scrollToBottom(); } function showTyping() { const typing = document.createElement("div"); typing.className = "message assistant"; typing.id = "typing"; typing.innerHTML = ` `; chatMessages.appendChild(typing); scrollToBottom(); } function hideTyping() { const typing = document.getElementById("typing"); if (typing) typing.remove(); } function scrollToBottom() { chatMessages.scrollTop = chatMessages.scrollHeight; } function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } function renderMarkdown(text) { // Check if libraries are loaded if (typeof marked === 'undefined' || typeof DOMPurify === 'undefined') { logger.error('Markdown libraries not loaded', { markedAvailable: typeof marked !== 'undefined', dompurifyAvailable: typeof DOMPurify !== 'undefined' }); return escapeHtml(text); } try { // Configure marked.js for optimal chat rendering marked.setOptions({ breaks: true, // Convert line breaks to elements in the given element
const preElements = element.querySelectorAll('pre');
preElements.forEach((pre) => {
// Skip if button already exists
if (pre.querySelector('.copy-button')) {
return;
}
// Create copy button
const button = document.createElement('button');
button.className = 'copy-button';
button.textContent = 'Copy';
// Add click handler
button.addEventListener('click', async () => {
const code = pre.querySelector('code');
const text = code ? code.textContent : pre.textContent;
try {
await navigator.clipboard.writeText(text);
button.textContent = 'Copied!';
button.classList.add('copied');
// Reset after 2 seconds
setTimeout(() => {
button.textContent = 'Copy';
button.classList.remove('copied');
}, 2000);
} catch (error) {
logger.error('Failed to copy code to clipboard', error);
button.textContent = 'Failed';
setTimeout(() => {
button.textContent = 'Copy';
}, 2000);
}
});
// Add button to pre element
pre.appendChild(button);
});
}
async function sendMessage(text) {
if (!text.trim()) return;
if (!isInChat) {
switchToChat();
}
addMessage(text, "user");
chatInput.value = "";
chatInput.disabled = true;
showTyping();
try {
const token = getToken();
const response = await fetch(`${API_URL}/api/chat/stream`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify({
message: text,
provider: null,
}),
});
if (response.status === 401) {
hideTyping();
clearToken();
showLoginScreen();
return;
}
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
hideTyping();
// Create message element for streaming
const msgDiv = document.createElement("div");
msgDiv.className = "message assistant";
const textDiv = document.createElement("div");
textDiv.className = "message-text";
msgDiv.appendChild(textDiv);
chatMessages.appendChild(msgDiv);
// Read stream
const reader = response.body.getReader();
const decoder = new TextDecoder();
let assistantMessage = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value);
const lines = chunk.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = JSON.parse(line.slice(6));
if (data.error) {
textDiv.textContent = `Error: ${data.error}`;
break;
}
if (data.chunk) {
assistantMessage += data.chunk;
textDiv.innerHTML = renderMarkdown(assistantMessage);
scrollToBottom();
}
if (data.done) {
// Add copy buttons to code blocks after streaming is complete
addCopyButtonsToCodeBlocks(msgDiv);
break;
}
}
}
}
} catch (error) {
hideTyping();
addMessage(`Error: ${error.message}`, "assistant");
} finally {
chatInput.disabled = false;
chatInput.focus();
}
}
// Event listeners
loginBtn.addEventListener("click", handleLogin);
welcomeInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
const text = welcomeInput.value.trim();
if (text) {
sendMessage(text);
}
}
});
chatInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
e.preventDefault();
sendMessage(chatInput.value);
}
});
// Initialize
async function init() {
logger.info('Application initialization started', {
userAgent: navigator.userAgent,
url: window.location.href,
timestamp: new Date().toISOString()
});
// Check for CSS loading
const styleLink = document.querySelector('link[rel="stylesheet"]');
if (styleLink) {
logger.info('CSS link found', { href: styleLink.href });
styleLink.addEventListener('load', () => {
logger.info('CSS loaded successfully');
});
styleLink.addEventListener('error', (e) => {
logger.error('CSS failed to load', { href: styleLink.href, error: e });
});
} else {
logger.error('CSS link not found in document');
}
// Check DOM elements
const elements = ['loginScreen', 'welcomeScreen', 'chatScreen', 'chatMessages', 'welcomeInput', 'chatInput', 'loginBtn'];
elements.forEach(id => {
const el = document.getElementById(id);
if (!el) {
logger.error(`Required DOM element not found: ${id}`);
} else {
logger.info(`DOM element found: ${id}`);
}
});
try {
// Check for OAuth callback first
logger.info('Checking for OAuth callback parameters');
const callbackSuccess = await handleCallback();
if (callbackSuccess) {
logger.info('OAuth callback processed successfully, showing welcome screen');
showWelcomeScreen();
} else {
logger.info('No OAuth callback or callback failed, checking existing auth');
await checkAuth();
}
} catch (error) {
logger.error('Initialization failed', {
error: error.message,
stack: error.stack
});
}
logger.info('Application initialization completed');
}
// Add global error handler
window.addEventListener('error', (event) => {
logger.error('Global JavaScript error', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
error: event.error
});
});
window.addEventListener('unhandledrejection', (event) => {
logger.error('Unhandled promise rejection', {
reason: event.reason,
promise: event.promise
});
});
// Debug panel functionality
const debugPanel = document.getElementById('debugPanel');
const exportLogsBtn = document.getElementById('exportLogsBtn');
const clearLogsBtn = document.getElementById('clearLogsBtn');
const viewBackendLogsBtn = document.getElementById('viewBackendLogsBtn');
const closeDebugBtn = document.getElementById('closeDebugBtn');
const debugOutput = document.getElementById('debugOutput');
function showDebugPanel() {
debugPanel.style.display = 'block';
logger.info('Debug panel opened');
}
function hideDebugPanel() {
debugPanel.style.display = 'none';
logger.info('Debug panel closed');
}
function updateDebugOutput(text) {
debugOutput.textContent = text;
debugOutput.scrollTop = debugOutput.scrollHeight;
}
// Event listeners for debug panel
exportLogsBtn.addEventListener('click', () => {
logger.exportLogs();
updateDebugOutput('Frontend logs exported to download');
logger.info('Logs exported via debug panel');
});
clearLogsBtn.addEventListener('click', () => {
logger.clearLogs();
updateDebugOutput('Logs cleared');
logger.info('Logs cleared via debug panel');
});
viewBackendLogsBtn.addEventListener('click', async () => {
try {
updateDebugOutput('Loading backend logs...');
const response = await fetch(`${API_URL}/logs`);
if (response.ok) {
const data = await response.json();
if (data.logs) {
updateDebugOutput(data.logs.join('\n'));
} else if (data.error) {
updateDebugOutput(`Error: ${data.error}`);
}
} else {
updateDebugOutput(`Failed to load backend logs: ${response.status}`);
}
} catch (error) {
updateDebugOutput(`Error loading backend logs: ${error.message}`);
logger.error('Failed to load backend logs', error);
}
});
closeDebugBtn.addEventListener('click', hideDebugPanel);
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
// Ctrl+Shift+L: Export logs
if (e.ctrlKey && e.shiftKey && e.key === 'L') {
e.preventDefault();
logger.exportLogs();
logger.info('Logs exported via keyboard shortcut');
}
// Ctrl+Shift+D: Toggle debug panel
if (e.ctrlKey && e.shiftKey && e.key === 'D') {
e.preventDefault();
if (debugPanel.style.display === 'none') {
showDebugPanel();
} else {
hideDebugPanel();
}
}
// Escape: Close debug panel
if (e.key === 'Escape' && debugPanel.style.display !== 'none') {
hideDebugPanel();
}
});
init();