Spaces:
Sleeping
Sleeping
Complete restructuring for hackathon validator compliance with server package, uv.lock, and openenv-core
1e1ca31 | <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Email Triage OpenEnv - Interactive Dashboard</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| } | |
| header { | |
| text-align: center; | |
| color: white; | |
| margin-bottom: 30px; | |
| } | |
| header h1 { | |
| font-size: 2.5em; | |
| margin-bottom: 10px; | |
| text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); | |
| } | |
| header p { | |
| font-size: 1.1em; | |
| opacity: 0.9; | |
| } | |
| .main-layout { | |
| display: grid; | |
| grid-template-columns: 1fr 2fr 1fr; | |
| gap: 20px; | |
| margin-bottom: 20px; | |
| } | |
| .panel { | |
| background: white; | |
| border-radius: 12px; | |
| padding: 20px; | |
| box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .panel h2 { | |
| color: #333; | |
| font-size: 1.3em; | |
| margin-bottom: 15px; | |
| padding-bottom: 10px; | |
| border-bottom: 2px solid #667eea; | |
| } | |
| .panel h3 { | |
| color: #555; | |
| font-size: 1em; | |
| margin-top: 15px; | |
| margin-bottom: 10px; | |
| } | |
| .task-selector { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .task-btn { | |
| padding: 12px; | |
| border: 2px solid #ddd; | |
| border-radius: 8px; | |
| background: white; | |
| cursor: pointer; | |
| font-size: 0.95em; | |
| transition: all 0.3s; | |
| text-align: left; | |
| } | |
| .task-btn:hover { | |
| border-color: #667eea; | |
| background: #f0f4ff; | |
| } | |
| .task-btn.active { | |
| background: #667eea; | |
| color: white; | |
| border-color: #667eea; | |
| } | |
| .task-btn-title { | |
| font-weight: bold; | |
| display: block; | |
| margin-bottom: 5px; | |
| } | |
| .task-btn-desc { | |
| font-size: 0.85em; | |
| opacity: 0.8; | |
| } | |
| .email-display { | |
| background: #f9f9f9; | |
| border-left: 4px solid #667eea; | |
| padding: 15px; | |
| border-radius: 8px; | |
| margin-bottom: 15px; | |
| min-height: 200px; | |
| } | |
| .email-header { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 15px; | |
| margin-bottom: 15px; | |
| font-size: 0.9em; | |
| } | |
| .email-field { | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| .email-label { | |
| color: #666; | |
| font-weight: bold; | |
| font-size: 0.85em; | |
| margin-bottom: 5px; | |
| } | |
| .email-value { | |
| color: #333; | |
| padding: 8px; | |
| background: white; | |
| border-radius: 4px; | |
| } | |
| .email-subject { | |
| font-weight: bold; | |
| margin-bottom: 10px; | |
| color: #333; | |
| } | |
| .email-body { | |
| background: white; | |
| padding: 12px; | |
| border-radius: 4px; | |
| line-height: 1.5; | |
| color: #555; | |
| max-height: 300px; | |
| overflow-y: auto; | |
| } | |
| .form-group { | |
| margin-bottom: 15px; | |
| } | |
| .form-group label { | |
| display: block; | |
| margin-bottom: 8px; | |
| color: #333; | |
| font-weight: bold; | |
| font-size: 0.95em; | |
| } | |
| select, | |
| input { | |
| width: 100%; | |
| padding: 10px; | |
| border: 1px solid #ddd; | |
| border-radius: 6px; | |
| font-size: 0.95em; | |
| font-family: inherit; | |
| } | |
| select:focus, | |
| input:focus { | |
| outline: none; | |
| border-color: #667eea; | |
| box-shadow: 0 0 5px rgba(102, 126, 234, 0.3); | |
| } | |
| .button-group { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 10px; | |
| margin-top: 15px; | |
| } | |
| button { | |
| padding: 12px; | |
| border: none; | |
| border-radius: 6px; | |
| font-size: 0.95em; | |
| font-weight: bold; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| } | |
| .btn-primary { | |
| background: #667eea; | |
| color: white; | |
| } | |
| .btn-primary:hover:not(:disabled) { | |
| background: #5568d3; | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); | |
| } | |
| .btn-primary:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| } | |
| .btn-secondary { | |
| background: #f0f0f0; | |
| color: #333; | |
| border: 1px solid #ddd; | |
| } | |
| .btn-secondary:hover:not(:disabled) { | |
| background: #e0e0e0; | |
| } | |
| .btn-secondary:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .stats-panel { | |
| display: grid; | |
| grid-template-columns: 1fr; | |
| gap: 10px; | |
| } | |
| .stat-box { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 15px; | |
| border-radius: 8px; | |
| text-align: center; | |
| } | |
| .stat-label { | |
| font-size: 0.9em; | |
| opacity: 0.9; | |
| margin-bottom: 5px; | |
| } | |
| .stat-value { | |
| font-size: 1.8em; | |
| font-weight: bold; | |
| } | |
| .results-area { | |
| background: #f0f8ff; | |
| border: 2px solid #667eea; | |
| padding: 15px; | |
| border-radius: 8px; | |
| margin-top: 15px; | |
| min-height: 100px; | |
| } | |
| .result-item { | |
| padding: 10px; | |
| margin-bottom: 8px; | |
| background: white; | |
| border-left: 4px solid #667eea; | |
| border-radius: 4px; | |
| } | |
| .result-label { | |
| font-weight: bold; | |
| color: #667eea; | |
| font-size: 0.9em; | |
| } | |
| .result-value { | |
| color: #333; | |
| margin-top: 5px; | |
| } | |
| .reward-high { | |
| color: #28a745; | |
| font-weight: bold; | |
| } | |
| .reward-low { | |
| color: #dc3545; | |
| font-weight: bold; | |
| } | |
| .progress-bar { | |
| width: 100%; | |
| height: 8px; | |
| background: #e0e0e0; | |
| border-radius: 4px; | |
| overflow: hidden; | |
| margin-top: 10px; | |
| } | |
| .progress-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, #667eea, #764ba2); | |
| transition: width 0.3s; | |
| } | |
| .status-message { | |
| padding: 12px; | |
| border-radius: 6px; | |
| margin-bottom: 15px; | |
| font-size: 0.95em; | |
| } | |
| .status-info { | |
| background: #e7f3ff; | |
| color: #00396b; | |
| border: 1px solid #667eea; | |
| } | |
| .status-success { | |
| background: #d4edda; | |
| color: #155724; | |
| border: 1px solid #28a745; | |
| } | |
| .status-error { | |
| background: #f8d7da; | |
| color: #721c24; | |
| border: 1px solid #dc3545; | |
| } | |
| .status-warning { | |
| background: #fff3cd; | |
| color: #856404; | |
| border: 1px solid #ffc107; | |
| } | |
| .history-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin-top: 10px; | |
| font-size: 0.85em; | |
| } | |
| .history-table th { | |
| background: #667eea; | |
| color: white; | |
| padding: 8px; | |
| text-align: left; | |
| } | |
| .history-table td { | |
| padding: 8px; | |
| border-bottom: 1px solid #ddd; | |
| } | |
| .history-table tr:hover { | |
| background: #f5f5f5; | |
| } | |
| .task-complete { | |
| background: linear-gradient(135deg, #28a745 0%, #20c997 100%); | |
| color: white; | |
| padding: 20px; | |
| border-radius: 8px; | |
| text-align: center; | |
| } | |
| .task-complete h3 { | |
| font-size: 1.5em; | |
| margin-bottom: 10px; | |
| color: white; | |
| } | |
| .idle-message { | |
| text-align: center; | |
| padding: 30px; | |
| color: #999; | |
| } | |
| @media (max-width: 1200px) { | |
| .main-layout { | |
| grid-template-columns: 1fr; | |
| } | |
| header h1 { | |
| font-size: 1.8em; | |
| } | |
| } | |
| .loader { | |
| display: inline-block; | |
| width: 20px; | |
| height: 20px; | |
| border: 3px solid #f3f3f3; | |
| border-top: 3px solid #667eea; | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| margin-right: 10px; | |
| } | |
| @keyframes spin { | |
| 0% { | |
| transform: rotate(0deg); | |
| } | |
| 100% { | |
| transform: rotate(360deg); | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1>📧 Email Triage OpenEnv</h1> | |
| <p>Interactive Dashboard - Test & Evaluate Email Classification Tasks</p> | |
| </header> | |
| <div id="statusMessage"></div> | |
| <div class="main-layout"> | |
| <!-- Left Panel: Task Selection & Controls --> | |
| <div class="panel"> | |
| <h2>📋 Tasks</h2> | |
| <div class="task-selector"> | |
| <button class="task-btn active" onclick="selectTask('spam_detection')"> | |
| <span class="task-btn-title">Task 1: Spam Detection</span> | |
| <span class="task-btn-desc">Easy - 10 emails</span> | |
| </button> | |
| <button class="task-btn" onclick="selectTask('multi_class_routing')"> | |
| <span class="task-btn-title">Task 2: Multi-Class Routing</span> | |
| <span class="task-btn-desc">Medium - 12 emails</span> | |
| </button> | |
| <button class="task-btn" onclick="selectTask('context_aware_triage')"> | |
| <span class="task-btn-title">Task 3: Context-Aware Triage</span> | |
| <span class="task-btn-desc">Hard - 20 emails</span> | |
| </button> | |
| </div> | |
| <h3 style="margin-top: 25px;">⚙️ Controls</h3> | |
| <button class="btn-primary" style="width: 100%; margin-bottom: 10px;" onclick="resetTask()"> | |
| 🔄 Reset Task | |
| </button> | |
| <button class="btn-secondary" style="width: 100%;" onclick="loadTaskInfo()"> | |
| ℹ️ Task Info | |
| </button> | |
| <h3 style="margin-top: 25px;">📊 Statistics</h3> | |
| <div class="stats-panel"> | |
| <div class="stat-box"> | |
| <div class="stat-label">Current Step</div> | |
| <div class="stat-value" id="statStep">0</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-label">Total Reward</div> | |
| <div class="stat-value" id="statReward">0.00</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-label">Average Reward</div> | |
| <div class="stat-value" id="statAvg">0.00</div> | |
| </div> | |
| <div class="stat-box"> | |
| <div class="stat-label">Final Score</div> | |
| <div class="stat-value" id="statScore">-</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Middle Panel: Email Display & Classification Form --> | |
| <div class="panel"> | |
| <h2>✉️ Email Classification</h2> | |
| <div id="emailContainer"> | |
| <div class="idle-message"> | |
| <p>Click "Reset Task" to start</p> | |
| </div> | |
| </div> | |
| <div id="formContainer" style="display: none;"> | |
| <div class="form-group"> | |
| <label for="classification">📌 Classification:</label> | |
| <select id="classification" onchange="updatePriorities()"> | |
| <option value="">-- Select Classification --</option> | |
| <option value="spam">🚫 Spam</option> | |
| <option value="normal">📄 Normal</option> | |
| <option value="urgent">⚡ Urgent</option> | |
| <option value="billing">💳 Billing</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="team">🏢 Route to Team:</label> | |
| <select id="team"> | |
| <option value="none">🚫 None</option> | |
| <option value="support">🆘 Support</option> | |
| <option value="sales">💼 Sales</option> | |
| <option value="billing">💰 Billing</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label for="priority">⭐ Priority (0-3):</label> | |
| <select id="priority"> | |
| <option value="0">0 - Low</option> | |
| <option value="1" selected>1 - Medium</option> | |
| <option value="2">2 - High</option> | |
| <option value="3">3 - Critical</option> | |
| </select> | |
| </div> | |
| <div class="button-group"> | |
| <button class="btn-primary" onclick="submitAction()">✓ Submit</button> | |
| <button class="btn-secondary" onclick="resetTask()">⟲ Reset</button> | |
| </div> | |
| <div id="resultArea" class="results-area" style="display: none;"> | |
| <div id="resultContent"></div> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" id="progressFill"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Right Panel: History & Results --> | |
| <div class="panel"> | |
| <h2>📝 History & Results</h2> | |
| <div id="historyContainer"> | |
| <div class="idle-message"> | |
| <p>History will appear here</p> | |
| </div> | |
| </div> | |
| <div id="completeMessage" style="display: none;"> | |
| <div class="task-complete"> | |
| <h3>✓ Task Complete!</h3> | |
| <p>Final Score: <span id="finalScore">0.00</span></p> | |
| <p>Steps Taken: <span id="finalSteps">0</span></p> | |
| <p style="margin-top: 15px; font-size: 0.9em;">Click "Reset Task" to try again or select another | |
| task</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| let currentTask = 'spam_detection'; | |
| let currentState = null; | |
| let history = []; | |
| let totalReward = 0; | |
| let taskDone = false; | |
| const taskDescriptions = { | |
| 'spam_detection': { | |
| title: 'Spam Detection (Easy)', | |
| desc: 'Classify 10 emails as spam or legitimate. High accuracy expected.', | |
| steps: 10, | |
| categories: ['spam', 'normal'] | |
| }, | |
| 'multi_class_routing': { | |
| title: 'Multi-Class Routing (Medium)', | |
| desc: 'Classify 12 emails into 4 categories and route to appropriate teams.', | |
| steps: 12, | |
| categories: ['spam', 'normal', 'urgent', 'billing'] | |
| }, | |
| 'context_aware_triage': { | |
| title: 'Context-Aware Triage (Hard)', | |
| desc: 'Handle 20 emails with VIP flags, SLAs, and complex context.', | |
| steps: 20, | |
| categories: ['spam', 'normal', 'urgent', 'billing'] | |
| } | |
| }; | |
| function showStatus(message, type = 'info') { | |
| const statusEl = document.getElementById('statusMessage'); | |
| statusEl.className = `status-message status-${type}`; | |
| statusEl.innerHTML = message; | |
| statusEl.style.display = 'block'; | |
| setTimeout(() => { | |
| statusEl.style.display = 'none'; | |
| }, 5000); | |
| } | |
| async function selectTask(taskName) { | |
| currentTask = taskName; | |
| document.querySelectorAll('.task-btn').forEach(btn => btn.classList.remove('active')); | |
| event.target.closest('.task-btn').classList.add('active'); | |
| loadTaskInfo(); | |
| showStatus(`Selected: ${taskDescriptions[taskName].title}`, 'info'); | |
| } | |
| function loadTaskInfo() { | |
| const task = taskDescriptions[currentTask]; | |
| showStatus(`<strong>${task.title}</strong><br>${task.desc}`, 'info'); | |
| } | |
| async function resetTask() { | |
| try { | |
| showStatus('🔄 Resetting task...', 'info'); | |
| const response = await fetch(`/reset?task=${currentTask}`, { | |
| method: 'POST' | |
| }); | |
| const data = await response.json(); | |
| currentState = data.observation; | |
| history = []; | |
| totalReward = 0; | |
| taskDone = false; | |
| document.getElementById('statStep').textContent = '0'; | |
| document.getElementById('statReward').textContent = '0.00'; | |
| document.getElementById('statAvg').textContent = '0.00'; | |
| document.getElementById('statScore').textContent = '-'; | |
| document.getElementById('completeMessage').style.display = 'none'; | |
| document.getElementById('formContainer').style.display = 'block'; | |
| displayEmail(); | |
| updateHistory(); | |
| showStatus(`✓ Task reset! Ready to classify emails.`, 'success'); | |
| } catch (error) { | |
| showStatus(`Error resetting task: ${error.message}`, 'error'); | |
| } | |
| } | |
| function displayEmail() { | |
| const email = currentState.current_email; | |
| const emailHTML = ` | |
| <div class="email-display"> | |
| <div class="email-subject">Subject: ${email.subject}</div> | |
| <div class="email-header"> | |
| <div class="email-field"> | |
| <span class="email-label">From:</span> | |
| <span class="email-value">${email.sender_domain}</span> | |
| </div> | |
| <div class="email-field"> | |
| <span class="email-label">VIP Sender:</span> | |
| <span class="email-value">${email.is_vip_sender ? '✓ Yes' : '✗ No'}</span> | |
| </div> | |
| </div> | |
| <div class="email-header" style="margin-bottom: 15px;"> | |
| <div class="email-field"> | |
| <span class="email-label">SLA Hours:</span> | |
| <span class="email-value">${email.sla_hours || 'N/A'}</span> | |
| </div> | |
| <div class="email-field"> | |
| <span class="email-label">Timestamp:</span> | |
| <span class="email-value">${new Date(email.timestamp).toLocaleString()}</span> | |
| </div> | |
| </div> | |
| <div class="email-body">${email.body.replace(/</g, '<').replace(/>/g, '>')}</div> | |
| </div> | |
| `; | |
| document.getElementById('emailContainer').innerHTML = emailHTML; | |
| } | |
| function updatePriorities() { | |
| const classification = document.getElementById('classification').value; | |
| updateTeamOptions(); | |
| } | |
| function updateTeamOptions() { | |
| const classification = document.getElementById('classification').value; | |
| const teamSelect = document.getElementById('team'); | |
| if (classification === 'spam') { | |
| teamSelect.value = 'none'; | |
| } | |
| } | |
| async function submitAction() { | |
| const classification = document.getElementById('classification').value; | |
| const team = document.getElementById('team').value; | |
| const priority = parseInt(document.getElementById('priority').value); | |
| if (!classification) { | |
| showStatus('Please select a classification', 'warning'); | |
| return; | |
| } | |
| try { | |
| const response = await fetch(`/step?task=${currentTask}`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| classification: classification, | |
| team: team, | |
| priority: priority | |
| }) | |
| }); | |
| const data = await response.json(); | |
| currentState = data.observation; | |
| totalReward += data.reward.value; | |
| taskDone = data.done; | |
| const stepNum = history.length + 1; | |
| history.push({ | |
| step: stepNum, | |
| email_subject: currentState.current_email.subject.substring(0, 30), | |
| classification: classification, | |
| team: team, | |
| priority: priority, | |
| reward: data.reward.value, | |
| done: taskDone | |
| }); | |
| updateStats(); | |
| displayResults(data); | |
| updateHistory(); | |
| if (taskDone) { | |
| showTaskComplete(); | |
| } else { | |
| setTimeout(() => { | |
| displayEmail(); | |
| document.getElementById('classification').value = ''; | |
| document.getElementById('team').value = 'none'; | |
| document.getElementById('priority').value = '1'; | |
| document.getElementById('resultArea').style.display = 'none'; | |
| }, 1500); | |
| } | |
| showStatus(`✓ Step ${stepNum} submitted!`, 'success'); | |
| } catch (error) { | |
| showStatus(`Error: ${error.message}`, 'error'); | |
| } | |
| } | |
| function updateStats() { | |
| const stepNum = history.length; | |
| const avgReward = stepNum > 0 ? (totalReward / stepNum) : 0; | |
| document.getElementById('statStep').textContent = stepNum.toString(); | |
| document.getElementById('statReward').textContent = totalReward.toFixed(2); | |
| document.getElementById('statAvg').textContent = avgReward.toFixed(2); | |
| const progressPercent = (stepNum / taskDescriptions[currentTask].steps) * 100; | |
| document.getElementById('progressFill').style.width = progressPercent + '%'; | |
| } | |
| function displayResults(data) { | |
| const resultHTML = ` | |
| <div class="result-item"> | |
| <span class="result-label">Reward:</span> | |
| <span class="result-value ${data.reward.value >= 0.7 ? 'reward-high' : 'reward-low'}"> | |
| ${data.reward.value.toFixed(3)} | |
| </span> | |
| </div> | |
| <div class="result-item"> | |
| <span class="result-label">Breakdown:</span> | |
| <span class="result-value">${JSON.stringify(data.reward.breakdown).replace(/[{}]/g, '')}</span> | |
| </div> | |
| <div class="result-item"> | |
| <span class="result-label">Status:</span> | |
| <span class="result-value">${data.done ? '✓ Task Complete' : '▶ In Progress'}</span> | |
| </div> | |
| `; | |
| document.getElementById('resultContent').innerHTML = resultHTML; | |
| document.getElementById('resultArea').style.display = 'block'; | |
| } | |
| function updateHistory() { | |
| if (history.length === 0) { | |
| document.getElementById('historyContainer').innerHTML = '<div class="idle-message"><p>No actions yet</p></div>'; | |
| return; | |
| } | |
| const tableHTML = ` | |
| <table class="history-table"> | |
| <thead> | |
| <tr> | |
| <th>Step</th> | |
| <th>Classification</th> | |
| <th>Team</th> | |
| <th>Priority</th> | |
| <th>Reward</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| ${history.map(h => ` | |
| <tr> | |
| <td>${h.step}</td> | |
| <td>${h.classification}</td> | |
| <td>${h.team}</td> | |
| <td>${h.priority}</td> | |
| <td>${h.reward.toFixed(3)}</td> | |
| </tr> | |
| `).join('')} | |
| </tbody> | |
| </table> | |
| `; | |
| document.getElementById('historyContainer').innerHTML = tableHTML; | |
| } | |
| function showTaskComplete() { | |
| const finalScore = (totalReward / history.length).toFixed(3); | |
| document.getElementById('finalScore').textContent = finalScore; | |
| document.getElementById('finalSteps').textContent = history.length; | |
| document.getElementById('completeMessage').style.display = 'block'; | |
| document.getElementById('statScore').textContent = finalScore; | |
| document.getElementById('formContainer').style.display = 'none'; | |
| showStatus(`🎉 Task complete! Final score: ${finalScore}`, 'success'); | |
| } | |
| // Initialize on load | |
| window.addEventListener('load', () => { | |
| loadTaskInfo(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |