codebook / potato /templates /admin.html
davidjurgens's picture
Deploy: Potato — Codebook Annotation
aceb1b2 verified
Raw
History Blame Contribute Delete
70.1 kB
<!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 -->
<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 Content -->
<main class="admin-main">
<!-- Tab Navigation -->
<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>
<!-- Overview Tab -->
<div class="admin-tab-content active" id="overview">
<div class="dashboard-grid" id="overviewGrid">
<!-- Overview cards will be loaded here -->
</div>
<div class="config-form">
<h3>System Information</h3>
<div id="systemInfo">
<!-- System info will be loaded here -->
</div>
</div>
</div>
<!-- Annotators Tab -->
<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">
<!-- Annotators data will be loaded here -->
</tbody>
</table>
</div>
<div class="pagination" id="annotatorsPagination">
<!-- Pagination will be added here if needed -->
</div>
</div>
<!-- Instances Tab -->
<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">
<!-- Instances data will be loaded here -->
</tbody>
</table>
</div>
<div class="pagination" id="instancesPagination">
<!-- Pagination will be loaded here -->
</div>
</div>
<!-- Questions Tab -->
<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">
<!-- Questions data will be loaded here -->
</div>
</div>
<!-- Behavioral Analytics Tab -->
<div class="admin-tab-content" id="behavioral">
<div class="table-controls">
<button
onclick="refreshBehavioral()"
class="shadcn-button shadcn-button-primary"
>
Refresh Data
</button>
</div>
<!-- Summary Cards -->
<div class="dashboard-grid" id="behavioralGrid">
<!-- Behavioral summary cards will be loaded here -->
</div>
<!-- AI Usage Section -->
<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">
<!-- AI usage data will be loaded here -->
</div>
</div>
<!-- Quality Summary Section -->
<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">
<!-- Quality indicators will be loaded here -->
</div>
</div>
<!-- Per-User Behavioral Data -->
<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">
<!-- Behavioral data will be loaded here -->
</tbody>
</table>
</div>
</div>
<!-- Crowdsourcing Tab -->
<div class="admin-tab-content" id="crowdsourcing">
<div class="table-controls">
<button
onclick="refreshCrowdsourcing()"
class="shadcn-button shadcn-button-primary"
>
Refresh Data
</button>
</div>
<!-- Summary Cards -->
<div class="dashboard-grid" id="crowdsourcingGrid">
<!-- Crowdsourcing summary cards will be loaded here -->
</div>
<!-- Platform Sections -->
<div id="crowdsourcingDetails">
<!-- Platform-specific data will be loaded here -->
</div>
</div>
<!-- Embedding Visualization Tab -->
{% if embedding_viz_enabled %}
<div class="admin-tab-content" id="embedding-viz">
<!-- Stats Bar -->
<div class="dashboard-grid" id="embedding-viz-stats" style="margin-bottom: 1rem;">
<div class="dashboard-card">
<h3>Loading...</h3>
</div>
</div>
<!-- Main Content Area -->
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
<!-- Scatter Plot Container -->
<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>
<!-- Side Panel -->
<div style="flex: 1; min-width: 300px;">
<!-- Preview Panel -->
<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>
<!-- Selection Panel -->
<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>
<!-- Controls -->
<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 %}
<!-- Configuration Tab -->
{% 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 (&lt;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>