| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8" /> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
| <title>{{ annotation_task_name }} - Admin Dashboard</title> |
| <link |
| rel="stylesheet" |
| href="{{ url_for('static', filename='styles.css') }}" |
| /> |
| <link |
| rel="stylesheet" |
| href="{{ url_for('static', filename='admin.css') }}" |
| /> |
| </head> |
| <body> |
| <div class="admin-container"> |
| |
| <header class="admin-header"> |
| <h1>📊 {{ annotation_task_name }} - Admin Dashboard</h1> |
| <div class="header-actions"> |
| <span class="status-badge status-active">Admin Mode</span> |
| <button |
| onclick="logout()" |
| class="shadcn-button shadcn-button-outline" |
| > |
| Logout |
| </button> |
| </div> |
| </header> |
|
|
| |
| <main class="admin-main"> |
| |
| <div class="admin-tabs"> |
| <div class="admin-tab active" data-tab="overview">Overview</div> |
| <div class="admin-tab" data-tab="annotators">Annotators</div> |
| <div class="admin-tab" data-tab="instances">Instances</div> |
| <div class="admin-tab" data-tab="questions">Questions</div> |
| <div class="admin-tab" data-tab="behavioral">Behavioral</div> |
| <div class="admin-tab" data-tab="crowdsourcing">Crowdsourcing</div> |
| {% if bws_enabled %} |
| <div class="admin-tab" data-tab="bws-scoring">BWS Scoring</div> |
| {% endif %} |
| {% if mace_enabled %} |
| <div class="admin-tab" data-tab="mace">MACE</div> |
| {% endif %} |
| {% if embedding_viz_enabled %} |
| <div class="admin-tab" data-tab="embedding-viz">Embeddings</div> |
| {% endif %} |
| <div class="admin-tab" data-tab="config">Configuration</div> |
| </div> |
|
|
| |
| <div class="admin-tab-content active" id="overview"> |
| <div class="dashboard-grid" id="overviewGrid"> |
| |
| </div> |
|
|
| <div class="config-form"> |
| <h3>System Information</h3> |
| <div id="systemInfo"> |
| |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="admin-tab-content" id="annotators"> |
| <div class="table-controls"> |
| <button |
| onclick="refreshAnnotators()" |
| class="shadcn-button shadcn-button-primary" |
| > |
| Refresh Data |
| </button> |
| </div> |
|
|
| <div class="admin-table-container"> |
| <table class="admin-table" id="annotatorsTable"> |
| <thead> |
| <tr> |
| <th>User ID</th> |
| <th>Phase</th> |
| <th>Annotations</th> |
| <th>Working Time</th> |
| <th>Avg Time/Annotation</th> |
| <th>Speed (per hour)</th> |
| <th>Completion %</th> |
| <th>Max Instances</th> |
| <th>Last Activity</th> |
| </tr> |
| </thead> |
| <tbody id="annotatorsTableBody"> |
| |
| </tbody> |
| </table> |
| </div> |
|
|
| <div class="pagination" id="annotatorsPagination"> |
| |
| </div> |
| </div> |
|
|
| |
| <div class="admin-tab-content" id="instances"> |
| <div class="table-controls"> |
| <select id="pageSize" onchange="loadInstances()"> |
| <option value="25">25 per page</option> |
| <option value="50">50 per page</option> |
| <option value="100">100 per page</option> |
| </select> |
|
|
| <select id="sortBy" onchange="loadInstances()"> |
| <option value="annotation_count">Sort by Annotations</option> |
| <option value="completion_percentage">Sort by Completion</option> |
| <option value="disagreement">Sort by Disagreement</option> |
| <option value="id">Sort by ID</option> |
| <option value="average_time">Sort by Avg Time</option> |
| </select> |
|
|
| <select id="sortOrder" onchange="loadInstances()"> |
| <option value="desc">Descending</option> |
| <option value="asc">Ascending</option> |
| </select> |
|
|
| <select id="filterCompletion" onchange="loadInstances()"> |
| <option value="">All Instances</option> |
| <option value="completed">Completed Only</option> |
| <option value="incomplete">Incomplete Only</option> |
| </select> |
|
|
| <button |
| onclick="refreshInstances()" |
| class="shadcn-button shadcn-button-primary" |
| > |
| Refresh |
| </button> |
| </div> |
|
|
| <div class="admin-table-container"> |
| <table class="admin-table" id="instancesTable"> |
| <thead> |
| <tr> |
| <th>Instance ID</th> |
| <th>Text Preview</th> |
| <th>Annotations</th> |
| <th>Completion %</th> |
| <th>Most Frequent Label</th> |
| <th>Disagreement</th> |
| <th>Avg Time</th> |
| <th>Annotators</th> |
| <th>Num AI Used</th> |
| </tr> |
| </thead> |
| <tbody id="instancesTableBody"> |
| |
| </tbody> |
| </table> |
| </div> |
|
|
| <div class="pagination" id="instancesPagination"> |
| |
| </div> |
| </div> |
|
|
| |
| <div class="admin-tab-content" id="questions"> |
| <div class="table-controls"> |
| <button |
| onclick="refreshQuestions()" |
| class="shadcn-button shadcn-button-primary" |
| > |
| Refresh Data |
| </button> |
| </div> |
|
|
| <div id="questionsContainer"> |
| |
| </div> |
| </div> |
|
|
| |
| <div class="admin-tab-content" id="behavioral"> |
| <div class="table-controls"> |
| <button |
| onclick="refreshBehavioral()" |
| class="shadcn-button shadcn-button-primary" |
| > |
| Refresh Data |
| </button> |
| </div> |
|
|
| |
| <div class="dashboard-grid" id="behavioralGrid"> |
| |
| </div> |
|
|
| |
| <div class="question-card" id="aiUsageSection" style="margin-bottom: 1.5rem;"> |
| <div class="question-header"> |
| <h3 class="question-title">AI Assistance Usage</h3> |
| </div> |
| <div id="aiUsageContent"> |
| |
| </div> |
| </div> |
|
|
| |
| <div class="question-card" id="qualitySection" style="margin-bottom: 1.5rem;"> |
| <div class="question-header"> |
| <h3 class="question-title">Quality Indicators</h3> |
| </div> |
| <div id="qualityContent"> |
| |
| </div> |
| </div> |
|
|
| |
| <div class="admin-table-container"> |
| <table class="admin-table" id="behavioralTable"> |
| <thead> |
| <tr> |
| <th>User ID</th> |
| <th>Instances</th> |
| <th>Avg Time (s)</th> |
| <th>Interactions</th> |
| <th>Changes</th> |
| <th>AI Requests</th> |
| <th>AI Accept Rate</th> |
| <th>Suspicion</th> |
| </tr> |
| </thead> |
| <tbody id="behavioralTableBody"> |
| |
| </tbody> |
| </table> |
| </div> |
| </div> |
|
|
| |
| <div class="admin-tab-content" id="crowdsourcing"> |
| <div class="table-controls"> |
| <button |
| onclick="refreshCrowdsourcing()" |
| class="shadcn-button shadcn-button-primary" |
| > |
| Refresh Data |
| </button> |
| </div> |
|
|
| |
| <div class="dashboard-grid" id="crowdsourcingGrid"> |
| |
| </div> |
|
|
| |
| <div id="crowdsourcingDetails"> |
| |
| </div> |
| </div> |
|
|
| |
| {% if embedding_viz_enabled %} |
| <div class="admin-tab-content" id="embedding-viz"> |
| |
| <div class="dashboard-grid" id="embedding-viz-stats" style="margin-bottom: 1rem;"> |
| <div class="dashboard-card"> |
| <h3>Loading...</h3> |
| </div> |
| </div> |
|
|
| |
| <div style="display: flex; gap: 1rem; flex-wrap: wrap;"> |
| |
| <div style="flex: 2; min-width: 600px;"> |
| <div class="dashboard-card" style="position: relative; min-height: 500px;"> |
| <div id="embedding-viz-loader" style="display: none; position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: rgba(255,255,255,0.8); z-index: 10; justify-content: center; align-items: center;"> |
| <div class="loading-spinner" style="width: 40px; height: 40px; border: 4px solid #e5e7eb; border-top-color: #3b82f6;"></div> |
| </div> |
| <div id="embedding-viz-error" style="display: none;"></div> |
| <div id="embedding-viz-chart" style="width: 100%; height: 500px;"></div> |
| </div> |
| </div> |
|
|
| |
| <div style="flex: 1; min-width: 300px;"> |
| |
| <div class="dashboard-card" style="margin-bottom: 1rem;"> |
| <h3 style="margin: 0 0 0.5rem 0; font-size: 0.875rem; color: var(--muted-foreground);">INSTANCE PREVIEW</h3> |
| <div id="embedding-viz-preview"> |
| <p style="color: var(--muted-foreground);">Hover over a point to see details</p> |
| </div> |
| </div> |
|
|
| |
| <div class="dashboard-card"> |
| <h3 style="margin: 0 0 0.5rem 0; font-size: 0.875rem; color: var(--muted-foreground);">SELECTION QUEUE</h3> |
| <div id="embedding-viz-selection-panel"> |
| <div class="selection-empty"> |
| <p style="color: var(--muted-foreground);">Use lasso or box selection to select points</p> |
| <p style="font-size: 0.75rem; color: var(--muted-foreground);">Selected items will be prioritized for annotation</p> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="dashboard-card" style="margin-top: 1rem;"> |
| <h3 style="margin: 0 0 0.5rem 0; font-size: 0.875rem; color: var(--muted-foreground);">CONTROLS</h3> |
| <div style="display: flex; flex-direction: column; gap: 0.5rem;"> |
| <button class="shadcn-button shadcn-button-primary" onclick="embeddingViz && embeddingViz.refresh(true)" style="width: 100%;"> |
| Refresh Visualization |
| </button> |
| <select id="embedding-viz-label-source" onchange="changeEmbeddingVizLabelSource()" style="padding: 0.5rem; border: 1px solid var(--border); border-radius: var(--radius);"> |
| <option value="mace">Color by MACE Labels</option> |
| <option value="majority">Color by Majority Vote</option> |
| </select> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| {% endif %} |
|
|
| {% if bws_enabled %} |
| <div class="admin-tab-content" id="bws-scoring"> |
| <div class="dashboard-grid" id="bwsScoringGrid"> |
| <p style="color: var(--muted-foreground);">Loading BWS scoring data...</p> |
| </div> |
|
|
| <div class="dashboard-card" style="margin-bottom: 1.5rem;"> |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;"> |
| <h3 style="margin: 0;">BWS Item Scores</h3> |
| <div style="display: flex; gap: 8px; align-items: center;"> |
| <select id="bwsScoringMethod" class="shadcn-input" style="width: auto; padding: 6px 12px;"> |
| <option value="counting">Counting</option> |
| <option value="bradley_terry">Bradley-Terry</option> |
| <option value="plackett_luce">Plackett-Luce</option> |
| </select> |
| <button class="shadcn-button shadcn-button-primary" onclick="generateBwsScores()"> |
| Generate Scores |
| </button> |
| </div> |
| </div> |
| <p style="color: var(--muted-foreground); margin-bottom: 1rem;"> |
| Compute item scores from best-worst annotations. Counting is the default method (no dependencies). |
| Bradley-Terry and Plackett-Luce require the <code>choix</code> package. |
| </p> |
| <div class="admin-table-container"> |
| <table class="admin-table"> |
| <thead> |
| <tr> |
| <th>Rank</th> |
| <th>Item ID</th> |
| <th>Text</th> |
| <th>Score</th> |
| <th>Best Count</th> |
| <th>Worst Count</th> |
| <th>Appearances</th> |
| </tr> |
| </thead> |
| <tbody id="bwsScoresBody"> |
| <tr><td colspan="7" style="color: var(--muted-foreground);">Click "Generate Scores" to compute BWS scores</td></tr> |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
| {% endif %} |
|
|
| |
| {% if mace_enabled %} |
| <div class="admin-tab-content" id="mace"> |
| <div class="dashboard-grid" id="maceGrid"> |
| <p style="color: var(--muted-foreground);">Loading MACE data...</p> |
| </div> |
|
|
| <div class="dashboard-card" style="margin-bottom: 1.5rem;"> |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1rem;"> |
| <h3 style="margin: 0;">Annotator Competence</h3> |
| <button class="shadcn-button shadcn-button-primary" onclick="triggerMace()"> |
| Run MACE |
| </button> |
| </div> |
| <p style="color: var(--muted-foreground); margin-bottom: 1rem;"> |
| MACE estimates each annotator's reliability (0 = random guessing, 1 = always correct). |
| </p> |
| <div class="admin-table-container"> |
| <table class="admin-table"> |
| <thead> |
| <tr> |
| <th>Annotator</th> |
| <th>Competence</th> |
| <th>Reliability</th> |
| </tr> |
| </thead> |
| <tbody id="maceCompetenceBody"> |
| <tr><td colspan="3" style="color: var(--muted-foreground);">Click "Run MACE" to compute scores</td></tr> |
| </tbody> |
| </table> |
| </div> |
| </div> |
|
|
| <div class="dashboard-card" style="margin-bottom: 1.5rem;"> |
| <h3>Predicted Labels</h3> |
| <p style="color: var(--muted-foreground); margin-bottom: 1rem;"> |
| MACE's best estimate of the true label for each item, weighted by annotator competence. |
| </p> |
| <div id="macePredictionsContainer"> |
| <p style="color: var(--muted-foreground);">Run MACE to see predictions.</p> |
| </div> |
| </div> |
| </div> |
| {% endif %} |
|
|
| <div class="admin-tab-content" id="config"> |
| <div class="config-form"> |
| <h3>System Configuration</h3> |
| <form id="configForm"> |
| <div class="config-group"> |
| <label for="maxAnnotationsPerUser" |
| >Max Annotations per User</label |
| > |
| <input |
| type="number" |
| id="maxAnnotationsPerUser" |
| name="maxAnnotationsPerUser" |
| min="-1" |
| value="-1" |
| /> |
| <small>Use -1 for unlimited</small> |
| </div> |
|
|
| <div class="config-group"> |
| <label for="maxAnnotationsPerItem" |
| >Max Annotations per Item</label |
| > |
| <input |
| type="number" |
| id="maxAnnotationsPerItem" |
| name="maxAnnotationsPerItem" |
| min="-1" |
| value="-1" |
| /> |
| <small>Use -1 for unlimited</small> |
| </div> |
|
|
| <div class="config-group"> |
| <label for="assignmentStrategy">Assignment Strategy</label> |
| <select id="assignmentStrategy" name="assignmentStrategy"> |
| <option value="random">Random</option> |
| <option value="fixed_order">Fixed Order</option> |
| <option value="least_annotated">Least Annotated</option> |
| <option value="max_diversity">Max Diversity</option> |
| <option value="active_learning">Active Learning</option> |
| <option value="llm_confidence">LLM Confidence</option> |
| </select> |
| </div> |
|
|
| <div class="config-actions"> |
| <button |
| type="submit" |
| class="shadcn-button shadcn-button-primary" |
| > |
| Save Changes |
| </button> |
| <button |
| type="button" |
| onclick="loadConfig()" |
| class="shadcn-button shadcn-button-outline" |
| > |
| Reload |
| </button> |
| </div> |
| </form> |
| </div> |
| </div> |
| </main> |
| </div> |
|
|
| <script> |
| // Global state |
| let currentTab = "overview"; |
| let currentPage = 1; |
| let currentPageSize = 25; |
| let currentSortBy = "annotation_count"; |
| let currentSortOrder = "desc"; |
| let currentFilter = ""; |
| |
| // Initialize dashboard |
| document.addEventListener("DOMContentLoaded", function () { |
| initializeTabs(); |
| loadOverview(); |
| loadConfig(); |
| }); |
| |
| // Tab functionality |
| function initializeTabs() { |
| const tabs = document.querySelectorAll(".admin-tab"); |
| tabs.forEach((tab) => { |
| tab.addEventListener("click", function () { |
| const tabName = this.getAttribute("data-tab"); |
| switchTab(tabName); |
| }); |
| }); |
| } |
| |
| function switchTab(tabName) { |
| // Update active tab |
| document.querySelectorAll(".admin-tab").forEach((tab) => { |
| tab.classList.remove("active"); |
| }); |
| document |
| .querySelector(`[data-tab="${tabName}"]`) |
| .classList.add("active"); |
| |
| // Update active content |
| document.querySelectorAll(".admin-tab-content").forEach((content) => { |
| content.classList.remove("active"); |
| }); |
| document.getElementById(tabName).classList.add("active"); |
| |
| currentTab = tabName; |
| |
| // Load data for the tab |
| switch (tabName) { |
| case "overview": |
| loadOverview(); |
| break; |
| case "annotators": |
| loadAnnotators(); |
| break; |
| case "instances": |
| loadInstances(); |
| break; |
| case "questions": |
| loadQuestions(); |
| break; |
| case "behavioral": |
| loadBehavioral(); |
| break; |
| case "crowdsourcing": |
| loadCrowdsourcing(); |
| break; |
| case "bws-scoring": |
| loadBwsScoring(); |
| break; |
| case "mace": |
| loadMace(); |
| break; |
| case "embedding-viz": |
| loadEmbeddingVisualization(); |
| break; |
| case "config": |
| loadConfig(); |
| break; |
| } |
| } |
| |
| // API functions |
| async function makeApiRequest(endpoint, options = {}) { |
| const defaultOptions = { |
| headers: { |
| "Content-Type": "application/json", |
| "X-API-Key": "{{ admin_api_key }}", |
| }, |
| }; |
| |
| const response = await fetch(endpoint, { |
| ...defaultOptions, |
| ...options, |
| }); |
| |
| if (!response.ok) { |
| throw new Error(`API request failed: ${response.status}`); |
| } |
| |
| return response.json(); |
| } |
| |
| // Overview functions |
| async function loadOverview() { |
| try { |
| const data = await makeApiRequest("/admin/api/overview"); |
| displayOverview(data); |
| } catch (error) { |
| console.error("Error loading overview:", error); |
| showError("Failed to load overview data"); |
| } |
| } |
| |
| function displayOverview(data) { |
| const overviewGrid = document.getElementById("overviewGrid"); |
| const systemInfo = document.getElementById("systemInfo"); |
| |
| // Display overview cards |
| overviewGrid.innerHTML = ` |
| <div class="dashboard-card"> |
| <h3>Total Users</h3> |
| <div class="value">${data.overview.total_users}</div> |
| <div class="description">Registered annotators</div> |
| </div> |
| <div class="dashboard-card"> |
| <h3>Active Users</h3> |
| <div class="value">${data.overview.active_users}</div> |
| <div class="description">Currently annotating</div> |
| </div> |
| <div class="dashboard-card"> |
| <h3>Total Annotations</h3> |
| <div class="value">${data.overview.total_annotations}</div> |
| <div class="description">Completed annotations</div> |
| </div> |
| <div class="dashboard-card"> |
| <h3>Completion</h3> |
| <div class="value">${data.overview.completion_percentage}%</div> |
| <div class="description">Items with annotations</div> |
| </div> |
| <div class="dashboard-card"> |
| <h3>Total Items</h3> |
| <div class="value">${data.overview.total_items}</div> |
| <div class="description">Items in dataset</div> |
| </div> |
| <div class="dashboard-card"> |
| <h3>Working Time</h3> |
| <div class="value">${data.overview.total_working_time}</div> |
| <div class="description">Total time spent</div> |
| </div> |
| `; |
| |
| // Display system info |
| systemInfo.innerHTML = ` |
| <div class="config-group"> |
| <label>Task Name</label> |
| <input type="text" value="${ |
| data.config.annotation_task_name |
| }" readonly> |
| </div> |
| <div class="config-group"> |
| <label>Max Annotations per User</label> |
| <input type="text" value="${ |
| data.config.max_annotations_per_user |
| }" readonly> |
| </div> |
| <div class="config-group"> |
| <label>Max Annotations per Item</label> |
| <input type="text" value="${ |
| data.config.max_annotations_per_item |
| }" readonly> |
| </div> |
| <div class="config-group"> |
| <label>Assignment Strategy</label> |
| <input type="text" value="${ |
| data.config.assignment_strategy |
| }" readonly> |
| </div> |
| <div class="config-group"> |
| <label>Debug Mode</label> |
| <input type="text" value="${ |
| data.config.debug_mode ? "Enabled" : "Disabled" |
| }" readonly> |
| </div> |
| `; |
| } |
| |
| // Annotators functions |
| async function loadAnnotators() { |
| try { |
| const data = await makeApiRequest("/admin/api/annotators"); |
| console.log(data); |
| displayAnnotators(data); |
| } catch (error) { |
| console.error("Error loading annotators:", error); |
| showError("Failed to load annotators data"); |
| } |
| } |
| |
| function displayAnnotators(data) { |
| const tbody = document.getElementById("annotatorsTableBody"); |
| |
| tbody.innerHTML = data.annotators |
| .map( |
| (annotator) => ` |
| <tr> |
| <td>${annotator.user_id}</td> |
| <td><span class="status-badge status-${annotator.phase.toLowerCase()}">${ |
| annotator.phase |
| }</span></td> |
| <td>${annotator.total_annotations}</td> |
| <td>${annotator.total_working_time || "N/A"}</td> |
| <td>${annotator.average_time_per_annotation || "N/A"}</td> |
| <td>${annotator.annotations_per_hour}/hr</td> |
| <td>${annotator.completion_percentage.toFixed(1)}%</td> |
| <td> |
| <div style="display:flex;align-items:center;gap:4px;"> |
| <button onclick="adjustUserInstances('${annotator.user_id}', -5)" style="padding:2px 6px;cursor:pointer;">-</button> |
| <span id="max-inst-${annotator.user_id}">${annotator.max_assignments < 0 ? '∞' : annotator.max_assignments}</span> |
| <button onclick="adjustUserInstances('${annotator.user_id}', 5)" style="padding:2px 6px;cursor:pointer;">+</button> |
| </div> |
| </td> |
| <td>${ |
| annotator.last_activity |
| ? new Date(annotator.last_activity).toLocaleString() |
| : "N/A" |
| }</td> |
| </tr> |
| ` |
| ) |
| .join(""); |
| } |
| |
| async function adjustUserInstances(userId, delta) { |
| try { |
| // First get current value |
| const span = document.getElementById(`max-inst-${userId}`); |
| let current = span.textContent === '∞' ? -1 : parseInt(span.textContent); |
| let newVal = current < 0 ? delta : current + delta; |
| if (newVal < 0) newVal = -1; // Switch to unlimited |
| |
| const response = await makeApiRequest(`/admin/api/user/${encodeURIComponent(userId)}/set_instances`, { |
| method: 'POST', |
| headers: {'Content-Type': 'application/json'}, |
| body: JSON.stringify({max_instances: newVal}) |
| }); |
| |
| if (response.success) { |
| span.textContent = response.max_instances < 0 ? '∞' : response.max_instances; |
| } |
| } catch (err) { |
| console.error('Failed to adjust instances:', err); |
| } |
| } |
| |
| function refreshAnnotators() { |
| loadAnnotators(); |
| } |
| |
| // Instances functions |
| async function loadInstances() { |
| try { |
| // Update global state from form controls |
| currentPageSize = parseInt(document.getElementById("pageSize").value); |
| currentSortBy = document.getElementById("sortBy").value; |
| currentSortOrder = document.getElementById("sortOrder").value; |
| currentFilter = document.getElementById("filterCompletion").value; |
| |
| const params = new URLSearchParams({ |
| page: currentPage, |
| page_size: currentPageSize, |
| sort_by: currentSortBy, |
| sort_order: currentSortOrder, |
| }); |
| |
| if (currentFilter) { |
| params.append("filter_completion", currentFilter); |
| } |
| |
| const data = await makeApiRequest(`/admin/api/instances?${params}`); |
| displayInstances(data); |
| } catch (error) { |
| console.error("Error loading instances:", error); |
| showError("Failed to load instances data"); |
| } |
| } |
| |
| function displayInstances(data) { |
| const tbody = document.getElementById("instancesTableBody"); |
| const pagination = document.getElementById("instancesPagination"); |
| console.log(data.instances); |
| // Display instances |
| tbody.innerHTML = data.instances |
| .map( |
| (instance) => ` |
| <tr> |
| <td>${instance.id}</td> |
| <td title="${instance.text}">${instance.text}</td> |
| <td>${instance.annotation_count}</td> |
| <td>${instance.completion_percentage}%</td> |
| <td>${instance.most_frequent_label || "N/A"}</td> |
| <td>${instance.label_disagreement}</td> |
| <td>${instance.average_time_per_annotation || "N/A"}</td> |
| <td>${instance.annotators.join(", ") || "None"}</td> |
| <td>${instance.num_ai_instance}</td> |
| </tr> |
| ` |
| ) |
| .join(""); |
| |
| // Display pagination |
| displayPagination(pagination, data.pagination, "instances"); |
| } |
| |
| function displayPagination(container, pagination, type) { |
| const { page, total_pages, has_next, has_prev } = pagination; |
| |
| let paginationHtml = ` |
| <div class="pagination-info"> |
| Showing page ${page} of ${total_pages} (${ |
| pagination.total_instances || pagination.total_annotators |
| } total) |
| </div> |
| <div class="pagination-controls"> |
| `; |
| |
| // Previous button |
| paginationHtml += ` |
| <button class="pagination-button" ${!has_prev ? "disabled" : ""} |
| onclick="changePage(${ |
| page - 1 |
| }, '${type}')">Previous</button> |
| `; |
| |
| // Page numbers |
| const startPage = Math.max(1, page - 2); |
| const endPage = Math.min(total_pages, page + 2); |
| |
| for (let i = startPage; i <= endPage; i++) { |
| paginationHtml += ` |
| <button class="pagination-button ${ |
| i === page ? "active" : "" |
| }" |
| onclick="changePage(${i}, '${type}')">${i}</button> |
| `; |
| } |
| |
| // Next button |
| paginationHtml += ` |
| <button class="pagination-button" ${!has_next ? "disabled" : ""} |
| onclick="changePage(${ |
| page + 1 |
| }, '${type}')">Next</button> |
| `; |
| |
| paginationHtml += "</div>"; |
| container.innerHTML = paginationHtml; |
| } |
| |
| function changePage(page, type) { |
| currentPage = page; |
| if (type === "instances") { |
| loadInstances(); |
| } |
| } |
| |
| function refreshInstances() { |
| currentPage = 1; |
| loadInstances(); |
| } |
| |
| // Questions functions |
| async function loadQuestions() { |
| try { |
| const data = await makeApiRequest("/admin/api/questions"); |
| displayQuestions(data); |
| } catch (error) { |
| console.error("Error loading questions:", error); |
| showError("Failed to load questions data"); |
| } |
| } |
| |
| function displayQuestions(data) { |
| const container = document.getElementById("questionsContainer"); |
| |
| if (!data.questions || data.questions.length === 0) { |
| container.innerHTML = ` |
| <div class="question-card"> |
| <div class="error-message"> |
| No annotation schemas found. Please check your configuration. |
| </div> |
| </div> |
| `; |
| return; |
| } |
| |
| container.innerHTML = data.questions |
| .map( |
| (question) => ` |
| <div class="question-card"> |
| <div class="question-header"> |
| <h3 class="question-title">${question.name}</h3> |
| <span class="question-type">${question.type}</span> |
| </div> |
| |
| <p class="question-description">${question.description}</p> |
| |
| <div class="question-stats"> |
| <div class="question-stat"> |
| <span class="value">${ |
| question.total_annotations |
| }</span> |
| <span class="label">Total Annotations</span> |
| </div> |
| <div class="question-stat"> |
| <span class="value">${ |
| question.items_with_annotations |
| }</span> |
| <span class="label">Items with Annotations</span> |
| </div> |
| </div> |
| |
| <div class="visualization"> |
| ${generateVisualization(question)} |
| </div> |
| </div> |
| ` |
| ) |
| .join(""); |
| } |
| |
| function generateVisualization(question) { |
| const analysis = question.analysis; |
| |
| if (analysis.error) { |
| return `<div class="error-message">${analysis.error}</div>`; |
| } |
| |
| switch (analysis.visualization_type) { |
| case "histogram": |
| return generateHistogram(analysis); |
| case "distribution": |
| return generateDistribution(analysis); |
| case "text_analysis": |
| return generateTextAnalysis(analysis); |
| case "span_analysis": |
| return generateSpanAnalysis(analysis); |
| case "multiselect_analysis": |
| return generateMultiselectAnalysis(analysis); |
| default: |
| return `<div class="error-message">Unsupported visualization type: ${analysis.visualization_type}</div>`; |
| } |
| } |
| |
| function generateHistogram(analysis) { |
| const { labels, counts, percentages } = analysis.data; |
| const maxCount = Math.max(...counts); |
| |
| let html = "<h4>Response Distribution</h4>"; |
| html += '<div class="histogram">'; |
| |
| labels.forEach((label, index) => { |
| const count = counts[index]; |
| const percentage = percentages[index]; |
| const height = maxCount > 0 ? (count / maxCount) * 100 : 0; |
| |
| html += ` |
| <div class="histogram-bar" style="height: ${height}%"> |
| <div class="histogram-value">${count}</div> |
| <div class="histogram-label">${label}</div> |
| </div> |
| `; |
| }); |
| |
| html += "</div>"; |
| |
| if (analysis.most_common) { |
| html += `<p><strong>Most common:</strong> ${analysis.most_common[0]} (${analysis.most_common[1]} times)</p>`; |
| } |
| |
| if (analysis.agreement_score !== undefined) { |
| html += `<p><strong>Agreement score:</strong> ${analysis.agreement_score}%</p>`; |
| } |
| |
| return html; |
| } |
| |
| function generateDistribution(analysis) { |
| const { statistics, bins } = analysis.data; |
| |
| let html = "<h4>Value Distribution</h4>"; |
| html += '<div class="distribution-chart">'; |
| |
| const maxCount = Math.max(...bins.counts); |
| bins.counts.forEach((count, index) => { |
| const height = maxCount > 0 ? (count / maxCount) * 100 : 0; |
| const binLabel = `${bins.bins[index]}-${bins.bins[index + 1]}`; |
| |
| html += ` |
| <div class="distribution-bar" style="height: ${height}%"> |
| <div class="histogram-value">${count}</div> |
| <div class="histogram-label">${binLabel}</div> |
| </div> |
| `; |
| }); |
| |
| html += "</div>"; |
| |
| html += ` |
| <div class="text-analysis"> |
| <div class="text-stat"> |
| <span class="value">${statistics.mean}</span> |
| <span class="label">Mean</span> |
| </div> |
| <div class="text-stat"> |
| <span class="value">${statistics.median}</span> |
| <span class="label">Median</span> |
| </div> |
| <div class="text-stat"> |
| <span class="value">${statistics.min} - ${statistics.max}</span> |
| <span class="label">Range</span> |
| </div> |
| <div class="text-stat"> |
| <span class="value">${statistics.std}</span> |
| <span class="label">Std Dev</span> |
| </div> |
| </div> |
| `; |
| |
| return html; |
| } |
| |
| function generateTextAnalysis(analysis) { |
| const { statistics, common_words } = analysis.data; |
| |
| let html = "<h4>Text Analysis</h4>"; |
| |
| html += ` |
| <div class="text-analysis"> |
| <div class="text-stat"> |
| <span class="value">${statistics.avg_length}</span> |
| <span class="label">Avg Length (chars)</span> |
| </div> |
| <div class="text-stat"> |
| <span class="value">${statistics.avg_words}</span> |
| <span class="label">Avg Words</span> |
| </div> |
| <div class="text-stat"> |
| <span class="value">${statistics.min_length} - ${statistics.max_length}</span> |
| <span class="label">Length Range</span> |
| </div> |
| <div class="text-stat"> |
| <span class="value">${statistics.empty_responses}</span> |
| <span class="label">Empty Responses</span> |
| </div> |
| </div> |
| `; |
| |
| if (common_words && common_words.length > 0) { |
| html += "<h5>Most Common Words</h5>"; |
| html += '<div class="common-words">'; |
| common_words.forEach(([word, count]) => { |
| html += `<span class="word-tag">${word}<span class="count">(${count})</span></span>`; |
| }); |
| html += "</div>"; |
| } |
| |
| return html; |
| } |
| |
| function generateSpanAnalysis(analysis) { |
| const { statistics, total_spans } = analysis.data; |
| |
| let html = "<h4>Span Analysis</h4>"; |
| |
| html += ` |
| <div class="span-analysis"> |
| <div class="span-stat"> |
| <span class="value">${statistics.avg_spans_per_item}</span> |
| <span class="label">Avg Spans per Item</span> |
| </div> |
| <div class="span-stat"> |
| <span class="value">${statistics.items_with_spans}</span> |
| <span class="label">Items with Spans</span> |
| </div> |
| <div class="span-stat"> |
| <span class="value">${statistics.min_spans} - ${statistics.max_spans}</span> |
| <span class="label">Span Range</span> |
| </div> |
| <div class="span-stat"> |
| <span class="value">${total_spans}</span> |
| <span class="label">Total Spans</span> |
| </div> |
| </div> |
| `; |
| |
| return html; |
| } |
| |
| function generateMultiselectAnalysis(analysis) { |
| const { labels, counts, percentages, average_labels_per_item } = |
| analysis.data; |
| const maxCount = Math.max(...counts); |
| |
| let html = "<h4>Label Frequency</h4>"; |
| html += '<div class="histogram">'; |
| |
| labels.forEach((label, index) => { |
| const count = counts[index]; |
| const percentage = percentages[index]; |
| const height = maxCount > 0 ? (count / maxCount) * 100 : 0; |
| |
| html += ` |
| <div class="histogram-bar" style="height: ${height}%"> |
| <div class="histogram-value">${count}</div> |
| <div class="histogram-label">${label}</div> |
| </div> |
| `; |
| }); |
| |
| html += "</div>"; |
| |
| html += `<p><strong>Average labels per item:</strong> ${average_labels_per_item}</p>`; |
| |
| if (analysis.most_common && analysis.most_common.length > 0) { |
| html += "<p><strong>Most common labels:</strong> "; |
| html += analysis.most_common |
| .map(([label, count]) => `${label} (${count})`) |
| .join(", "); |
| html += "</p>"; |
| } |
| |
| return html; |
| } |
| |
| function refreshQuestions() { |
| loadQuestions(); |
| } |
| |
| // Crowdsourcing functions |
| async function loadCrowdsourcing() { |
| try { |
| const data = await makeApiRequest("/admin/api/crowdsourcing"); |
| displayCrowdsourcing(data); |
| } catch (error) { |
| console.error("Error loading crowdsourcing data:", error); |
| showError("Failed to load crowdsourcing data"); |
| } |
| } |
| |
| function displayCrowdsourcing(data) { |
| const grid = document.getElementById("crowdsourcingGrid"); |
| const details = document.getElementById("crowdsourcingDetails"); |
| |
| // Display summary cards |
| grid.innerHTML = ` |
| <div class="dashboard-card"> |
| <h3>Total Workers</h3> |
| <div class="value">${data.summary.total_workers}</div> |
| <div class="description">All crowdsourcing workers</div> |
| </div> |
| <div class="dashboard-card"> |
| <h3>Prolific Workers</h3> |
| <div class="value">${data.summary.prolific_workers}</div> |
| <div class="description">${data.summary.prolific_studies} study/studies</div> |
| </div> |
| <div class="dashboard-card"> |
| <h3>MTurk Workers</h3> |
| <div class="value">${data.summary.mturk_workers}</div> |
| <div class="description">${data.summary.mturk_hits} HIT(s)</div> |
| </div> |
| <div class="dashboard-card"> |
| <h3>Other Workers</h3> |
| <div class="value">${data.summary.other_workers}</div> |
| <div class="description">Direct or non-platform</div> |
| </div> |
| `; |
| |
| // Display platform details |
| let detailsHtml = ""; |
| |
| // Prolific section |
| if (data.prolific.workers.length > 0) { |
| detailsHtml += generatePlatformSection("Prolific", data.prolific, "prolific"); |
| } |
| |
| // MTurk section |
| if (data.mturk.workers.length > 0) { |
| detailsHtml += generatePlatformSection("Amazon MTurk", data.mturk, "mturk"); |
| } |
| |
| // Other workers section |
| if (data.other.workers.length > 0) { |
| detailsHtml += generatePlatformSection("Other", data.other, "other"); |
| } |
| |
| if (!detailsHtml) { |
| detailsHtml = ` |
| <div class="question-card"> |
| <div class="error-message"> |
| No crowdsourcing workers detected. Workers will appear here when they access the task via Prolific or MTurk. |
| </div> |
| </div> |
| `; |
| } |
| |
| details.innerHTML = detailsHtml; |
| } |
| |
| function generatePlatformSection(platformName, platformData, platformKey) { |
| const stats = platformData.stats; |
| const workers = platformData.workers; |
| |
| let html = ` |
| <div class="question-card"> |
| <div class="question-header"> |
| <h3 class="question-title">${platformName}</h3> |
| <span class="question-type">${stats.count} workers</span> |
| </div> |
| |
| <div class="question-stats"> |
| <div class="question-stat"> |
| <span class="value">${stats.total_annotations}</span> |
| <span class="label">Total Annotations</span> |
| </div> |
| <div class="question-stat"> |
| <span class="value">${stats.avg_annotations_per_worker}</span> |
| <span class="label">Avg per Worker</span> |
| </div> |
| <div class="question-stat"> |
| <span class="value">${stats.avg_time_per_worker_minutes}m</span> |
| <span class="label">Avg Time</span> |
| </div> |
| <div class="question-stat"> |
| <span class="value">${stats.completed_count}</span> |
| <span class="label">Completed</span> |
| </div> |
| <div class="question-stat"> |
| <span class="value">${stats.in_progress_count}</span> |
| <span class="label">In Progress</span> |
| </div> |
| </div> |
| `; |
| |
| // Show study IDs or HIT IDs |
| if (platformKey === "prolific" && platformData.study_ids && platformData.study_ids.length > 0) { |
| html += `<p><strong>Study IDs:</strong> ${platformData.study_ids.join(", ")}</p>`; |
| } |
| if (platformKey === "mturk" && platformData.hit_ids && platformData.hit_ids.length > 0) { |
| html += `<p><strong>HIT IDs:</strong> ${platformData.hit_ids.join(", ")}</p>`; |
| } |
| |
| // Workers table |
| html += ` |
| <div class="admin-table-container" style="margin-top: 1rem;"> |
| <table class="admin-table"> |
| <thead> |
| <tr> |
| <th>Worker ID</th> |
| <th>Phase</th> |
| <th>Annotations</th> |
| <th>Time</th> |
| <th>Speed</th> |
| <th>Completion</th> |
| <th>Status</th> |
| `; |
| |
| if (platformKey === "prolific") { |
| html += `<th>Session ID</th>`; |
| } else if (platformKey === "mturk") { |
| html += `<th>Assignment ID</th>`; |
| } |
| |
| html += ` |
| </tr> |
| </thead> |
| <tbody> |
| `; |
| |
| workers.forEach(worker => { |
| const phaseClass = worker.phase.includes("DONE") ? "status-completed" : |
| worker.phase.includes("ANNOTATION") ? "status-active" : "status-login"; |
| const suspiciousClass = worker.suspicious_level === "High" || worker.suspicious_level === "Very High" ? |
| "color: var(--error-color);" : ""; |
| |
| html += ` |
| <tr> |
| <td>${worker.worker_id}</td> |
| <td><span class="status-badge ${phaseClass}">${worker.phase.replace("Phase.", "")}</span></td> |
| <td>${worker.total_annotations}</td> |
| <td>${formatTime(worker.total_seconds)}</td> |
| <td>${worker.annotations_per_hour.toFixed(1)}/hr</td> |
| <td>${worker.completion_percentage.toFixed(1)}%</td> |
| <td style="${suspiciousClass}">${worker.suspicious_level}</td> |
| `; |
| |
| if (platformKey === "prolific") { |
| html += `<td>${worker.session_id || "N/A"}</td>`; |
| } else if (platformKey === "mturk") { |
| html += `<td>${worker.assignment_id || "N/A"}</td>`; |
| } |
| |
| html += `</tr>`; |
| }); |
| |
| html += ` |
| </tbody> |
| </table> |
| </div> |
| </div> |
| `; |
| |
| return html; |
| } |
| |
| function formatTime(seconds) { |
| if (seconds < 60) return `${seconds}s`; |
| if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`; |
| return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`; |
| } |
| |
| function refreshCrowdsourcing() { |
| loadCrowdsourcing(); |
| } |
| |
| // Behavioral Analytics functions |
| async function loadBehavioral() { |
| try { |
| const data = await makeApiRequest("/admin/api/behavioral_analytics"); |
| displayBehavioral(data); |
| } catch (error) { |
| console.error("Error loading behavioral data:", error); |
| showError("Failed to load behavioral analytics data"); |
| } |
| } |
| |
| function displayBehavioral(data) { |
| const grid = document.getElementById("behavioralGrid"); |
| const aiContent = document.getElementById("aiUsageContent"); |
| const qualityContent = document.getElementById("qualityContent"); |
| const tbody = document.getElementById("behavioralTableBody"); |
| |
| // Display summary cards |
| const stats = data.aggregate_stats; |
| grid.innerHTML = ` |
| <div class="dashboard-card"> |
| <h3>Users with Data</h3> |
| <div class="value">${stats.total_users}</div> |
| <div class="description">Users with behavioral tracking</div> |
| </div> |
| <div class="dashboard-card"> |
| <h3>Total Instances</h3> |
| <div class="value">${stats.total_instances}</div> |
| <div class="description">Tracked annotation sessions</div> |
| </div> |
| <div class="dashboard-card"> |
| <h3>Avg Time</h3> |
| <div class="value">${stats.avg_time_per_instance_sec.toFixed(1)}s</div> |
| <div class="description">Per annotation instance</div> |
| </div> |
| <div class="dashboard-card"> |
| <h3>Total Interactions</h3> |
| <div class="value">${stats.total_interactions}</div> |
| <div class="description">Clicks, focus, navigation</div> |
| </div> |
| <div class="dashboard-card"> |
| <h3>Annotation Changes</h3> |
| <div class="value">${stats.total_changes}</div> |
| <div class="description">Label modifications</div> |
| </div> |
| <div class="dashboard-card"> |
| <h3>AI Requests</h3> |
| <div class="value">${stats.total_ai_requests}</div> |
| <div class="description">AI assistance requests</div> |
| </div> |
| `; |
| |
| // Display AI usage section |
| const ai = data.ai_usage; |
| if (ai.total_requests > 0) { |
| aiContent.innerHTML = ` |
| <div class="question-stats"> |
| <div class="question-stat"> |
| <span class="value">${ai.total_requests}</span> |
| <span class="label">Total Requests</span> |
| </div> |
| <div class="question-stat"> |
| <span class="value">${ai.total_accepts}</span> |
| <span class="label">Accepted</span> |
| </div> |
| <div class="question-stat"> |
| <span class="value">${ai.total_rejects}</span> |
| <span class="label">Rejected</span> |
| </div> |
| <div class="question-stat"> |
| <span class="value">${ai.accept_rate.toFixed(1)}%</span> |
| <span class="label">Accept Rate</span> |
| </div> |
| <div class="question-stat"> |
| <span class="value">${(ai.avg_decision_time_ms / 1000).toFixed(1)}s</span> |
| <span class="label">Avg Decision Time</span> |
| </div> |
| </div> |
| `; |
| } else { |
| aiContent.innerHTML = `<p style="color: var(--muted-foreground);">No AI assistance usage recorded yet.</p>`; |
| } |
| |
| // Display quality indicators |
| const quality = data.quality_summary; |
| const suspicionColor = quality.high_suspicion_users > 0 ? "var(--error-color)" : "var(--success-color)"; |
| qualityContent.innerHTML = ` |
| <div class="question-stats"> |
| <div class="question-stat"> |
| <span class="value" style="color: ${suspicionColor}">${quality.high_suspicion_users}</span> |
| <span class="label">High Suspicion Users</span> |
| </div> |
| <div class="question-stat"> |
| <span class="value">${quality.fast_annotation_rate.toFixed(1)}%</span> |
| <span class="label">Fast Annotations (<2s)</span> |
| </div> |
| <div class="question-stat"> |
| <span class="value">${quality.low_interaction_rate.toFixed(1)}%</span> |
| <span class="label">Low Interaction Rate</span> |
| </div> |
| <div class="question-stat"> |
| <span class="value">${quality.no_change_rate.toFixed(1)}%</span> |
| <span class="label">No Change Rate</span> |
| </div> |
| </div> |
| <div style="margin-top: 1rem;"> |
| <h4>Interaction Types</h4> |
| <div class="common-words"> |
| ${Object.entries(data.interaction_types).map(([type, count]) => |
| `<span class="word-tag">${type}<span class="count">(${count})</span></span>` |
| ).join('')} |
| </div> |
| </div> |
| <div style="margin-top: 1rem;"> |
| <h4>Change Sources</h4> |
| <div class="common-words"> |
| ${Object.entries(data.change_sources).map(([source, count]) => |
| `<span class="word-tag">${source}<span class="count">(${count})</span></span>` |
| ).join('')} |
| </div> |
| </div> |
| `; |
| |
| // Display per-user table |
| tbody.innerHTML = data.users.map(user => { |
| const suspicionClass = user.suspicion_score > 0.5 ? "color: var(--error-color); font-weight: bold;" : |
| user.suspicion_score > 0.3 ? "color: orange;" : ""; |
| return ` |
| <tr> |
| <td>${user.user_id}</td> |
| <td>${user.total_instances}</td> |
| <td>${user.avg_time_sec.toFixed(1)}</td> |
| <td>${user.total_interactions}</td> |
| <td>${user.total_changes}</td> |
| <td>${user.ai_requests}</td> |
| <td>${user.ai_requests > 0 ? (user.ai_accept_rate * 100).toFixed(0) + '%' : 'N/A'}</td> |
| <td style="${suspicionClass}">${(user.suspicion_score * 100).toFixed(0)}%</td> |
| </tr> |
| `; |
| }).join(''); |
| } |
| |
| function refreshBehavioral() { |
| loadBehavioral(); |
| } |
| |
| // Configuration functions |
| async function loadConfig() { |
| try { |
| const data = await makeApiRequest("/admin/api/config"); |
| displayConfig(data); |
| } catch (error) { |
| console.error("Error loading config:", error); |
| showError("Failed to load configuration"); |
| } |
| } |
| |
| function displayConfig(data) { |
| document.getElementById("maxAnnotationsPerUser").value = |
| data.max_annotations_per_user; |
| document.getElementById("maxAnnotationsPerItem").value = |
| data.max_annotations_per_item; |
| document.getElementById("assignmentStrategy").value = |
| data.assignment_strategy; |
| } |
| |
| // Form submission |
| document |
| .getElementById("configForm") |
| .addEventListener("submit", async function (e) { |
| e.preventDefault(); |
| |
| const formData = { |
| max_annotations_per_user: parseInt( |
| document.getElementById("maxAnnotationsPerUser").value |
| ), |
| max_annotations_per_item: parseInt( |
| document.getElementById("maxAnnotationsPerItem").value |
| ), |
| assignment_strategy: |
| document.getElementById("assignmentStrategy").value, |
| }; |
| |
| try { |
| const result = await makeApiRequest("/admin/api/config", { |
| method: "POST", |
| body: JSON.stringify(formData), |
| }); |
| |
| showSuccess("Configuration updated successfully"); |
| loadOverview(); // Refresh overview to show updated stats |
| } catch (error) { |
| console.error("Error updating config:", error); |
| showError("Failed to update configuration"); |
| } |
| }); |
| |
| // BWS Scoring functions |
| async function loadBwsScoring() { |
| try { |
| const data = await makeApiRequest("/admin/api/bws_scoring"); |
| displayBwsScoring(data); |
| } catch (error) { |
| console.error("Error loading BWS scoring data:", error); |
| showError("Failed to load BWS scoring data"); |
| } |
| } |
| |
| async function generateBwsScores() { |
| try { |
| const method = document.getElementById('bwsScoringMethod').value; |
| const btn = document.querySelector('#bws-scoring .shadcn-button-primary'); |
| if (btn) { btn.textContent = 'Computing...'; btn.disabled = true; } |
| const result = await makeApiRequest(`/admin/api/bws_scoring/generate?method=${method}`, { method: "POST" }); |
| if (result.status === "success") { |
| displayBwsScoring(result); |
| } else { |
| showError(result.error || "BWS score generation failed"); |
| } |
| if (btn) { btn.textContent = 'Generate Scores'; btn.disabled = false; } |
| } catch (error) { |
| console.error("Error generating BWS scores:", error); |
| showError("Failed to generate BWS scores"); |
| const btn = document.querySelector('#bws-scoring .shadcn-button-primary'); |
| if (btn) { btn.textContent = 'Generate Scores'; btn.disabled = false; } |
| } |
| } |
| |
| function displayBwsScoring(data) { |
| const grid = document.getElementById("bwsScoringGrid"); |
| const tbody = document.getElementById("bwsScoresBody"); |
| |
| if (!data || data.error) { |
| grid.innerHTML = '<p style="color: var(--muted-foreground);">BWS scoring not available.</p>'; |
| return; |
| } |
| |
| // Summary cards |
| grid.innerHTML = ` |
| <div class="dashboard-card"> |
| <div class="stat-number">${data.total_items || 0}</div> |
| <div class="description">Pool Items</div> |
| </div> |
| <div class="dashboard-card"> |
| <div class="stat-number">${data.total_annotations || 0}</div> |
| <div class="description">BWS Annotations</div> |
| </div> |
| <div class="dashboard-card"> |
| <div class="stat-number">${data.method || 'counting'}</div> |
| <div class="description">Scoring Method</div> |
| </div> |
| `; |
| |
| // Scores table |
| const scores = data.scores || []; |
| if (scores.length === 0) { |
| tbody.innerHTML = '<tr><td colspan="7" style="color: var(--muted-foreground);">Click "Generate Scores" to compute BWS scores</td></tr>'; |
| return; |
| } |
| |
| tbody.innerHTML = scores.map((row, i) => ` |
| <tr> |
| <td>${row.rank || i + 1}</td> |
| <td><code>${row.item_id || ''}</code></td> |
| <td style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${row.text || ''}</td> |
| <td><strong>${typeof row.score === 'number' ? row.score.toFixed(4) : row.score || ''}</strong></td> |
| <td>${row.best_count !== undefined ? row.best_count : ''}</td> |
| <td>${row.worst_count !== undefined ? row.worst_count : ''}</td> |
| <td>${row.appearances !== undefined ? row.appearances : ''}</td> |
| </tr> |
| `).join(''); |
| } |
| |
| // MACE functions |
| async function loadMace() { |
| try { |
| const data = await makeApiRequest("/admin/api/mace/overview"); |
| displayMace(data); |
| } catch (error) { |
| console.error("Error loading MACE data:", error); |
| showError("Failed to load MACE data"); |
| } |
| } |
| |
| async function triggerMace() { |
| try { |
| const btn = document.querySelector('#mace .shadcn-button-primary'); |
| if (btn) { btn.textContent = 'Running...'; btn.disabled = true; } |
| const result = await makeApiRequest("/admin/api/mace/trigger", { method: "POST" }); |
| if (result.status === "success") { |
| loadMace(); |
| } else { |
| showError(result.error || "MACE trigger failed"); |
| } |
| if (btn) { btn.textContent = 'Run MACE'; btn.disabled = false; } |
| } catch (error) { |
| console.error("Error triggering MACE:", error); |
| showError("Failed to trigger MACE"); |
| const btn = document.querySelector('#mace .shadcn-button-primary'); |
| if (btn) { btn.textContent = 'Run MACE'; btn.disabled = false; } |
| } |
| } |
| |
| function displayMace(data) { |
| const grid = document.getElementById("maceGrid"); |
| const tbody = document.getElementById("maceCompetenceBody"); |
| const predsContainer = document.getElementById("macePredictionsContainer"); |
| |
| if (!data.enabled) { |
| grid.innerHTML = '<p style="color: var(--muted-foreground);">MACE is not enabled in the configuration.</p>'; |
| return; |
| } |
| |
| // Summary cards |
| const hasResults = data.has_results; |
| const numSchemas = data.schemas ? data.schemas.length : 0; |
| const numAnnotators = Object.keys(data.annotator_competence || {}).length; |
| |
| grid.innerHTML = ` |
| <div class="dashboard-card"> |
| <h3>Status</h3> |
| <div class="value" style="color: ${hasResults ? 'var(--success-color)' : 'var(--muted-foreground)'}"> |
| ${hasResults ? 'Results Available' : 'Not Run Yet'} |
| </div> |
| <div class="description">MACE estimation status</div> |
| </div> |
| <div class="dashboard-card"> |
| <h3>Schemas</h3> |
| <div class="value">${numSchemas}</div> |
| <div class="description">Schemas processed</div> |
| </div> |
| <div class="dashboard-card"> |
| <h3>Annotators</h3> |
| <div class="value">${numAnnotators}</div> |
| <div class="description">With competence scores</div> |
| </div> |
| <div class="dashboard-card"> |
| <h3>Configuration</h3> |
| <div class="value">${data.config ? data.config.trigger_every_n : '?'}</div> |
| <div class="description">Trigger every N annotations</div> |
| </div> |
| `; |
| |
| // Competence table |
| if (hasResults && data.annotator_competence) { |
| const sorted = Object.entries(data.annotator_competence) |
| .sort((a, b) => b[1].average - a[1].average); |
| |
| tbody.innerHTML = sorted.map(([uid, info]) => { |
| const score = info.average; |
| const pct = (score * 100).toFixed(1); |
| let color = 'var(--success-color)'; |
| let label = 'High'; |
| if (score < 0.5) { color = 'var(--error-color)'; label = 'Low'; } |
| else if (score < 0.7) { color = 'orange'; label = 'Moderate'; } |
| |
| return ` |
| <tr> |
| <td>${uid}</td> |
| <td> |
| <div style="display: flex; align-items: center; gap: 0.5rem;"> |
| <div style="flex: 1; background: var(--muted); border-radius: 4px; height: 8px; overflow: hidden;"> |
| <div style="width: ${pct}%; background: ${color}; height: 100%; border-radius: 4px;"></div> |
| </div> |
| <span style="font-weight: 600; min-width: 50px;">${pct}%</span> |
| </div> |
| </td> |
| <td><span style="color: ${color}; font-weight: 500;">${label}</span></td> |
| </tr> |
| `; |
| }).join(''); |
| } else { |
| tbody.innerHTML = '<tr><td colspan="3" style="color: var(--muted-foreground);">Click "Run MACE" to compute scores</td></tr>'; |
| } |
| |
| // Predictions - load for each schema |
| if (hasResults && data.schemas && data.schemas.length > 0) { |
| loadMacePredictions(data.schemas); |
| } else { |
| predsContainer.innerHTML = '<p style="color: var(--muted-foreground);">Run MACE to see predictions.</p>'; |
| } |
| } |
| |
| async function loadMacePredictions(schemas) { |
| const container = document.getElementById("macePredictionsContainer"); |
| let html = ''; |
| |
| for (const schema of schemas) { |
| try { |
| const data = await makeApiRequest(`/admin/api/mace/predictions?schema=${encodeURIComponent(schema.schema_name)}`); |
| if (data.error) continue; |
| |
| const preds = data.predicted_labels || {}; |
| const entropy = data.label_entropy || {}; |
| const items = Object.keys(preds).sort(); |
| |
| html += ` |
| <h4 style="margin-top: 1rem;">${schema.schema_name}${schema.option_name ? ' :: ' + schema.option_name : ''}</h4> |
| <div class="admin-table-container"> |
| <table class="admin-table"> |
| <thead> |
| <tr> |
| <th>Instance</th> |
| <th>Predicted Label</th> |
| <th>Uncertainty</th> |
| </tr> |
| </thead> |
| <tbody> |
| ${items.map(id => { |
| const ent = entropy[id] || 0; |
| const entPct = (ent * 100).toFixed(1); |
| let entColor = 'var(--success-color)'; |
| if (ent > 0.5) entColor = 'var(--error-color)'; |
| else if (ent > 0.1) entColor = 'orange'; |
| |
| return ` |
| <tr> |
| <td>${id}</td> |
| <td><strong>${preds[id]}</strong></td> |
| <td> |
| <div style="display: flex; align-items: center; gap: 0.5rem;"> |
| <div style="flex: 1; max-width: 100px; background: var(--muted); border-radius: 4px; height: 6px; overflow: hidden;"> |
| <div style="width: ${Math.min(entPct, 100)}%; background: ${entColor}; height: 100%; border-radius: 4px;"></div> |
| </div> |
| <span style="color: ${entColor}; font-size: 0.85em;">${entPct}%</span> |
| </div> |
| </td> |
| </tr> |
| `; |
| }).join('')} |
| </tbody> |
| </table> |
| </div> |
| `; |
| } catch (e) { |
| console.error(`Error loading predictions for ${schema.schema_name}:`, e); |
| } |
| } |
| |
| container.innerHTML = html || '<p style="color: var(--muted-foreground);">No predictions available.</p>'; |
| } |
| |
| // Utility functions |
| function showError(message) { |
| // Simple error display - could be enhanced with a toast notification |
| alert("Error: " + message); |
| } |
| |
| function showSuccess(message) { |
| // Simple success display - could be enhanced with a toast notification |
| alert("Success: " + message); |
| } |
| |
| function logout() { |
| // Clear admin session and redirect to login |
| fetch("/admin", { method: "GET" }).then(() => { |
| window.location.href = "/admin"; |
| }); |
| } |
| |
| // ============================================= |
| // Embedding Visualization Functions |
| // ============================================= |
| let embeddingVizLoaded = false; |
| |
| async function loadEmbeddingVisualization() { |
| if (embeddingVizLoaded && typeof embeddingViz !== 'undefined' && embeddingViz) { |
| return; |
| } |
| |
| // Load Plotly.js if not already loaded |
| if (typeof Plotly === 'undefined') { |
| await loadScript('https://cdn.plot.ly/plotly-2.27.0.min.js'); |
| } |
| |
| // Load embedding-viz.js if not already loaded |
| if (typeof initEmbeddingVisualization === 'undefined') { |
| await loadScript('/static/js/embedding-viz.js'); |
| } |
| |
| // Initialize the visualization |
| try { |
| const apiKey = "{{ admin_api_key }}"; |
| initEmbeddingVisualization('embedding-viz-chart', apiKey); |
| embeddingVizLoaded = true; |
| } catch (error) { |
| console.error('Failed to initialize embedding visualization:', error); |
| document.getElementById('embedding-viz-error').innerHTML = ` |
| <div class="error-message"> |
| <strong>Error:</strong> ${error.message || 'Failed to load visualization'} |
| </div> |
| `; |
| document.getElementById('embedding-viz-error').style.display = 'block'; |
| } |
| } |
| |
| function loadScript(src) { |
| return new Promise((resolve, reject) => { |
| const existing = document.querySelector(`script[src="${src}"]`); |
| if (existing) { |
| resolve(); |
| return; |
| } |
| const script = document.createElement('script'); |
| script.src = src; |
| script.onload = resolve; |
| script.onerror = reject; |
| document.head.appendChild(script); |
| }); |
| } |
| |
| function changeEmbeddingVizLabelSource() { |
| const select = document.getElementById('embedding-viz-label-source'); |
| if (select && typeof embeddingViz !== 'undefined' && embeddingViz) { |
| // For now, just refresh - a more advanced implementation would |
| // pass the label source to the backend |
| embeddingViz.refresh(true); |
| } |
| } |
| </script> |
| </body> |
| </html> |
|
|