import { marked } from 'marked'; import DOMPurify from 'dompurify'; const API_URL = '/api'; // These should ideally be environment-specific const AUTH_CONFIG = { issuer: 'https://idm.flegr.me/application/o/bot/', clientId: 'CicDk8mpSBY1SW4ofCamO3B583ttmvKTSPDQwvpb', redirectUri: window.location.origin + '/callback', authorizeEndpoint: 'https://idm.flegr.me/application/o/authorize/', }; const state = { tasks: [], selectedTaskId: null, selectedRunId: null, currentView: 'dashboard', // 'dashboard' or 'task' isEditing: false, isAuthenticated: false, chatMessages: [], activeDashboardTab: 'chat' // 'chat' or 'activity' }; // DOM elements const loginOverlay = document.getElementById('login-overlay'); const callbackOverlay = document.getElementById('callback-overlay'); const loginBtn = document.getElementById('login-btn'); const appEl = document.getElementById('app'); const logoutBtn = document.getElementById('logout-btn'); const taskListEl = document.getElementById('task-list'); const newTaskBtn = document.getElementById('new-task-btn'); const modalContainer = document.getElementById('modal-container'); const newTaskForm = document.getElementById('new-task-form'); const cancelTaskBtn = document.getElementById('cancel-task'); const emptyStateEl = document.getElementById('empty-state'); const taskViewEl = document.getElementById('task-view'); const logsOutputEl = document.getElementById('logs-output'); const viewGoalEl = document.getElementById('view-goal'); const viewStatusEl = document.getElementById('view-status'); const viewDateEl = document.getElementById('view-date'); const answerContainerEl = document.getElementById('answer-container'); const answerOutputEl = document.getElementById('answer-output'); const rerunBtn = document.getElementById('rerun-btn'); const runListEl = document.getElementById('run-list'); const toggleLogsBtn = document.getElementById('toggle-logs-btn'); const logsContainerEl = document.getElementById('logs-container'); const dashboardViewEl = document.getElementById('dashboard-view'); const recentRunsListEl = document.getElementById('recent-runs-list'); const logoLink = document.getElementById('logo-link'); const editTaskBtn = document.getElementById('edit-task-btn'); const modalTitle = document.getElementById('modal-title'); const submitTaskBtn = document.getElementById('submit-task-btn'); const goalTextarea = document.getElementById('goal-textarea'); const cronInput = document.getElementById('cron-input'); const schedulePresets = document.getElementById('schedule-presets'); const toggleCustomCronBtn = document.getElementById('toggle-custom-cron'); const customCronContainer = document.getElementById('custom-cron-container'); const presetBtns = document.querySelectorAll('.btn-preset'); const chatMessagesEl = document.getElementById('chat-messages'); const chatForm = document.getElementById('chat-form'); const chatInput = document.getElementById('chat-input'); const clearChatBtn = document.getElementById('clear-chat-btn'); const chatSendBtn = document.getElementById('chat-send-btn'); const sidebarEl = document.querySelector('.sidebar'); const menuToggle = document.getElementById('menu-toggle'); const sidebarOverlay = document.getElementById('sidebar-overlay'); function updateState(newState) { Object.assign(state, newState); renderApp(); } function showToast(message, type = 'info') { const container = document.getElementById('toast-container'); const toast = document.createElement('div'); toast.className = `toast ${type}`; const icons = { success: '✓', error: '✕', info: 'ℹ' }; toast.innerHTML = ` ${icons[type] || 'ℹ'} ${message} `; container.appendChild(toast); // Auto remove setTimeout(() => { toast.style.animation = 'fadeOut 0.3s forwards'; setTimeout(() => toast.remove(), 300); }, 4000); } function renderApp() { renderTaskList(); if (state.currentView === 'dashboard') { fetchRecentRuns(); renderChat(); } else if (state.selectedTaskId) { const task = state.tasks.find(t => t.id === state.selectedTaskId); if (task) { renderRunHistory(task); showTaskView(task); } } } // Wrapper for fetch to include Authorization header async function fetchWithAuth(url, options = {}) { let response = await fetch(url, { ...options, credentials: 'include' }); if (response.status === 401) { // Try to refresh token try { const success = await attemptTokenRefresh(); if (success) { // Retry original request response = await fetch(url, { ...options, credentials: 'include' }); } } catch (error) { console.error('Token refresh failed:', error); } } if (response.status === 401) { logout(); throw new Error('Session expired'); } return response; } async function attemptTokenRefresh() { try { const response = await fetch(`${API_URL}/auth/refresh`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ refresh_token: '' }) }); if (response.ok) { return true; } } catch (error) { console.error('Error during token refresh:', error); } return false; } // I'll replace the fetchTasks function and add updateState async function fetchTasks() { try { const response = await fetchWithAuth(`${API_URL}/tasks`); const newTasks = await response.json(); // Check if we should follow the latest run let newSelectedRunId = state.selectedRunId; if (state.selectedTaskId) { const currentTask = newTasks.find(t => t.id === state.selectedTaskId); if (currentTask && currentTask.runs && currentTask.runs.length > 0) { // If we don't have a selected run or the runs changed, we might want to update if (!state.selectedRunId || (state.tasks.find(t => t.id === state.selectedTaskId)?.runs?.length !== currentTask.runs.length)) { // Only auto-switch if we are "following" the latest const wasFollowingLatest = state.tasks.find(t => t.id === state.selectedTaskId)?.runs?.[0]?.id === state.selectedRunId; if (wasFollowingLatest || !state.selectedRunId) { newSelectedRunId = currentTask.runs[0].id; } } } } updateState({ tasks: newTasks, selectedRunId: newSelectedRunId }); } catch (error) { console.error('Error fetching tasks:', error); } } async function fetchRecentRuns() { try { const response = await fetchWithAuth(`${API_URL}/runs/recent`); const recentRuns = await response.json(); renderDashboard(recentRuns); } catch (error) { console.error('Error fetching recent runs:', error); } } function renderTaskList() { const sortedTasks = [...state.tasks].sort((a, b) => { return new Date(b.created_at) - new Date(a.created_at); }); taskListEl.innerHTML = sortedTasks .map((task) => { const latestRun = task.runs && task.runs.length > 0 ? task.runs[0] : null; const status = latestRun ? latestRun.status : 'pending'; const date = latestRun ? new Date(latestRun.created_at) : new Date(task.created_at); return `
  • ${task.goal}
    ${date.toLocaleDateString()} ${date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
  • `; }) .join(''); // Add event listeners document.querySelectorAll('.task-item').forEach((item) => { item.addEventListener('click', () => { selectTask(item.dataset.id); }); }); } function selectTask(id, runId = null) { state.selectedTaskId = id; state.currentView = 'task'; const task = state.tasks.find((t) => t.id === id); if (!task) return; if (runId) { state.selectedRunId = runId; } else if (task.runs && task.runs.length > 0) { // Default to latest run if not specified (index 0 is newest) state.selectedRunId = task.runs[0].id; } else { state.selectedRunId = null; } renderTaskList(); renderRunHistory(task); showTaskView(task); // Close sidebar on mobile after selection if (window.innerWidth <= 768) { closeMobileMenu(); } } function renderRunHistory(task) { runListEl.innerHTML = task.runs .map( (run, index) => `
  • Run #${task.runs.length - index} ${new Date(run.created_at).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
  • ` ) .join(''); document.querySelectorAll('.run-item').forEach((item) => { item.addEventListener('click', () => { selectTask(task.id, item.dataset.id); }); }); } function showDashboard() { state.currentView = 'dashboard'; state.selectedTaskId = null; state.selectedRunId = null; emptyStateEl.classList.add('hidden'); taskViewEl.classList.add('hidden'); dashboardViewEl.classList.remove('hidden'); renderTaskList(); fetchRecentRuns(); renderDashboardTabs(); } function renderDashboardTabs() { const tabs = document.querySelectorAll('.tab-btn'); const panels = document.querySelectorAll('.tab-panel'); tabs.forEach(tab => { if (tab.dataset.tab === state.activeDashboardTab) { tab.classList.add('active'); } else { tab.classList.remove('active'); } }); panels.forEach(panel => { if (panel.id === `${state.activeDashboardTab}-tab-panel`) { panel.classList.add('active'); } else { panel.classList.remove('active'); } }); } function renderDashboard(recentRuns) { recentRunsListEl.innerHTML = recentRuns .map( (run) => ` ${escapeHtml(run.goal)} ${run.status} ${new Date(run.created_at).toLocaleString()} ` ) .join(''); document.querySelectorAll('#recent-runs-list tr').forEach((row) => { row.addEventListener('click', () => { selectTask(row.dataset.taskId, row.dataset.runId); }); }); } function showTaskView(task) { state.currentView = 'task'; emptyStateEl.classList.add('hidden'); dashboardViewEl.classList.add('hidden'); taskViewEl.classList.remove('hidden'); const run = task.runs.find(r => r.id === state.selectedRunId) || task.runs[0]; viewGoalEl.textContent = task.goal; // Show schedule info if exists const scheduleInfo = task.cron ? `
    🕒 Scheduled: ${task.cron}
    ` : ''; const headerMain = document.querySelector('.header-main'); const existingBadge = headerMain.querySelector('.schedule-badge'); if (existingBadge) existingBadge.remove(); if (scheduleInfo) { headerMain.insertAdjacentHTML('beforeend', scheduleInfo); } if (!run) { viewStatusEl.textContent = 'No runs'; viewStatusEl.className = 'status-badge pending'; viewDateEl.textContent = '-'; logsOutputEl.innerHTML = '
    No logs available. Click "Run Task" to start the agent.
    '; answerContainerEl.classList.remove('hidden'); answerOutputEl.innerHTML = '
    Waiting for the first execution...
    '; return; } viewStatusEl.textContent = run.status; viewStatusEl.className = `status-badge ${run.status}`; viewDateEl.textContent = new Date(run.created_at).toLocaleString(undefined, { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' }); answerContainerEl.classList.remove('hidden'); if (run.answer) { const rawHtml = marked.parse(run.answer); answerOutputEl.innerHTML = DOMPurify.sanitize(rawHtml); } else { answerOutputEl.innerHTML = '
    Agent is working on the final answer...
    '; } // Check if we should auto-scroll const isAtBottom = logsOutputEl.scrollHeight - logsOutputEl.scrollTop <= logsOutputEl.clientHeight + 10; const isFirstLoad = logsOutputEl.innerHTML === ''; // Simple log format logsOutputEl.innerHTML = run.logs .split('\n') .map((line) => `
    ${escapeHtml(line)}
    `) .join(''); if (isAtBottom || isFirstLoad) { logsOutputEl.scrollTop = logsOutputEl.scrollHeight; } } function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } function renderChat() { if (!chatMessagesEl) return; if (state.chatMessages.length === 0) { chatMessagesEl.innerHTML = `

    Start a conversation with the assistant.

    `; return; } chatMessagesEl.innerHTML = state.chatMessages .map(msg => `
    ${DOMPurify.sanitize(marked.parse(msg.content || ''))}
    `) .join(''); chatMessagesEl.scrollTop = chatMessagesEl.scrollHeight; } async function sendChatMessage(text) { const userMessage = { role: 'user', content: text }; state.chatMessages.push(userMessage); renderChat(); chatInput.value = ''; chatInput.disabled = true; chatSendBtn.disabled = true; try { const response = await fetchWithAuth(`${API_URL}/chat`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ messages: state.chatMessages }) }); if (!response.ok) throw new Error('Chat API failed'); const result = await response.json(); state.chatMessages.push(result.message); renderChat(); } catch (error) { console.error('Chat error:', error); showToast('Failed to get chat response.', 'error'); state.chatMessages.push({ role: 'assistant', content: 'Sorry, I encountered an error. Please try again.' }); renderChat(); } finally { chatInput.disabled = false; chatSendBtn.disabled = false; chatInput.focus(); } } // Event Listeners rerunBtn.addEventListener('click', async () => { if (!state.selectedTaskId) return; try { const response = await fetchWithAuth(`${API_URL}/tasks/${state.selectedTaskId}/runs`, { method: 'POST', }); const updatedTask = await response.json(); const index = state.tasks.findIndex(t => t.id === updatedTask.id); if (index !== -1) { state.tasks[index] = updatedTask; } selectTask(updatedTask.id); showToast('Task rerun successfully!', 'success'); } catch (error) { console.error('Failed to rerun task:', error); showToast('Failed to rerun task.', 'error'); } }); newTaskBtn.addEventListener('click', () => { state.isEditing = false; modalTitle.textContent = 'New Agent Task'; submitTaskBtn.textContent = 'Save Task'; goalTextarea.value = ''; cronInput.value = ''; updateScheduleUI(''); modalContainer.classList.remove('hidden'); }); toggleLogsBtn.addEventListener('click', () => { logsContainerEl.classList.toggle('hidden'); const isHidden = logsContainerEl.classList.contains('hidden'); toggleLogsBtn.classList.toggle('active', !isHidden); toggleLogsBtn.innerHTML = isHidden ? ' Inspect Logs' : ' Hide Logs'; }); editTaskBtn.addEventListener('click', () => { const task = state.tasks.find((t) => t.id === state.selectedTaskId); if (!task) return; state.isEditing = true; modalTitle.textContent = 'Edit Agent Task'; submitTaskBtn.textContent = 'Save Task'; goalTextarea.value = task.goal; cronInput.value = task.cron || ''; updateScheduleUI(task.cron || ''); modalContainer.classList.remove('hidden'); }); cancelTaskBtn.addEventListener('click', () => { modalContainer.classList.add('hidden'); state.isEditing = false; }); logoLink.addEventListener('click', (e) => { e.preventDefault(); showDashboard(); }); function updateScheduleUI(cron) { let matched = false; presetBtns.forEach(btn => { if (btn.dataset.cron === (cron || '')) { btn.classList.add('active'); matched = true; } else { btn.classList.remove('active'); } }); if (matched) { customCronContainer.classList.add('hidden'); } else if (cron) { customCronContainer.classList.remove('hidden'); } else { customCronContainer.classList.add('hidden'); } } presetBtns.forEach(btn => { btn.addEventListener('click', () => { const cron = btn.dataset.cron; cronInput.value = cron; updateScheduleUI(cron); }); }); toggleCustomCronBtn.addEventListener('click', () => { customCronContainer.classList.toggle('hidden'); }); chatForm.addEventListener('submit', (e) => { e.preventDefault(); const text = chatInput.value.trim(); if (text) { sendChatMessage(text); } }); clearChatBtn.addEventListener('click', () => { state.chatMessages = []; renderChat(); }); cronInput.addEventListener('input', () => { // If user types manually, update presets active state updateScheduleUI(cronInput.value); }); newTaskForm.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(newTaskForm); const goal = formData.get('goal'); const cron = formData.get('cron') || null; modalContainer.classList.add('hidden'); newTaskForm.reset(); try { let response; if (state.isEditing) { response = await fetchWithAuth(`${API_URL}/tasks/${state.selectedTaskId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ goal, cron }), }); } else { response = await fetchWithAuth(`${API_URL}/tasks`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ goal, cron }), }); } const updatedTask = await response.json(); if (state.isEditing) { const index = state.tasks.findIndex(t => t.id === updatedTask.id); if (index !== -1) { state.tasks[index] = updatedTask; } } else { state.tasks.unshift(updatedTask); } state.isEditing = false; selectTask(updatedTask.id); renderTaskList(); showToast(state.isEditing ? 'Task updated successfully' : 'Task created successfully', 'success'); } catch (error) { console.error('Save task failed:', error); showToast('Failed to execute task. Check console.', 'error'); } }); async function checkSession() { try { const response = await fetch(`${API_URL}/auth/session`, { credentials: 'include' }); if (response.ok) { state.isAuthenticated = true; return true; } } catch (error) { console.error('Session check failed:', error); } state.isAuthenticated = false; return false; } let socket = null; let reconnectDelay = 1000; function connectWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/api/ws`; console.log('Connecting to WebSocket:', wsUrl); socket = new WebSocket(wsUrl); socket.onopen = () => { console.log('WebSocket connected'); reconnectDelay = 1000; // Initial fetch to sync state fetchTasks(); }; socket.onmessage = (event) => { try { const { type, data } = JSON.parse(event.data); console.log('WebSocket event:', type, data); switch (type) { case 'TaskCreated': state.tasks.unshift(data); renderApp(); showToast('New task created', 'success'); break; case 'TaskUpdated': case 'RunFinished': const index = state.tasks.findIndex(t => t.id === data.id); if (index !== -1) { const wasSelected = state.selectedTaskId === data.id; state.tasks[index] = data; if (wasSelected) { // Update selected run if we were following latest const wasFollowingLatest = state.selectedRunId === state.tasks[index].runs?.[1]?.id || !state.selectedRunId; if (wasFollowingLatest && data.runs && data.runs.length > 0) { state.selectedRunId = data.runs[0].id; } } } else { state.tasks.unshift(data); } renderApp(); if (type === 'RunFinished') { showToast(`Task run completed: ${data.goal}`, 'info'); } break; case 'RunStarted': const taskIndex = state.tasks.findIndex(t => t.id === data.task_id); if (taskIndex !== -1) { // We don't have the full task update here, but we can update status // For simplicity, we just trigger a fetch or wait for RunFinished // But let's at least show it's running in the UI if selected if (state.tasks[taskIndex].runs) { // Prepend a dummy run or just fetch fetchTasks(); } } showToast(`Task started: ${data.goal}`, 'info'); break; } } catch (e) { console.error('Error handling WebSocket message:', e); } }; socket.onclose = () => { console.log('WebSocket disconnected. Reconnecting...'); setTimeout(() => { reconnectDelay = Math.min(reconnectDelay * 1.5, 30000); connectWebSocket(); }, reconnectDelay); }; socket.onerror = (error) => { console.error('WebSocket error:', error); socket.close(); }; } async function showLogin() { loginOverlay.classList.remove('hidden'); appEl.classList.add('hidden'); } async function logout() { try { await fetch(`${API_URL}/auth/logout`, { method: 'POST', credentials: 'include' }); } catch (error) { console.error('Logout failed:', error); } state.isAuthenticated = false; showLogin(); } async function handleCallback() { const params = new URLSearchParams(window.location.search); const code = params.get('code'); if (!code) return; window.history.replaceState({}, document.title, "/"); callbackOverlay.classList.remove('hidden'); loginOverlay.classList.add('hidden'); try { const response = await fetch(`${API_URL}/auth/callback?code=${code}&redirect_uri=${encodeURIComponent(AUTH_CONFIG.redirectUri)}`, { credentials: 'include' }); const data = await response.json(); if (response.ok) { state.isAuthenticated = true; callbackOverlay.classList.add('hidden'); appEl.classList.remove('hidden'); initializeApp(); } else { throw new Error('No access token in response'); } } catch (error) { console.error('Callback failed:', error); showToast('Authentication failed.', 'error'); showLogin(); } } loginBtn.addEventListener('click', () => { const authUrl = `${AUTH_CONFIG.authorizeEndpoint}?client_id=${AUTH_CONFIG.clientId}&response_type=code&redirect_uri=${encodeURIComponent(AUTH_CONFIG.redirectUri)}&scope=openid profile email offline_access`; window.location.href = authUrl; }); logoutBtn.addEventListener('click', () => { logout(); }); document.querySelectorAll('.tab-btn').forEach(btn => { btn.addEventListener('click', () => { state.activeDashboardTab = btn.dataset.tab; renderDashboardTabs(); }); }); function toggleMobileMenu() { sidebarEl.classList.toggle('open'); menuToggle.classList.toggle('active'); sidebarOverlay.classList.toggle('hidden'); document.body.style.overflow = sidebarEl.classList.contains('open') ? 'hidden' : ''; } function closeMobileMenu() { sidebarEl.classList.remove('open'); menuToggle.classList.remove('active'); sidebarOverlay.classList.add('hidden'); document.body.style.overflow = ''; } menuToggle.addEventListener('click', toggleMobileMenu); sidebarOverlay.addEventListener('click', closeMobileMenu); async function initializeApp() { const hasSession = await checkSession(); if (hasSession) { appEl.classList.remove('hidden'); loginOverlay.classList.add('hidden'); await fetchTasks(); connectWebSocket(); } else { showLogin(); } } // Check for callback on load if (window.location.pathname === '/callback' || window.location.search.includes('code=')) { handleCallback(); } else { initializeApp(); }