This repository has been archived on 2026-01-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
DevDen/script.js
2026-01-16 13:38:00 +00:00

730 lines
20 KiB
JavaScript

// 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 = `<div class="message-text">${content}</div>`;
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 = `
<div class="message-text">
<div class="typing">
<span></span>
<span></span>
<span></span>
</div>
</div>
`;
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 <br>
gfm: true, // GitHub-flavored markdown
headerIds: false, // No IDs for headers
mangle: false // Don't obfuscate emails
});
// Parse markdown to HTML
const rawHtml = marked.parse(text);
// Sanitize HTML to prevent XSS attacks (include button element for copy functionality)
const cleanHtml = DOMPurify.sanitize(rawHtml, {
ALLOWED_TAGS: ['p', 'br', 'strong', 'em', 'u', 's', 'code', 'pre',
'a', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'blockquote', 'hr', 'table', 'thead', 'tbody', 'tr', 'th', 'td', 'button'],
ALLOWED_ATTR: ['href', 'target', 'rel', 'class']
});
return cleanHtml;
} catch (error) {
logger.error('Markdown rendering failed', error);
return escapeHtml(text);
}
}
function addCopyButtonsToCodeBlocks(element) {
// Find all <pre> 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();