// UI Component Builders
class UIComponents {
// Toast notifications
static showToast(message, type = 'info', duration = 3000) {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = `toast active ${type}`;
setTimeout(() => {
toast.classList.remove('active');
}, duration);
}
// Modal management
static openModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.add('active');
}
}
static closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) {
modal.classList.remove('active');
}
}
// Clear form
static clearForm(formId) {
const form = document.getElementById(formId);
if (form) {
form.reset();
}
}
// Create action buttons
static createActionButtons(editCallback, deleteCallback) {
const div = document.createElement('div');
div.className = 'action-buttons';
const editBtn = document.createElement('button');
editBtn.className = 'btn-edit';
editBtn.textContent = '✏️ Edit';
editBtn.onclick = editCallback;
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn-delete';
deleteBtn.textContent = '🗑️ Delete';
deleteBtn.onclick = deleteCallback;
div.appendChild(editBtn);
div.appendChild(deleteBtn);
return div;
}
// Create priority badge
static createPriorityBadge(priority) {
// Convert int to string label
const priorityMap = {
0: 'Low',
1: 'Medium',
2: 'High',
3: 'Critical'
};
// Handle both int and string priority
const priorityLabel = typeof priority === 'number' ? priorityMap[priority] : priority;
const span = document.createElement('span');
span.className = `priority-badge priority-${(priorityLabel || 'medium').toLowerCase()}`;
span.textContent = priorityLabel || 'Medium';
return span;
}
// Convert snake_case/camelCase key to Title Case header label
static formatHeader(key) {
return key
.replace(/_/g, ' ')
.replace(/\b\w/g, c => c.toUpperCase());
}
// Render categories table with dynamic thead derived from API keys
static renderCategoriesTable(categories) {
const thead = document.getElementById('categoriesTableHead');
const tbody = document.getElementById('categoriesTableBody');
tbody.innerHTML = '';
if (!categories || categories.length === 0) {
thead.innerHTML = '';
tbody.innerHTML = '
| No categories found |
';
return;
}
// Derive columns from first item's keys
const columns = Object.keys(categories[0]);
// Render thead
const headRow = document.createElement('tr');
[...columns, 'actions'].forEach(col => {
const th = document.createElement('th');
th.textContent = UIComponents.formatHeader(col);
headRow.appendChild(th);
});
thead.innerHTML = '';
thead.appendChild(headRow);
// Render tbody
categories.forEach(category => {
const row = document.createElement('tr');
columns.forEach(col => {
const td = document.createElement('td');
const value = category[col];
if (value === null || value === undefined) {
td.textContent = '—';
} else if (col === 'created_at') {
td.textContent = new Date(value).toLocaleString();
} else if (col === 'embedding' && typeof value === 'string' && value.length > 50) {
td.textContent = value.substring(0, 50) + '…';
td.title = value;
} else {
td.textContent = value;
}
row.appendChild(td);
});
const actionTd = document.createElement('td');
actionTd.appendChild(
UIComponents.createActionButtons(
() => window.editCategory(category),
() => window.deleteCategory(category.id)
)
);
row.appendChild(actionTd);
tbody.appendChild(row);
});
}
// Render intents table with dynamic thead derived from API keys
// categoriesMap: { [id]: label } for looking up category names
static renderIntentsTable(intents, categoriesMap = {}) {
const thead = document.getElementById('intentsTableHead');
const tbody = document.getElementById('intentsTableBody');
tbody.innerHTML = '';
if (!intents || intents.length === 0) {
thead.innerHTML = '';
tbody.innerHTML = '| No intents found |
';
return;
}
// Derive columns from first item, excluding category_label (null, redundant with category_id lookup)
const excludedCols = new Set(['category_label']);
const columns = Object.keys(intents[0]).filter(k => !excludedCols.has(k));
// Render thead
const headRow = document.createElement('tr');
[...columns, 'actions'].forEach(col => {
const th = document.createElement('th');
th.textContent = UIComponents.formatHeader(col);
headRow.appendChild(th);
});
thead.innerHTML = '';
thead.appendChild(headRow);
// Render tbody
intents.forEach(intent => {
const row = document.createElement('tr');
columns.forEach(col => {
const td = document.createElement('td');
const value = intent[col];
if (col === 'priority') {
td.appendChild(UIComponents.createPriorityBadge(value));
} else if (col === 'category_id') {
td.textContent = categoriesMap[value] || value;
} else if (value === null || value === undefined) {
td.textContent = '—';
} else if (col === 'created_at') {
td.textContent = new Date(value).toLocaleString();
} else if (col === 'embedding' && typeof value === 'string' && value.length > 50) {
td.textContent = value.substring(0, 50) + '…';
td.title = value;
} else {
td.textContent = value;
}
row.appendChild(td);
});
const actionTd = document.createElement('td');
actionTd.appendChild(
UIComponents.createActionButtons(
() => window.editIntent(intent),
() => window.deleteIntent(intent.id)
)
);
row.appendChild(actionTd);
tbody.appendChild(row);
});
}
// Populate category dropdown
static populateCategoryDropdown(categories, selectId = 'intentCategory') {
const select = document.getElementById(selectId);
if (!select) return;
const currentValue = select.value;
select.innerHTML = '';
categories.forEach(category => {
const option = document.createElement('option');
option.value = category.id;
option.textContent = category.label;
select.appendChild(option);
});
if (currentValue) {
select.value = currentValue;
}
}
// Populate filter dropdowns
static populateFilterDropdowns(categories) {
const categoryFilter = document.getElementById('categoryFilter');
if (categoryFilter) {
const currentValue = categoryFilter.value;
categoryFilter.innerHTML = '';
categories.forEach(category => {
const option = document.createElement('option');
option.value = category.id;
option.textContent = category.label;
categoryFilter.appendChild(option);
});
if (currentValue) {
categoryFilter.value = currentValue;
}
}
}
// Update dashboard stats
static updateDashboardStats(categories, intents) {
const criticalCount = intents.filter(i => i.priority === 3 || i.priority === 'Critical').length;
const els = {
totalCategories: categories.length,
totalIntents: intents.length,
criticalCount,
};
Object.entries(els).forEach(([id, val]) => {
const el = document.getElementById(id);
if (el) el.textContent = val;
});
}
// Update status indicator
static updateStatus(connected = true) {
const statusDot = document.querySelector('.status-dot');
const statusText = document.getElementById('statusText');
if (connected) {
statusDot.style.backgroundColor = 'var(--accent-green)';
statusText.textContent = 'Connected';
statusText.style.color = 'var(--text-secondary)';
} else {
statusDot.style.backgroundColor = 'var(--accent-red)';
statusText.textContent = 'Disconnected';
statusText.style.color = 'var(--accent-red)';
}
}
}