Spaces:
Running
Running
Upload dashboard/web/app.js with huggingface_hub
Browse files- dashboard/web/app.js +930 -930
dashboard/web/app.js
CHANGED
|
@@ -1,930 +1,930 @@
|
|
| 1 |
-
// SupportMind Dashboard
|
| 2 |
-
// Interactive demo with real API calls (falls back to simulation if API unavailable)
|
| 3 |
-
|
| 4 |
-
let API_BASE = window.location.origin;
|
| 5 |
-
let apiOnline = false;
|
| 6 |
-
|
| 7 |
-
function apiCandidates() {
|
| 8 |
-
const candidates = [];
|
| 9 |
-
const url = new URL(window.location.href);
|
| 10 |
-
if (url.hostname === '127.0.0.1' && url.port === '7860') {
|
| 11 |
-
candidates.push('http://127.0.0.1:7862');
|
| 12 |
-
candidates.push('http://127.0.0.1:7861');
|
| 13 |
-
}
|
| 14 |
-
candidates.push(window.location.origin);
|
| 15 |
-
return [...new Set(candidates)];
|
| 16 |
-
}
|
| 17 |
-
|
| 18 |
-
// Category colors
|
| 19 |
-
const CAT_COLORS = {
|
| 20 |
-
billing: '#fb923c', technical_support: '#8083ff', account_management: '#89ceff',
|
| 21 |
-
feature_request: '#c0c1ff', compliance_legal: '#f87171', onboarding: '#4ade80',
|
| 22 |
-
general_inquiry: '#94a3b8', churn_risk: '#facc15',
|
| 23 |
-
};
|
| 24 |
-
|
| 25 |
-
//
|
| 26 |
-
document.addEventListener('DOMContentLoaded', () => {
|
| 27 |
-
animateCounters();
|
| 28 |
-
initPresets();
|
| 29 |
-
initDropoutViz();
|
| 30 |
-
initScrollAnimations();
|
| 31 |
-
initSmoothScroll();
|
| 32 |
-
checkAPI();
|
| 33 |
-
updateMetrics();
|
| 34 |
-
setInterval(updateMetrics, 5000); // Update every 5 seconds
|
| 35 |
-
});
|
| 36 |
-
|
| 37 |
-
//
|
| 38 |
-
function animateCounters() {
|
| 39 |
-
document.querySelectorAll('.stat-card').forEach(card => {
|
| 40 |
-
const counter = card.querySelector('.counter');
|
| 41 |
-
const target = parseFloat(card.dataset.value);
|
| 42 |
-
const duration = 1500;
|
| 43 |
-
const start = performance.now();
|
| 44 |
-
function update(now) {
|
| 45 |
-
const elapsed = now - start;
|
| 46 |
-
const progress = Math.min(elapsed / duration, 1);
|
| 47 |
-
const eased = 1 - Math.pow(1 - progress, 3);
|
| 48 |
-
counter.textContent = Math.round(target * eased * 10) / 10;
|
| 49 |
-
if (progress < 1) requestAnimationFrame(update);
|
| 50 |
-
else counter.textContent = target;
|
| 51 |
-
}
|
| 52 |
-
requestAnimationFrame(update);
|
| 53 |
-
});
|
| 54 |
-
}
|
| 55 |
-
|
| 56 |
-
//
|
| 57 |
-
//
|
| 58 |
-
async function updateMetrics() {
|
| 59 |
-
try {
|
| 60 |
-
const res = await fetch(`${API_BASE}/metrics`);
|
| 61 |
-
if (!res.ok) return;
|
| 62 |
-
const data = await res.json();
|
| 63 |
-
|
| 64 |
-
// Update Counter
|
| 65 |
-
document.getElementById('live-total').textContent = data.total_requests.toLocaleString();
|
| 66 |
-
|
| 67 |
-
// Update Model Name
|
| 68 |
-
document.getElementById('live-model').textContent = data.model;
|
| 69 |
-
|
| 70 |
-
// Update Distribution Bar
|
| 71 |
-
const dist = data.routing_distribution;
|
| 72 |
-
document.getElementById('dist-route').style.width = `${dist.route_pct}%`;
|
| 73 |
-
document.getElementById('dist-clarify').style.width = `${dist.clarify_pct}%`;
|
| 74 |
-
document.getElementById('dist-escalate').style.width = `${dist.escalate_pct}%`;
|
| 75 |
-
|
| 76 |
-
// Update Status Pulse
|
| 77 |
-
const indicator = document.getElementById('live-indicator');
|
| 78 |
-
indicator.style.opacity = '1';
|
| 79 |
-
setTimeout(() => { indicator.style.opacity = '0.8'; }, 500);
|
| 80 |
-
|
| 81 |
-
} catch (err) {
|
| 82 |
-
console.warn("Metrics sync failed:", err);
|
| 83 |
-
}
|
| 84 |
-
}
|
| 85 |
-
|
| 86 |
-
//
|
| 87 |
-
function initPresets() {
|
| 88 |
-
document.querySelectorAll('.preset-btn').forEach(btn => {
|
| 89 |
-
btn.addEventListener('click', () => {
|
| 90 |
-
document.getElementById('ticket-input').value = btn.dataset.text;
|
| 91 |
-
});
|
| 92 |
-
});
|
| 93 |
-
}
|
| 94 |
-
|
| 95 |
-
function initSmoothScroll() {
|
| 96 |
-
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
| 97 |
-
anchor.addEventListener('click', function (e) {
|
| 98 |
-
e.preventDefault();
|
| 99 |
-
document.querySelector(this.getAttribute('href')).scrollIntoView({
|
| 100 |
-
behavior: 'smooth'
|
| 101 |
-
});
|
| 102 |
-
});
|
| 103 |
-
});
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
//
|
| 107 |
-
function initDropoutViz() {
|
| 108 |
-
const grid = document.getElementById('dropout-grid');
|
| 109 |
-
if (!grid) return;
|
| 110 |
-
for (let pass = 0; pass < 20; pass++) {
|
| 111 |
-
const col = document.createElement('div');
|
| 112 |
-
col.className = 'dropout-col';
|
| 113 |
-
for (let neuron = 0; neuron < 12; neuron++) {
|
| 114 |
-
const cell = document.createElement('div');
|
| 115 |
-
cell.className = 'dropout-cell';
|
| 116 |
-
const active = Math.random() > 0.15;
|
| 117 |
-
cell.style.background = active ? 'var(--primary)' : 'rgba(192, 193, 255, 0.05)';
|
| 118 |
-
cell.style.border = active ? 'none' : '1px solid rgba(192, 193, 255, 0.1)';
|
| 119 |
-
col.appendChild(cell);
|
| 120 |
-
}
|
| 121 |
-
grid.appendChild(col);
|
| 122 |
-
}
|
| 123 |
-
// Animate dropout
|
| 124 |
-
setInterval(() => {
|
| 125 |
-
grid.querySelectorAll('.dropout-cell').forEach(cell => {
|
| 126 |
-
const active = Math.random() > 0.15;
|
| 127 |
-
cell.style.background = active ? 'var(--primary)' : 'rgba(192, 193, 255, 0.05)';
|
| 128 |
-
cell.style.border = active ? 'none' : '1px solid rgba(192, 193, 255, 0.1)';
|
| 129 |
-
});
|
| 130 |
-
}, 2000);
|
| 131 |
-
}
|
| 132 |
-
|
| 133 |
-
//
|
| 134 |
-
function initScrollAnimations() {
|
| 135 |
-
const observer = new IntersectionObserver((entries) => {
|
| 136 |
-
entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('visible'); });
|
| 137 |
-
}, { threshold: 0.1 });
|
| 138 |
-
document.querySelectorAll('.section-header, .stat-card, .arch-stage, .bench-card, .ops-card').forEach(el => {
|
| 139 |
-
el.classList.add('fade-in');
|
| 140 |
-
observer.observe(el);
|
| 141 |
-
});
|
| 142 |
-
}
|
| 143 |
-
|
| 144 |
-
//
|
| 145 |
-
async function checkAPI() {
|
| 146 |
-
for (const candidate of apiCandidates()) {
|
| 147 |
-
try {
|
| 148 |
-
const res = await fetch(`${candidate}/health`, { signal: AbortSignal.timeout(2000) });
|
| 149 |
-
if (!res.ok) continue;
|
| 150 |
-
API_BASE = candidate;
|
| 151 |
-
apiOnline = true;
|
| 152 |
-
const statusEl = document.querySelector('.status-text');
|
| 153 |
-
if (statusEl) statusEl.textContent = API_BASE === window.location.origin ? 'API Connected' : 'Fixed API Connected';
|
| 154 |
-
return;
|
| 155 |
-
} catch {
|
| 156 |
-
// Try the next candidate before falling back to demo mode.
|
| 157 |
-
}
|
| 158 |
-
}
|
| 159 |
-
apiOnline = false;
|
| 160 |
-
const statusEl = document.querySelector('.status-text');
|
| 161 |
-
if (statusEl) statusEl.textContent = 'Demo Mode';
|
| 162 |
-
}
|
| 163 |
-
|
| 164 |
-
//
|
| 165 |
-
async function updateLiveMetrics() {
|
| 166 |
-
if (!apiOnline) return;
|
| 167 |
-
try {
|
| 168 |
-
const res = await fetch(`${API_BASE}/metrics`);
|
| 169 |
-
const data = await res.json();
|
| 170 |
-
|
| 171 |
-
document.getElementById('live-model').textContent = data.model;
|
| 172 |
-
document.getElementById('live-total').textContent = data.total_requests;
|
| 173 |
-
|
| 174 |
-
const dist = data.routing_distribution;
|
| 175 |
-
document.getElementById('dist-route').style.width = dist.route_pct + '%';
|
| 176 |
-
document.getElementById('dist-clarify').style.width = dist.clarify_pct + '%';
|
| 177 |
-
document.getElementById('dist-escalate').style.width = dist.escalate_pct + '%';
|
| 178 |
-
} catch (err) {
|
| 179 |
-
console.warn('Metrics update failed:', err);
|
| 180 |
-
}
|
| 181 |
-
}
|
| 182 |
-
|
| 183 |
-
//
|
| 184 |
-
async function routeTicket(extraPayload = {}) {
|
| 185 |
-
const text = document.getElementById('ticket-input').value.trim();
|
| 186 |
-
if (!text) return;
|
| 187 |
-
|
| 188 |
-
const btn = document.getElementById('route-btn');
|
| 189 |
-
btn.innerHTML = '<span class="spinner"></span> Routing...';
|
| 190 |
-
btn.disabled = true;
|
| 191 |
-
|
| 192 |
-
let result;
|
| 193 |
-
try {
|
| 194 |
-
if (apiOnline) {
|
| 195 |
-
const res = await fetch(`${API_BASE}/route`, {
|
| 196 |
-
method: 'POST',
|
| 197 |
-
headers: { 'Content-Type': 'application/json' },
|
| 198 |
-
body: JSON.stringify({ text, ...extraPayload }),
|
| 199 |
-
});
|
| 200 |
-
result = await res.json();
|
| 201 |
-
} else {
|
| 202 |
-
result = simulateRouting(text, extraPayload);
|
| 203 |
-
}
|
| 204 |
-
displayResult(result, text);
|
| 205 |
-
} catch (err) {
|
| 206 |
-
result = simulateRouting(text, extraPayload);
|
| 207 |
-
displayResult(result, text);
|
| 208 |
-
}
|
| 209 |
-
|
| 210 |
-
btn.innerHTML = '
|
| 211 |
-
btn.disabled = false;
|
| 212 |
-
}
|
| 213 |
-
|
| 214 |
-
//
|
| 215 |
-
function displayResult(r, routedText) {
|
| 216 |
-
// Handle edge cases
|
| 217 |
-
if (r.action === 'invalid_input') {
|
| 218 |
-
document.getElementById('result-placeholder').style.display = 'none';
|
| 219 |
-
const content = document.getElementById('result-content');
|
| 220 |
-
content.style.display = 'block';
|
| 221 |
-
|
| 222 |
-
const badge = document.getElementById('action-badge');
|
| 223 |
-
badge.textContent = r.error_type.toUpperCase().replace('_', ' ');
|
| 224 |
-
badge.className = 'action-badge clarify'; // yellow
|
| 225 |
-
|
| 226 |
-
document.getElementById('action-queue').textContent = r.response;
|
| 227 |
-
document.getElementById('result-reason').textContent = r.response;
|
| 228 |
-
|
| 229 |
-
// Hide gauges for invalid input
|
| 230 |
-
document.querySelector('.gauge-row').style.display = 'none';
|
| 231 |
-
document.querySelector('.signals-grid').style.display = 'none';
|
| 232 |
-
document.getElementById('prob-chart').innerHTML = '';
|
| 233 |
-
document.getElementById('clarification-box').style.display = 'none';
|
| 234 |
-
const evidenceGrid = document.getElementById('signal-evidence-grid');
|
| 235 |
-
if (evidenceGrid) evidenceGrid.style.display = 'none';
|
| 236 |
-
const explainBtn = document.getElementById('explain-btn');
|
| 237 |
-
if (explainBtn) explainBtn.style.display = 'none';
|
| 238 |
-
document.getElementById('explanation-box').style.display = 'none';
|
| 239 |
-
return;
|
| 240 |
-
}
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
// Show gauges for valid input
|
| 244 |
-
document.querySelector('.gauge-row').style.display = 'grid';
|
| 245 |
-
document.querySelector('.signals-grid').style.display = 'grid';
|
| 246 |
-
|
| 247 |
-
document.getElementById('result-placeholder').style.display = 'none';
|
| 248 |
-
const content = document.getElementById('result-content');
|
| 249 |
-
content.style.display = 'block';
|
| 250 |
-
const evidenceGrid = document.getElementById('signal-evidence-grid');
|
| 251 |
-
if (evidenceGrid) evidenceGrid.style.display = 'grid';
|
| 252 |
-
|
| 253 |
-
// Action Badge Logic
|
| 254 |
-
const badge = document.getElementById('action-badge');
|
| 255 |
-
const queue = document.getElementById('action-queue');
|
| 256 |
-
|
| 257 |
-
if (r.action === 'multi_route') {
|
| 258 |
-
badge.textContent = 'MULTI-ROUTE';
|
| 259 |
-
badge.className = 'action-badge';
|
| 260 |
-
badge.style.background = 'linear-gradient(90deg, var(--primary), var(--accent))';
|
| 261 |
-
queue.innerHTML = `
|
| 262 |
-
<div style="display: flex; gap: 8px; margin-top: 4px;">
|
| 263 |
-
<span class="tech-tag" style="background: rgba(192, 193, 255, 0.2)">Primary: ${r.primary_queue}</span>
|
| 264 |
-
<span class="tech-tag" style="background: rgba(255, 255, 255, 0.1)">Secondary: ${r.secondary_queue}</span>
|
| 265 |
-
</div>
|
| 266 |
-
`;
|
| 267 |
-
} else {
|
| 268 |
-
badge.textContent = r.action.toUpperCase();
|
| 269 |
-
badge.className = `action-badge ${r.action}`;
|
| 270 |
-
queue.textContent = r.action === 'route' ? `
|
| 271 |
-
r.action === 'clarify' ? 'Needs 1 clarification question' : 'Immediate human triage';
|
| 272 |
-
}
|
| 273 |
-
|
| 274 |
-
// Gauges
|
| 275 |
-
const confPct = Math.min(r.confidence * 100, 100);
|
| 276 |
-
document.getElementById('conf-fill').style.width = confPct + '%';
|
| 277 |
-
document.getElementById('conf-value').textContent = r.confidence.toFixed(4);
|
| 278 |
-
const maxEnt = Math.log(8);
|
| 279 |
-
const entPct = Math.min((r.entropy / maxEnt) * 100, 100);
|
| 280 |
-
document.getElementById('ent-fill').style.width = entPct + '%';
|
| 281 |
-
document.getElementById('ent-value').textContent = r.entropy.toFixed(4);
|
| 282 |
-
if (r.margin !== undefined && document.getElementById('margin-value')) {
|
| 283 |
-
document.getElementById('margin-value').textContent = r.margin.toFixed(4);
|
| 284 |
-
}
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
// Prob chart
|
| 288 |
-
const chart = document.getElementById('prob-chart');
|
| 289 |
-
chart.innerHTML = '';
|
| 290 |
-
const probs = r.all_probs || {};
|
| 291 |
-
const sorted = Object.entries(probs).sort((a, b) => b[1] - a[1]);
|
| 292 |
-
const maxProb = sorted.length ? sorted[0][1] : 1;
|
| 293 |
-
sorted.forEach(([cat, prob]) => {
|
| 294 |
-
const row = document.createElement('div');
|
| 295 |
-
row.className = 'prob-row';
|
| 296 |
-
const pct = (prob / Math.max(maxProb, 0.01)) * 100;
|
| 297 |
-
row.innerHTML = `
|
| 298 |
-
<span class="prob-label">${cat.replace(/_/g, ' ')}</span>
|
| 299 |
-
<div class="prob-bar-track"><div class="prob-bar-fill" style="width:${pct}%;background:${CAT_COLORS[cat] || '#6366f1'}"></div></div>
|
| 300 |
-
<span class="prob-val">${(prob * 100).toFixed(1)}%</span>`;
|
| 301 |
-
chart.appendChild(row);
|
| 302 |
-
});
|
| 303 |
-
|
| 304 |
-
// Clarification
|
| 305 |
-
const clarBox = document.getElementById('clarification-box');
|
| 306 |
-
if (r.action === 'clarify' && r.clarification) {
|
| 307 |
-
clarBox.style.display = 'block';
|
| 308 |
-
document.getElementById('clarify-question').textContent = r.clarification.question_text;
|
| 309 |
-
const optEl = document.getElementById('clarify-options');
|
| 310 |
-
optEl.innerHTML = '';
|
| 311 |
-
const optionTargets = r.clarification.option_targets || [];
|
| 312 |
-
(r.clarification.options || []).forEach((o, index) => {
|
| 313 |
-
const btn = document.createElement('button');
|
| 314 |
-
btn.className = 'option-btn';
|
| 315 |
-
btn.textContent = o;
|
| 316 |
-
btn.onclick = () => {
|
| 317 |
-
// Provide visual feedback
|
| 318 |
-
document.querySelectorAll('#clarify-options .option-btn').forEach(b => b.disabled = true);
|
| 319 |
-
btn.style.background = 'var(--primary)';
|
| 320 |
-
btn.style.color = '#fff';
|
| 321 |
-
|
| 322 |
-
const target = optionTargets[index]
|
| 323 |
-
|| inferClarificationTarget(o, r.clarification.relevant_classes || r.top_two_classes || [], index);
|
| 324 |
-
|
| 325 |
-
// Keep the selection visible and machine-readable for repeat manual runs.
|
| 326 |
-
const input = document.getElementById('ticket-input');
|
| 327 |
-
input.value = input.value.trim() + '\n\n[Clarification: ' + target + ' - ' + o + ']';
|
| 328 |
-
|
| 329 |
-
// Re-route with new context after a short delay
|
| 330 |
-
setTimeout(() => {
|
| 331 |
-
routeTicket({
|
| 332 |
-
clarification_choice: o,
|
| 333 |
-
clarification_target: target,
|
| 334 |
-
clarification_question_id: r.clarification.question_id,
|
| 335 |
-
});
|
| 336 |
-
}, 800);
|
| 337 |
-
};
|
| 338 |
-
optEl.appendChild(btn);
|
| 339 |
-
});
|
| 340 |
-
|
| 341 |
-
// Remove existing badge if any
|
| 342 |
-
const existingBadge = document.getElementById('source-badge');
|
| 343 |
-
if (existingBadge) existingBadge.remove();
|
| 344 |
-
|
| 345 |
-
// After displaying the question, add source badge
|
| 346 |
-
const sourceBadge = document.createElement('div');
|
| 347 |
-
sourceBadge.id = 'source-badge';
|
| 348 |
-
sourceBadge.style.cssText = 'font-size:11px;margin-top:8px;opacity:0.6;';
|
| 349 |
-
sourceBadge.textContent = r.clarification.source === 'llm_groq'
|
| 350 |
-
? '
|
| 351 |
-
: '
|
| 352 |
-
document.getElementById('clarification-box').appendChild(sourceBadge);
|
| 353 |
-
|
| 354 |
-
document.getElementById('clarify-gain').textContent =
|
| 355 |
-
`Expected information gain: ${r.clarification.expected_gain?.toFixed(4) || 'N/A'}`;
|
| 356 |
-
} else {
|
| 357 |
-
clarBox.style.display = 'none';
|
| 358 |
-
}
|
| 359 |
-
|
| 360 |
-
// Signals
|
| 361 |
-
const slaRiskVal = r.sla_risk || r.sla_breach_probability || 0;
|
| 362 |
-
const slaPct = slaRiskVal * 100;
|
| 363 |
-
document.getElementById('sla-value').textContent = slaPct.toFixed(1) + '%';
|
| 364 |
-
document.getElementById('sla-fill').style.width = slaPct + '%';
|
| 365 |
-
document.getElementById('sla-fill').style.background =
|
| 366 |
-
slaPct > 65 ? 'var(--red)' : slaPct > 35 ? 'var(--yellow)' : 'var(--green)';
|
| 367 |
-
|
| 368 |
-
const feat = r.features || {};
|
| 369 |
-
const sent = feat.sentiment_score;
|
| 370 |
-
const sentLabel = feat.sentiment_label || sentimentLabelFromScore(sent);
|
| 371 |
-
const sentimentValue = document.getElementById('sentiment-value');
|
| 372 |
-
sentimentValue.textContent = sentLabel ? sentLabel.toUpperCase() : '
|
| 373 |
-
sentimentValue.style.color = sentimentColor(sentLabel, sent);
|
| 374 |
-
const sentimentScore = document.getElementById('sentiment-score');
|
| 375 |
-
if (sentimentScore) {
|
| 376 |
-
const raw = typeof feat.sentiment_raw_score === 'number'
|
| 377 |
-
? ` raw ${feat.sentiment_raw_score.toFixed(2)}`
|
| 378 |
-
: '';
|
| 379 |
-
sentimentScore.textContent = sent !== undefined ? `score ${sent.toFixed(2)}${raw}` : '
|
| 380 |
-
}
|
| 381 |
-
|
| 382 |
-
const urgScore = numericValue(r.urgency_score, feat.urgency_score, 0);
|
| 383 |
-
const urgLevel = feat.urgency_level || urgencyLevelFromScore(urgScore);
|
| 384 |
-
const urgencyCard = document.getElementById('urgency-value').parentElement;
|
| 385 |
-
const urgencyValue = document.getElementById('urgency-value');
|
| 386 |
-
urgencyValue.textContent = urgLevel.toUpperCase();
|
| 387 |
-
urgencyValue.style.color = urgencyColor(urgLevel);
|
| 388 |
-
const urgencyScore = document.getElementById('urgency-score');
|
| 389 |
-
if (urgencyScore) urgencyScore.textContent = `score ${urgScore.toFixed(2)}`;
|
| 390 |
-
|
| 391 |
-
if (urgLevel === 'critical') {
|
| 392 |
-
urgencyCard.style.border = '1px solid var(--red)';
|
| 393 |
-
urgencyCard.style.boxShadow = '0 0 15px rgba(248, 113, 113, 0.2)';
|
| 394 |
-
} else if (urgLevel === 'high') {
|
| 395 |
-
urgencyCard.style.border = '1px solid var(--yellow)';
|
| 396 |
-
urgencyCard.style.boxShadow = '';
|
| 397 |
-
} else {
|
| 398 |
-
urgencyCard.style.border = '';
|
| 399 |
-
urgencyCard.style.boxShadow = '';
|
| 400 |
-
}
|
| 401 |
-
|
| 402 |
-
renderEvidenceList('urgency-evidence-list', feat.urgency_evidence || []);
|
| 403 |
-
renderEvidenceList('sentiment-evidence-list', feat.sentiment_evidence || []);
|
| 404 |
-
|
| 405 |
-
document.getElementById('latency-value').textContent =
|
| 406 |
-
r.latency_ms ? r.latency_ms + 'ms' : '
|
| 407 |
-
|
| 408 |
-
// Reason
|
| 409 |
-
let decisionReason = '';
|
| 410 |
-
if (r.clarification_applied) {
|
| 411 |
-
decisionReason = `Clarification answer applied: <strong>${escapeHtml(r.clarification_choice || r.top_category)}</strong>. Routing to <strong>${r.top_category}</strong> without asking another question.`;
|
| 412 |
-
} else if (r.action === 'multi_route') {
|
| 413 |
-
decisionReason = `Multiple distinct intents detected in the request. Primary intent is <strong>${r.primary_queue}</strong>, secondary is <strong>${r.secondary_queue}</strong>.`;
|
| 414 |
-
} else if (r.action === 'clarify') {
|
| 415 |
-
decisionReason = `Model uncertainty is high (entropy: ${r.entropy.toFixed(3)}) or the top two classes are too close (margin: ${r.margin?.toFixed(3)}). A clarification question was generated to refine the intent.`;
|
| 416 |
-
} else if (r.action === 'escalate') {
|
| 417 |
-
decisionReason = `Low model confidence detected (${(r.confidence * 100).toFixed(1)}%). Routing directly to human experts to ensure accuracy.`;
|
| 418 |
-
} else {
|
| 419 |
-
decisionReason = `High-confidence intent detected: <strong>${r.top_category}</strong>. Automatically routing to specialized queue.`;
|
| 420 |
-
}
|
| 421 |
-
|
| 422 |
-
document.getElementById('result-reason').innerHTML = `
|
| 423 |
-
<div style="padding: 12px; background: rgba(192, 193, 255, 0.05); border: 1px solid rgba(192, 193, 255, 0.1); border-radius: 8px; margin-top: 16px;">
|
| 424 |
-
<div style="font-size: 11px; text-transform: uppercase; color: var(--primary); margin-bottom: 8px; font-weight: 600;">Decision Reason</div>
|
| 425 |
-
<div style="font-size: 13px; color: var(--on-surface-variant); line-height: 1.5;">${decisionReason}</div>
|
| 426 |
-
</div>
|
| 427 |
-
`;
|
| 428 |
-
|
| 429 |
-
// Show explain button for valid input
|
| 430 |
-
const explainBtn = document.getElementById('explain-btn');
|
| 431 |
-
if (explainBtn) {
|
| 432 |
-
explainBtn.style.display = 'flex';
|
| 433 |
-
explainBtn.dataset.text = routedText || document.getElementById('ticket-input').value;
|
| 434 |
-
explainBtn.dataset.category = r.top_category;
|
| 435 |
-
}
|
| 436 |
-
document.getElementById('explanation-box').style.display = 'none';
|
| 437 |
-
}
|
| 438 |
-
|
| 439 |
-
//
|
| 440 |
-
async function explainDecision() {
|
| 441 |
-
const btn = document.getElementById('explain-btn');
|
| 442 |
-
const text = btn.dataset.text;
|
| 443 |
-
const targetClass = btn.dataset.category;
|
| 444 |
-
|
| 445 |
-
btn.innerHTML = '<span class="spinner"></span> Analyzing tokens...';
|
| 446 |
-
btn.disabled = true;
|
| 447 |
-
|
| 448 |
-
try {
|
| 449 |
-
let result;
|
| 450 |
-
if (apiOnline) {
|
| 451 |
-
const res = await fetch(`${API_BASE}/explain`, {
|
| 452 |
-
method: 'POST',
|
| 453 |
-
headers: { 'Content-Type': 'application/json' },
|
| 454 |
-
body: JSON.stringify({ text, target_class: targetClass }),
|
| 455 |
-
});
|
| 456 |
-
if (!res.ok) throw new Error(`Explain API returned ${res.status}`);
|
| 457 |
-
result = await res.json();
|
| 458 |
-
} else {
|
| 459 |
-
// Simulate SHAP for demo mode
|
| 460 |
-
result = simulateSHAP(text);
|
| 461 |
-
}
|
| 462 |
-
|
| 463 |
-
renderSHAP(result);
|
| 464 |
-
} catch (err) {
|
| 465 |
-
console.error('SHAP failed:', err);
|
| 466 |
-
renderSHAP(simulateSHAP(text));
|
| 467 |
-
}
|
| 468 |
-
|
| 469 |
-
btn.innerHTML = '<span class="material-symbols-outlined btn-icon">query_stats</span> Analyze Decision';
|
| 470 |
-
btn.disabled = false;
|
| 471 |
-
}
|
| 472 |
-
|
| 473 |
-
function renderSHAP(data) {
|
| 474 |
-
const box = document.getElementById('explanation-box');
|
| 475 |
-
const textEl = document.getElementById('explain-text');
|
| 476 |
-
box.style.display = 'block';
|
| 477 |
-
textEl.innerHTML = '';
|
| 478 |
-
|
| 479 |
-
if (data.error) {
|
| 480 |
-
textEl.textContent = 'Error generating explanation: ' + data.error;
|
| 481 |
-
return;
|
| 482 |
-
}
|
| 483 |
-
|
| 484 |
-
const source = document.createElement('div');
|
| 485 |
-
source.className = 'explain-source';
|
| 486 |
-
source.textContent = data.source === 'shap_transformer'
|
| 487 |
-
? 'Transformer SHAP explanation'
|
| 488 |
-
: 'Keyword evidence fallback';
|
| 489 |
-
if (data.note) source.title = data.note;
|
| 490 |
-
textEl.appendChild(source);
|
| 491 |
-
|
| 492 |
-
const tokens = data.tokens || [];
|
| 493 |
-
const values = data.values || [];
|
| 494 |
-
|
| 495 |
-
tokens.forEach((token, i) => {
|
| 496 |
-
const val = values[i];
|
| 497 |
-
const span = document.createElement('span');
|
| 498 |
-
span.className = 'shap-token';
|
| 499 |
-
span.textContent = token.replace('##', ''); // Simple handling for subwords
|
| 500 |
-
|
| 501 |
-
// Normalize opacity based on value
|
| 502 |
-
const absVal = Math.abs(val);
|
| 503 |
-
const opacity = Math.min(absVal * 5, 0.8); // Scale for visibility
|
| 504 |
-
|
| 505 |
-
if (val > 0) {
|
| 506 |
-
span.style.background = `rgba(74, 222, 128, ${opacity})`;
|
| 507 |
-
span.style.borderBottom = `2px solid rgba(74, 222, 128, ${opacity + 0.2})`;
|
| 508 |
-
} else if (val < 0) {
|
| 509 |
-
span.style.background = `rgba(248, 113, 113, ${opacity})`;
|
| 510 |
-
span.style.borderBottom = `2px solid rgba(248, 113, 113, ${opacity + 0.2})`;
|
| 511 |
-
}
|
| 512 |
-
|
| 513 |
-
textEl.appendChild(span);
|
| 514 |
-
textEl.appendChild(document.createTextNode(' '));
|
| 515 |
-
});
|
| 516 |
-
|
| 517 |
-
box.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
| 518 |
-
}
|
| 519 |
-
|
| 520 |
-
function simulateSHAP(text) {
|
| 521 |
-
const tokens = text.split(/\s+/);
|
| 522 |
-
const values = tokens.map(() => (Math.random() - 0.4) * 0.2);
|
| 523 |
-
return { tokens, values, source: 'demo_simulated' };
|
| 524 |
-
}
|
| 525 |
-
|
| 526 |
-
function numericValue(...values) {
|
| 527 |
-
for (const value of values) {
|
| 528 |
-
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
| 529 |
-
}
|
| 530 |
-
return 0;
|
| 531 |
-
}
|
| 532 |
-
|
| 533 |
-
function sentimentLabelFromScore(score) {
|
| 534 |
-
if (typeof score !== 'number') return null;
|
| 535 |
-
if (score <= -0.55) return 'frustrated';
|
| 536 |
-
if (score <= -0.2) return 'concerned';
|
| 537 |
-
if (score >= 0.3) return 'positive';
|
| 538 |
-
return 'neutral';
|
| 539 |
-
}
|
| 540 |
-
|
| 541 |
-
function sentimentColor(label, score) {
|
| 542 |
-
const normalized = (label || sentimentLabelFromScore(score) || '').toLowerCase();
|
| 543 |
-
if (normalized === 'frustrated') return 'var(--red)';
|
| 544 |
-
if (normalized === 'concerned') return 'var(--yellow)';
|
| 545 |
-
if (normalized === 'positive') return 'var(--green)';
|
| 546 |
-
return 'var(--text)';
|
| 547 |
-
}
|
| 548 |
-
|
| 549 |
-
function urgencyLevelFromScore(score) {
|
| 550 |
-
if (score >= 0.75) return 'critical';
|
| 551 |
-
if (score >= 0.5) return 'high';
|
| 552 |
-
if (score >= 0.25) return 'medium';
|
| 553 |
-
return 'low';
|
| 554 |
-
}
|
| 555 |
-
|
| 556 |
-
function urgencyColor(level) {
|
| 557 |
-
const normalized = (level || '').toLowerCase();
|
| 558 |
-
if (normalized === 'critical') return 'var(--red)';
|
| 559 |
-
if (normalized === 'high' || normalized === 'medium') return 'var(--yellow)';
|
| 560 |
-
return 'var(--green)';
|
| 561 |
-
}
|
| 562 |
-
|
| 563 |
-
function escapeHtml(value) {
|
| 564 |
-
return String(value)
|
| 565 |
-
.replace(/&/g, '&')
|
| 566 |
-
.replace(/</g, '<')
|
| 567 |
-
.replace(/>/g, '>')
|
| 568 |
-
.replace(/"/g, '"')
|
| 569 |
-
.replace(/'/g, ''');
|
| 570 |
-
}
|
| 571 |
-
|
| 572 |
-
function renderEvidenceList(elementId, evidence) {
|
| 573 |
-
const list = document.getElementById(elementId);
|
| 574 |
-
if (!list) return;
|
| 575 |
-
|
| 576 |
-
const items = Array.isArray(evidence) ? evidence.filter(Boolean) : [];
|
| 577 |
-
if (!items.length) {
|
| 578 |
-
list.innerHTML = '<div class="evidence-empty">No contextual evidence triggered.</div>';
|
| 579 |
-
return;
|
| 580 |
-
}
|
| 581 |
-
|
| 582 |
-
list.innerHTML = items.slice(0, 5).map(item => {
|
| 583 |
-
const [rawType, ...phraseParts] = String(item).split(':');
|
| 584 |
-
const type = rawType ? rawType.replace(/_/g, ' ') : 'signal';
|
| 585 |
-
const phrase = phraseParts.join(':').trim() || item;
|
| 586 |
-
return `
|
| 587 |
-
<div class="evidence-item">
|
| 588 |
-
<span class="evidence-type">${escapeHtml(type)}</span>
|
| 589 |
-
<span class="evidence-phrase">${escapeHtml(phrase)}</span>
|
| 590 |
-
</div>
|
| 591 |
-
`;
|
| 592 |
-
}).join('');
|
| 593 |
-
}
|
| 594 |
-
|
| 595 |
-
function inferClarificationTarget(option, relevantClasses, index) {
|
| 596 |
-
const optionLow = String(option || '').toLowerCase();
|
| 597 |
-
const keywordTargets = [
|
| 598 |
-
['billing', ['billing', 'invoice', 'payment', 'charge', 'refund', 'credit', 'pricing', 'cost', 'bill']],
|
| 599 |
-
['technical_support', ['software', 'error', 'technical', 'broken', 'malfunction', 'functionality', 'api', 'integration', 'performance', 'specific issue', 'data movement']],
|
| 600 |
-
['account_management', ['account', 'plan', 'subscription', 'administrator', 'admin', 'user management', 'regular user', 'settings']],
|
| 601 |
-
['feature_request', ['new capability', 'feature', 'request', 'enhancement']],
|
| 602 |
-
['compliance_legal', ['compliance', 'regulatory', 'audit', 'gdpr', 'security', 'data affected']],
|
| 603 |
-
['onboarding', ['new user', 'onboarding', 'guidance', 'training', 'walkthrough', 'setting up']],
|
| 604 |
-
['churn_risk', ['continuing', 'switching', 'evaluating options', 'mostly negative']],
|
| 605 |
-
['general_inquiry', ['general', 'guidance', 'not urgent', 'no specific deadline', 'positive']],
|
| 606 |
-
];
|
| 607 |
-
|
| 608 |
-
for (const [category, keywords] of keywordTargets) {
|
| 609 |
-
if (keywords.some(keyword => optionLow.includes(keyword))) return category;
|
| 610 |
-
}
|
| 611 |
-
return relevantClasses[index] || relevantClasses[0] || 'general_inquiry';
|
| 612 |
-
}
|
| 613 |
-
|
| 614 |
-
function firstPatternHit(text, patterns) {
|
| 615 |
-
for (const pattern of patterns) {
|
| 616 |
-
const match = text.match(pattern);
|
| 617 |
-
if (match) return match[0];
|
| 618 |
-
}
|
| 619 |
-
return null;
|
| 620 |
-
}
|
| 621 |
-
|
| 622 |
-
function inferDemoSignals(t) {
|
| 623 |
-
const urgencySpecs = [
|
| 624 |
-
['business_impact', 0.30, [
|
| 625 |
-
/\b(?:affecting|impacting|blocking)\s+(?:our\s+)?(?:customers|users|team|business|operations|sales|revenue|payroll|launch|production)\b/,
|
| 626 |
-
/\b(?:customers?|clients?)\s+(?:(?:are|is)\s+)?(?:waiting|blocked|affected|unable)\b/,
|
| 627 |
-
/\b(?:cannot|can't|unable to)\s+(?:process|ship|launch|serve|sell|invoice|onboard|work|access)\b/,
|
| 628 |
-
]],
|
| 629 |
-
['deadline_pressure', 0.25, [
|
| 630 |
-
/\b(?:in|within)\s+\d+\s*(?:min|mins|minutes|hour|hours|hrs|days?)\b/,
|
| 631 |
-
/\b(?:by|before)\s+(?:today|tomorrow|eod|end of day|tonight|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/,
|
| 632 |
-
/\b(?:launch|demo|go-live|renewal|payroll|board meeting|presentation)\b/,
|
| 633 |
-
]],
|
| 634 |
-
['production_outage', 0.40, [
|
| 635 |
-
/\bproduction\s+(?:is\s+)?(?:down|blocked|broken|failing|impacted)\b/,
|
| 636 |
-
/\b(?:all|multiple|many)\s+(?:users|customers|accounts|teams)\s+(?:are\s+)?(?:affected|blocked|down|unable)\b/,
|
| 637 |
-
/\b(?:system|service|platform|dashboard|api)\s+(?:is\s+)?(?:down|unavailable|not responding)\b/,
|
| 638 |
-
]],
|
| 639 |
-
['access_loss', 0.25, [
|
| 640 |
-
/\b(?:locked out|cannot access|can't access|unable to access|access is blocked)\b/,
|
| 641 |
-
/\b(?:login|sso|authentication)\s+(?:is\s+)?(?:broken|failing|down|not working)\b/,
|
| 642 |
-
]],
|
| 643 |
-
['repeat_issue', 0.20, [
|
| 644 |
-
/\b(?:again|still|keeps?|repeated|recurring)\b/,
|
| 645 |
-
/\b(?:second|third|fourth)\s+time\b/,
|
| 646 |
-
/\b(?:raised|reported|opened)\s+(?:this\s+)?(?:before|multiple times|again)\b/,
|
| 647 |
-
]],
|
| 648 |
-
];
|
| 649 |
-
|
| 650 |
-
const explicitCritical = ['crash', 'blocked', 'down', 'failing', 'cannot access', 'production issue', 'outage', 'emergency', 'critical', 'urgent', 'immediately', 'blocking', 'locked out'];
|
| 651 |
-
const explicitGeneral = ['asap', 'deadline', 'sla', 'escalate', 'priority', 'time-sensitive', 'showstopper', 'presentation'];
|
| 652 |
-
|
| 653 |
-
const urgencyEvidence = [];
|
| 654 |
-
const urgencyFlags = [];
|
| 655 |
-
explicitCritical.forEach(word => {
|
| 656 |
-
if (t.includes(word)) {
|
| 657 |
-
urgencyEvidence.push(`explicit_critical: ${word}`);
|
| 658 |
-
urgencyFlags.push(word);
|
| 659 |
-
}
|
| 660 |
-
});
|
| 661 |
-
explicitGeneral.forEach(word => {
|
| 662 |
-
if (t.includes(word)) {
|
| 663 |
-
urgencyEvidence.push(`explicit_general: ${word}`);
|
| 664 |
-
urgencyFlags.push(word);
|
| 665 |
-
}
|
| 666 |
-
});
|
| 667 |
-
|
| 668 |
-
let urgencyScore = (explicitCritical.length ? 0 : 0);
|
| 669 |
-
urgencyScore += explicitCritical.filter(word => t.includes(word)).length * 0.25;
|
| 670 |
-
urgencyScore += explicitGeneral.filter(word => t.includes(word)).length * 0.12;
|
| 671 |
-
urgencySpecs.forEach(([label, weight, patterns]) => {
|
| 672 |
-
const phrase = firstPatternHit(t, patterns);
|
| 673 |
-
if (phrase) {
|
| 674 |
-
urgencyScore += weight;
|
| 675 |
-
urgencyEvidence.push(`${label}: ${phrase}`);
|
| 676 |
-
urgencyFlags.push(label);
|
| 677 |
-
}
|
| 678 |
-
});
|
| 679 |
-
|
| 680 |
-
if (/\b(?:not urgent|no rush|whenever you can|when you have time)\b/.test(t)) {
|
| 681 |
-
urgencyScore = Math.min(urgencyScore, 0.35);
|
| 682 |
-
urgencyEvidence.push('deescalation: no immediate pressure');
|
| 683 |
-
}
|
| 684 |
-
|
| 685 |
-
urgencyScore = Math.max(0, Math.min(1, urgencyScore));
|
| 686 |
-
|
| 687 |
-
const sentimentSpecs = [
|
| 688 |
-
['frustration', -0.30, [
|
| 689 |
-
/\bfrustrat(?:ed|ing|ion)\b/,
|
| 690 |
-
/\bnot happy\b/,
|
| 691 |
-
/\bdisappoint(?:ed|ing|ment)\b/,
|
| 692 |
-
/\bthis is becoming difficult\b/,
|
| 693 |
-
/\bnot ideal\b/,
|
| 694 |
-
/\bunacceptable\b/,
|
| 695 |
-
/\bterrible\b/,
|
| 696 |
-
/\bawful\b/,
|
| 697 |
-
]],
|
| 698 |
-
['trust_risk', -0.25, [
|
| 699 |
-
/\b(?:losing|lost)\s+(?:trust|confidence)\b/,
|
| 700 |
-
/\b(?:considering|thinking about)\s+(?:switching|leaving|cancelling|canceling)\b/,
|
| 701 |
-
]],
|
| 702 |
-
['polite_negative', -0.22, [
|
| 703 |
-
/\b(?:this|it)\s+is\s+(?:affecting|impacting|blocking)\b/,
|
| 704 |
-
/\b(?:could you please|please)\b.*\b(?:fix|resolve|help)\b.*\b(?:blocking|affecting|stuck|broken|failing)\b/,
|
| 705 |
-
/\b(?:becoming|getting)\s+(?:difficult|hard|painful)\b/,
|
| 706 |
-
]],
|
| 707 |
-
];
|
| 708 |
-
|
| 709 |
-
const negWords = ['frustrated','broken','terrible','angry','worst','cancel','bad','issue','error', 'invalid', 'locked out'];
|
| 710 |
-
const posWords = ['great','thanks','love','good','happy','please'];
|
| 711 |
-
let rawSentiment = 0;
|
| 712 |
-
negWords.forEach(w => { if (t.includes(w)) rawSentiment -= 0.18; });
|
| 713 |
-
posWords.forEach(w => { if (t.includes(w)) rawSentiment += 0.12; });
|
| 714 |
-
rawSentiment = Math.max(-1, Math.min(1, rawSentiment));
|
| 715 |
-
|
| 716 |
-
const sentimentEvidence = [];
|
| 717 |
-
let sentimentScore = rawSentiment;
|
| 718 |
-
sentimentSpecs.forEach(([label, weight, patterns]) => {
|
| 719 |
-
const phrase = firstPatternHit(t, patterns);
|
| 720 |
-
if (phrase) {
|
| 721 |
-
sentimentScore += weight;
|
| 722 |
-
sentimentEvidence.push(`${label}: ${phrase}`);
|
| 723 |
-
}
|
| 724 |
-
});
|
| 725 |
-
sentimentScore = Math.max(-1, Math.min(1, sentimentScore));
|
| 726 |
-
|
| 727 |
-
return {
|
| 728 |
-
urgency_score: Math.round(urgencyScore * 10000) / 10000,
|
| 729 |
-
urgency_level: urgencyLevelFromScore(urgencyScore),
|
| 730 |
-
urgency_flags: Array.from(new Set(urgencyFlags)),
|
| 731 |
-
urgency_evidence: urgencyEvidence,
|
| 732 |
-
sentiment_score: Math.round(sentimentScore * 10000) / 10000,
|
| 733 |
-
sentiment_raw_score: Math.round(rawSentiment * 10000) / 10000,
|
| 734 |
-
sentiment_label: sentimentLabelFromScore(sentimentScore),
|
| 735 |
-
sentiment_evidence: sentimentEvidence,
|
| 736 |
-
};
|
| 737 |
-
}
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
//
|
| 741 |
-
function hashText(str) {
|
| 742 |
-
let h = 0;
|
| 743 |
-
for (let i = 0; i < str.length; i++) {
|
| 744 |
-
h = ((h << 5) - h + str.charCodeAt(i)) | 0;
|
| 745 |
-
}
|
| 746 |
-
return Math.abs(h);
|
| 747 |
-
}
|
| 748 |
-
|
| 749 |
-
function seededRandom(seed) {
|
| 750 |
-
let s = seed;
|
| 751 |
-
return function() {
|
| 752 |
-
s = (s * 1664525 + 1013904223) & 0xffffffff;
|
| 753 |
-
return (s >>> 0) / 0xffffffff;
|
| 754 |
-
};
|
| 755 |
-
}
|
| 756 |
-
|
| 757 |
-
//
|
| 758 |
-
function simulateRouting(text, extraPayload = {}) {
|
| 759 |
-
const t = text.toLowerCase().trim();
|
| 760 |
-
const marker = t.match(/\[clarification:\s*([a-z_]+)\s*-\s*([^\]]+)\]/);
|
| 761 |
-
const clarificationTarget = extraPayload.clarification_target || (marker && marker[1]);
|
| 762 |
-
const clarificationChoice = extraPayload.clarification_choice || (marker && marker[2]);
|
| 763 |
-
const validTargets = Object.keys(CAT_COLORS);
|
| 764 |
-
|
| 765 |
-
if (clarificationTarget && validTargets.includes(clarificationTarget)) {
|
| 766 |
-
const allProbs = {};
|
| 767 |
-
validTargets.forEach(cat => { allProbs[cat] = cat === clarificationTarget ? 0.9 : 0.0143; });
|
| 768 |
-
const demoSignals = inferDemoSignals(t);
|
| 769 |
-
return {
|
| 770 |
-
action: 'route',
|
| 771 |
-
confidence: 0.9,
|
| 772 |
-
entropy: 0.35,
|
| 773 |
-
margin: 0.75,
|
| 774 |
-
top_category: clarificationTarget,
|
| 775 |
-
all_probs: allProbs,
|
| 776 |
-
top_two_classes: [clarificationTarget, validTargets.find(cat => cat !== clarificationTarget)],
|
| 777 |
-
queue: clarificationTarget,
|
| 778 |
-
reason: `Clarification answer resolved the ambiguity toward ${clarificationTarget}.`,
|
| 779 |
-
clarification_applied: true,
|
| 780 |
-
clarification_choice: clarificationChoice,
|
| 781 |
-
sla_breach_probability: Math.min(0.95, 0.15 + (demoSignals.urgency_score * 0.45)),
|
| 782 |
-
urgency_score: demoSignals.urgency_score,
|
| 783 |
-
features: {
|
| 784 |
-
...demoSignals,
|
| 785 |
-
text_complexity_score: Math.round(text.split(' ').length / 5 * 100) / 100,
|
| 786 |
-
},
|
| 787 |
-
latency_ms: 28 + (hashText(t) % 20),
|
| 788 |
-
};
|
| 789 |
-
}
|
| 790 |
-
|
| 791 |
-
// Basic validation in simulation to match real API behavior
|
| 792 |
-
if (t.length < 10) {
|
| 793 |
-
const greetings = ['hi', 'hello', 'hey', 'test'];
|
| 794 |
-
if (greetings.some(g => t.startsWith(g))) {
|
| 795 |
-
return {
|
| 796 |
-
action: 'invalid_input',
|
| 797 |
-
error_type: 'greeting',
|
| 798 |
-
response: "Hi there!
|
| 799 |
-
};
|
| 800 |
-
}
|
| 801 |
-
return {
|
| 802 |
-
action: 'invalid_input',
|
| 803 |
-
error_type: 'too_short',
|
| 804 |
-
response: "Could you share a bit more detail about your issue? We're here to help."
|
| 805 |
-
};
|
| 806 |
-
}
|
| 807 |
-
|
| 808 |
-
const rng = seededRandom(hashText(t)); // deterministic per text
|
| 809 |
-
|
| 810 |
-
const scores = {
|
| 811 |
-
billing: 0.02, technical_support: 0.02, account_management: 0.02,
|
| 812 |
-
feature_request: 0.02, compliance_legal: 0.02, onboarding: 0.02,
|
| 813 |
-
general_inquiry: 0.02, churn_risk: 0.02,
|
| 814 |
-
};
|
| 815 |
-
|
| 816 |
-
// Simple keyword scoring
|
| 817 |
-
const kw = {
|
| 818 |
-
billing: ['invoice','billing','payment','charge','refund','price','cost','subscription','plan','pricing','credit'],
|
| 819 |
-
technical_support: ['error','bug','broken','crash','fix','api','endpoint','500','timeout','issue','not working','failed'],
|
| 820 |
-
account_management: ['account','user','access','permission','settings','profile','password','role'],
|
| 821 |
-
feature_request: ['feature','add','implement','suggest','request','capability','enhancement','wish','could you'],
|
| 822 |
-
compliance_legal: ['gdpr','compliance','audit','regulation','privacy','security','data protection','legal'],
|
| 823 |
-
onboarding: ['new user','setup','getting started','onboarding','first time','just signed up','configure','install'],
|
| 824 |
-
general_inquiry: ['how do','what is','question','information','help','guide','documentation'],
|
| 825 |
-
churn_risk: ['cancel','switch','competitor','alternative','frustrated','unacceptable','leaving','terminate','fed up','last straw'],
|
| 826 |
-
};
|
| 827 |
-
|
| 828 |
-
Object.entries(kw).forEach(([cat, words]) => {
|
| 829 |
-
words.forEach(w => { if (t.includes(w)) scores[cat] += 0.15 + rng() * 0.05; });
|
| 830 |
-
});
|
| 831 |
-
|
| 832 |
-
// Normalize
|
| 833 |
-
const total = Object.values(scores).reduce((a, b) => a + b, 0);
|
| 834 |
-
Object.keys(scores).forEach(k => scores[k] /= total);
|
| 835 |
-
|
| 836 |
-
// Add small deterministic noise (simulate MC Dropout variance)
|
| 837 |
-
Object.keys(scores).forEach(k => {
|
| 838 |
-
scores[k] += (rng() - 0.5) * 0.03;
|
| 839 |
-
scores[k] = Math.max(0.001, scores[k]);
|
| 840 |
-
});
|
| 841 |
-
const total2 = Object.values(scores).reduce((a, b) => a + b, 0);
|
| 842 |
-
Object.keys(scores).forEach(k => scores[k] /= total2);
|
| 843 |
-
|
| 844 |
-
const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
|
| 845 |
-
const confidence = sorted[0][1];
|
| 846 |
-
const entropy = -Object.values(scores).reduce((s, p) => s + p * Math.log(p + 1e-9), 0);
|
| 847 |
-
const topCat = sorted[0][0];
|
| 848 |
-
const topTwo = [sorted[0][0], sorted[1][0]];
|
| 849 |
-
const margin = sorted[0][1] - sorted[1][1];
|
| 850 |
-
|
| 851 |
-
let action, reason;
|
| 852 |
-
const critical_labels = ['compliance_legal', 'account_management'];
|
| 853 |
-
|
| 854 |
-
if (critical_labels.includes(topCat)) {
|
| 855 |
-
if (confidence >= 0.90 && margin >= 0.35 && entropy < 0.60) {
|
| 856 |
-
action = 'route';
|
| 857 |
-
reason = `
|
| 858 |
-
} else {
|
| 859 |
-
action = 'escalate';
|
| 860 |
-
reason = `
|
| 861 |
-
}
|
| 862 |
-
} else {
|
| 863 |
-
if (confidence >= 0.85 && margin >= 0.25 && entropy < 0.70) {
|
| 864 |
-
action = 'route';
|
| 865 |
-
reason = `
|
| 866 |
-
} else if (confidence >= 0.60 && entropy < 1.05) {
|
| 867 |
-
action = 'clarify';
|
| 868 |
-
reason = `
|
| 869 |
-
} else {
|
| 870 |
-
action = 'escalate';
|
| 871 |
-
reason = `
|
| 872 |
-
}
|
| 873 |
-
}
|
| 874 |
-
|
| 875 |
-
// Clarification question
|
| 876 |
-
let clarification = null;
|
| 877 |
-
if (action === 'clarify') {
|
| 878 |
-
const questions = {
|
| 879 |
-
'billing+technical_support': { question_text: 'Is the main issue related to (A) a software error, or (B) your billing or invoice?', options: ['Software error','Billing/invoice'], expected_gain: 0.71 },
|
| 880 |
-
'technical_support+billing': { question_text: 'Is the main issue related to (A) a software error, or (B) your billing or invoice?', options: ['Software error','Billing/invoice'], expected_gain: 0.71 },
|
| 881 |
-
'feature_request+technical_support': { question_text: 'Are you reporting something broken, or requesting a new capability?', options: ['Something broken','New feature'], expected_gain: 0.68 },
|
| 882 |
-
'technical_support+feature_request': { question_text: 'Are you reporting something broken, or requesting a new capability?', options: ['Something broken','New feature'], expected_gain: 0.68 },
|
| 883 |
-
'churn_risk+account_management': { question_text: 'Are you looking to change your plan, or do you have concerns about continuing?', options: ['Change plan','Concerns about continuing'], expected_gain: 0.74 },
|
| 884 |
-
'account_management+churn_risk': { question_text: 'Are you looking to change your plan, or do you have concerns about continuing?', options: ['Change plan','Concerns about continuing'], expected_gain: 0.74 },
|
| 885 |
-
'onboarding+technical_support': { question_text: 'Is this affecting a new user, or an existing user?', options: ['New user','Existing user'], expected_gain: 0.65 },
|
| 886 |
-
'technical_support+onboarding': { question_text: 'Is this affecting a new user, or an existing user?', options: ['New user','Existing user'], expected_gain: 0.65 },
|
| 887 |
-
'compliance_legal+billing': { question_text: 'Does this relate to a regulatory requirement, or to payment/invoicing?', options: ['Regulatory','Payment'], expected_gain: 0.72 },
|
| 888 |
-
'billing+compliance_legal': { question_text: 'Does this relate to a regulatory requirement, or to payment/invoicing?', options: ['Regulatory','Payment'], expected_gain: 0.72 },
|
| 889 |
-
'technical_support+general_inquiry': { question_text: 'Is this a specific technical problem, or a general question about how something works?', options: ['Specific problem','General question'], expected_gain: 0.66 },
|
| 890 |
-
'general_inquiry+technical_support': { question_text: 'Is this a specific technical problem, or a general question about how something works?', options: ['Specific problem','General question'], expected_gain: 0.66 },
|
| 891 |
-
'billing+general_inquiry': { question_text: 'Is your question about a specific charge on your account, or general pricing information?', options: ['Specific charge','General pricing'], expected_gain: 0.64 },
|
| 892 |
-
'general_inquiry+billing': { question_text: 'Is your question about a specific charge on your account, or general pricing information?', options: ['Specific charge','General pricing'], expected_gain: 0.64 },
|
| 893 |
-
'churn_risk+technical_support': { question_text: 'Is the main concern a technical problem you need fixed, or are you considering leaving the platform?', options: ['Technical problem','Considering leaving'], expected_gain: 0.76 },
|
| 894 |
-
'technical_support+churn_risk': { question_text: 'Is the main concern a technical problem you need fixed, or are you considering leaving the platform?', options: ['Technical problem','Considering leaving'], expected_gain: 0.76 },
|
| 895 |
-
};
|
| 896 |
-
const key = topTwo[0] + '+' + topTwo[1];
|
| 897 |
-
clarification = questions[key] || {
|
| 898 |
-
question_text: 'Could you specify whether this is about a technical issue or an account/billing matter?',
|
| 899 |
-
options: ['Technical issue', 'Account/billing'], expected_gain: 0.62,
|
| 900 |
-
};
|
| 901 |
-
clarification.question_id = 'Q_SIM';
|
| 902 |
-
}
|
| 903 |
-
|
| 904 |
-
const demoSignals = inferDemoSignals(t);
|
| 905 |
-
|
| 906 |
-
// SLA
|
| 907 |
-
const outageWords = ['down', 'outage', 'crash', 'failing', 'blocked'];
|
| 908 |
-
const outageFlags = outageWords.filter(w => t.includes(w));
|
| 909 |
-
const slaBase = 0.15
|
| 910 |
-
+ (demoSignals.sentiment_score < -0.3 ? 0.2 : 0)
|
| 911 |
-
+ (demoSignals.urgency_score * 0.45)
|
| 912 |
-
+ (outageFlags.length * 0.15);
|
| 913 |
-
const slaBreach = Math.min(Math.round(slaBase * 1000) / 1000, 0.95);
|
| 914 |
-
|
| 915 |
-
return {
|
| 916 |
-
action, confidence: Math.round(confidence * 10000) / 10000,
|
| 917 |
-
entropy: Math.round(entropy * 10000) / 10000,
|
| 918 |
-
margin: Math.round(margin * 10000) / 10000,
|
| 919 |
-
top_category: topCat, all_probs: scores,
|
| 920 |
-
top_two_classes: topTwo, queue: topCat,
|
| 921 |
-
reason, clarification,
|
| 922 |
-
sla_breach_probability: slaBreach,
|
| 923 |
-
urgency_score: demoSignals.urgency_score,
|
| 924 |
-
features: {
|
| 925 |
-
...demoSignals,
|
| 926 |
-
text_complexity_score: Math.round(text.split(' ').length / 5 * 100) / 100,
|
| 927 |
-
},
|
| 928 |
-
latency_ms: 38 + (hashText(t) % 30),
|
| 929 |
-
};
|
| 930 |
-
}
|
|
|
|
| 1 |
+
// SupportMind Dashboard - app.js
|
| 2 |
+
// Interactive demo with real API calls (falls back to simulation if API unavailable)
|
| 3 |
+
|
| 4 |
+
let API_BASE = window.location.origin;
|
| 5 |
+
let apiOnline = false;
|
| 6 |
+
|
| 7 |
+
function apiCandidates() {
|
| 8 |
+
const candidates = [];
|
| 9 |
+
const url = new URL(window.location.href);
|
| 10 |
+
if (url.hostname === '127.0.0.1' && url.port === '7860') {
|
| 11 |
+
candidates.push('http://127.0.0.1:7862');
|
| 12 |
+
candidates.push('http://127.0.0.1:7861');
|
| 13 |
+
}
|
| 14 |
+
candidates.push(window.location.origin);
|
| 15 |
+
return [...new Set(candidates)];
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
// Category colors
|
| 19 |
+
const CAT_COLORS = {
|
| 20 |
+
billing: '#fb923c', technical_support: '#8083ff', account_management: '#89ceff',
|
| 21 |
+
feature_request: '#c0c1ff', compliance_legal: '#f87171', onboarding: '#4ade80',
|
| 22 |
+
general_inquiry: '#94a3b8', churn_risk: '#facc15',
|
| 23 |
+
};
|
| 24 |
+
|
| 25 |
+
// -- Init ----------------------------------------------
|
| 26 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 27 |
+
animateCounters();
|
| 28 |
+
initPresets();
|
| 29 |
+
initDropoutViz();
|
| 30 |
+
initScrollAnimations();
|
| 31 |
+
initSmoothScroll();
|
| 32 |
+
checkAPI();
|
| 33 |
+
updateMetrics();
|
| 34 |
+
setInterval(updateMetrics, 5000); // Update every 5 seconds
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
// -- Counter Animation ---------------------------------
|
| 38 |
+
function animateCounters() {
|
| 39 |
+
document.querySelectorAll('.stat-card').forEach(card => {
|
| 40 |
+
const counter = card.querySelector('.counter');
|
| 41 |
+
const target = parseFloat(card.dataset.value);
|
| 42 |
+
const duration = 1500;
|
| 43 |
+
const start = performance.now();
|
| 44 |
+
function update(now) {
|
| 45 |
+
const elapsed = now - start;
|
| 46 |
+
const progress = Math.min(elapsed / duration, 1);
|
| 47 |
+
const eased = 1 - Math.pow(1 - progress, 3);
|
| 48 |
+
counter.textContent = Math.round(target * eased * 10) / 10;
|
| 49 |
+
if (progress < 1) requestAnimationFrame(update);
|
| 50 |
+
else counter.textContent = target;
|
| 51 |
+
}
|
| 52 |
+
requestAnimationFrame(update);
|
| 53 |
+
});
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
// -- Presets --------------------------------------------
|
| 57 |
+
// -- Live Telemetry Engine ---------------------------
|
| 58 |
+
async function updateMetrics() {
|
| 59 |
+
try {
|
| 60 |
+
const res = await fetch(`${API_BASE}/metrics`);
|
| 61 |
+
if (!res.ok) return;
|
| 62 |
+
const data = await res.json();
|
| 63 |
+
|
| 64 |
+
// Update Counter
|
| 65 |
+
document.getElementById('live-total').textContent = data.total_requests.toLocaleString();
|
| 66 |
+
|
| 67 |
+
// Update Model Name
|
| 68 |
+
document.getElementById('live-model').textContent = data.model;
|
| 69 |
+
|
| 70 |
+
// Update Distribution Bar
|
| 71 |
+
const dist = data.routing_distribution;
|
| 72 |
+
document.getElementById('dist-route').style.width = `${dist.route_pct}%`;
|
| 73 |
+
document.getElementById('dist-clarify').style.width = `${dist.clarify_pct}%`;
|
| 74 |
+
document.getElementById('dist-escalate').style.width = `${dist.escalate_pct}%`;
|
| 75 |
+
|
| 76 |
+
// Update Status Pulse
|
| 77 |
+
const indicator = document.getElementById('live-indicator');
|
| 78 |
+
indicator.style.opacity = '1';
|
| 79 |
+
setTimeout(() => { indicator.style.opacity = '0.8'; }, 500);
|
| 80 |
+
|
| 81 |
+
} catch (err) {
|
| 82 |
+
console.warn("Metrics sync failed:", err);
|
| 83 |
+
}
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
// -- Presets --------------------------------------------
|
| 87 |
+
function initPresets() {
|
| 88 |
+
document.querySelectorAll('.preset-btn').forEach(btn => {
|
| 89 |
+
btn.addEventListener('click', () => {
|
| 90 |
+
document.getElementById('ticket-input').value = btn.dataset.text;
|
| 91 |
+
});
|
| 92 |
+
});
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
function initSmoothScroll() {
|
| 96 |
+
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
| 97 |
+
anchor.addEventListener('click', function (e) {
|
| 98 |
+
e.preventDefault();
|
| 99 |
+
document.querySelector(this.getAttribute('href')).scrollIntoView({
|
| 100 |
+
behavior: 'smooth'
|
| 101 |
+
});
|
| 102 |
+
});
|
| 103 |
+
});
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
// -- MC Dropout Visualization --------------------------
|
| 107 |
+
function initDropoutViz() {
|
| 108 |
+
const grid = document.getElementById('dropout-grid');
|
| 109 |
+
if (!grid) return;
|
| 110 |
+
for (let pass = 0; pass < 20; pass++) {
|
| 111 |
+
const col = document.createElement('div');
|
| 112 |
+
col.className = 'dropout-col';
|
| 113 |
+
for (let neuron = 0; neuron < 12; neuron++) {
|
| 114 |
+
const cell = document.createElement('div');
|
| 115 |
+
cell.className = 'dropout-cell';
|
| 116 |
+
const active = Math.random() > 0.15;
|
| 117 |
+
cell.style.background = active ? 'var(--primary)' : 'rgba(192, 193, 255, 0.05)';
|
| 118 |
+
cell.style.border = active ? 'none' : '1px solid rgba(192, 193, 255, 0.1)';
|
| 119 |
+
col.appendChild(cell);
|
| 120 |
+
}
|
| 121 |
+
grid.appendChild(col);
|
| 122 |
+
}
|
| 123 |
+
// Animate dropout
|
| 124 |
+
setInterval(() => {
|
| 125 |
+
grid.querySelectorAll('.dropout-cell').forEach(cell => {
|
| 126 |
+
const active = Math.random() > 0.15;
|
| 127 |
+
cell.style.background = active ? 'var(--primary)' : 'rgba(192, 193, 255, 0.05)';
|
| 128 |
+
cell.style.border = active ? 'none' : '1px solid rgba(192, 193, 255, 0.1)';
|
| 129 |
+
});
|
| 130 |
+
}, 2000);
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
// -- Scroll Animations ---------------------------------
|
| 134 |
+
function initScrollAnimations() {
|
| 135 |
+
const observer = new IntersectionObserver((entries) => {
|
| 136 |
+
entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('visible'); });
|
| 137 |
+
}, { threshold: 0.1 });
|
| 138 |
+
document.querySelectorAll('.section-header, .stat-card, .arch-stage, .bench-card, .ops-card').forEach(el => {
|
| 139 |
+
el.classList.add('fade-in');
|
| 140 |
+
observer.observe(el);
|
| 141 |
+
});
|
| 142 |
+
}
|
| 143 |
+
|
| 144 |
+
// -- API Check -----------------------------------------
|
| 145 |
+
async function checkAPI() {
|
| 146 |
+
for (const candidate of apiCandidates()) {
|
| 147 |
+
try {
|
| 148 |
+
const res = await fetch(`${candidate}/health`, { signal: AbortSignal.timeout(2000) });
|
| 149 |
+
if (!res.ok) continue;
|
| 150 |
+
API_BASE = candidate;
|
| 151 |
+
apiOnline = true;
|
| 152 |
+
const statusEl = document.querySelector('.status-text');
|
| 153 |
+
if (statusEl) statusEl.textContent = API_BASE === window.location.origin ? 'API Connected' : 'Fixed API Connected';
|
| 154 |
+
return;
|
| 155 |
+
} catch {
|
| 156 |
+
// Try the next candidate before falling back to demo mode.
|
| 157 |
+
}
|
| 158 |
+
}
|
| 159 |
+
apiOnline = false;
|
| 160 |
+
const statusEl = document.querySelector('.status-text');
|
| 161 |
+
if (statusEl) statusEl.textContent = 'Demo Mode';
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
// -- Live Metrics --------------------------------------
|
| 165 |
+
async function updateLiveMetrics() {
|
| 166 |
+
if (!apiOnline) return;
|
| 167 |
+
try {
|
| 168 |
+
const res = await fetch(`${API_BASE}/metrics`);
|
| 169 |
+
const data = await res.json();
|
| 170 |
+
|
| 171 |
+
document.getElementById('live-model').textContent = data.model;
|
| 172 |
+
document.getElementById('live-total').textContent = data.total_requests;
|
| 173 |
+
|
| 174 |
+
const dist = data.routing_distribution;
|
| 175 |
+
document.getElementById('dist-route').style.width = dist.route_pct + '%';
|
| 176 |
+
document.getElementById('dist-clarify').style.width = dist.clarify_pct + '%';
|
| 177 |
+
document.getElementById('dist-escalate').style.width = dist.escalate_pct + '%';
|
| 178 |
+
} catch (err) {
|
| 179 |
+
console.warn('Metrics update failed:', err);
|
| 180 |
+
}
|
| 181 |
+
}
|
| 182 |
+
|
| 183 |
+
// -- Route Ticket --------------------------------------
|
| 184 |
+
async function routeTicket(extraPayload = {}) {
|
| 185 |
+
const text = document.getElementById('ticket-input').value.trim();
|
| 186 |
+
if (!text) return;
|
| 187 |
+
|
| 188 |
+
const btn = document.getElementById('route-btn');
|
| 189 |
+
btn.innerHTML = '<span class="spinner"></span> Routing...';
|
| 190 |
+
btn.disabled = true;
|
| 191 |
+
|
| 192 |
+
let result;
|
| 193 |
+
try {
|
| 194 |
+
if (apiOnline) {
|
| 195 |
+
const res = await fetch(`${API_BASE}/route`, {
|
| 196 |
+
method: 'POST',
|
| 197 |
+
headers: { 'Content-Type': 'application/json' },
|
| 198 |
+
body: JSON.stringify({ text, ...extraPayload }),
|
| 199 |
+
});
|
| 200 |
+
result = await res.json();
|
| 201 |
+
} else {
|
| 202 |
+
result = simulateRouting(text, extraPayload);
|
| 203 |
+
}
|
| 204 |
+
displayResult(result, text);
|
| 205 |
+
} catch (err) {
|
| 206 |
+
result = simulateRouting(text, extraPayload);
|
| 207 |
+
displayResult(result, text);
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
btn.innerHTML = 'Route Ticket';
|
| 211 |
+
btn.disabled = false;
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
// -- Display Result ------------------------------------
|
| 215 |
+
function displayResult(r, routedText) {
|
| 216 |
+
// Handle edge cases
|
| 217 |
+
if (r.action === 'invalid_input') {
|
| 218 |
+
document.getElementById('result-placeholder').style.display = 'none';
|
| 219 |
+
const content = document.getElementById('result-content');
|
| 220 |
+
content.style.display = 'block';
|
| 221 |
+
|
| 222 |
+
const badge = document.getElementById('action-badge');
|
| 223 |
+
badge.textContent = r.error_type.toUpperCase().replace('_', ' ');
|
| 224 |
+
badge.className = 'action-badge clarify'; // yellow
|
| 225 |
+
|
| 226 |
+
document.getElementById('action-queue').textContent = r.response;
|
| 227 |
+
document.getElementById('result-reason').textContent = r.response;
|
| 228 |
+
|
| 229 |
+
// Hide gauges for invalid input
|
| 230 |
+
document.querySelector('.gauge-row').style.display = 'none';
|
| 231 |
+
document.querySelector('.signals-grid').style.display = 'none';
|
| 232 |
+
document.getElementById('prob-chart').innerHTML = '';
|
| 233 |
+
document.getElementById('clarification-box').style.display = 'none';
|
| 234 |
+
const evidenceGrid = document.getElementById('signal-evidence-grid');
|
| 235 |
+
if (evidenceGrid) evidenceGrid.style.display = 'none';
|
| 236 |
+
const explainBtn = document.getElementById('explain-btn');
|
| 237 |
+
if (explainBtn) explainBtn.style.display = 'none';
|
| 238 |
+
document.getElementById('explanation-box').style.display = 'none';
|
| 239 |
+
return;
|
| 240 |
+
}
|
| 241 |
+
|
| 242 |
+
|
| 243 |
+
// Show gauges for valid input
|
| 244 |
+
document.querySelector('.gauge-row').style.display = 'grid';
|
| 245 |
+
document.querySelector('.signals-grid').style.display = 'grid';
|
| 246 |
+
|
| 247 |
+
document.getElementById('result-placeholder').style.display = 'none';
|
| 248 |
+
const content = document.getElementById('result-content');
|
| 249 |
+
content.style.display = 'block';
|
| 250 |
+
const evidenceGrid = document.getElementById('signal-evidence-grid');
|
| 251 |
+
if (evidenceGrid) evidenceGrid.style.display = 'grid';
|
| 252 |
+
|
| 253 |
+
// Action Badge Logic
|
| 254 |
+
const badge = document.getElementById('action-badge');
|
| 255 |
+
const queue = document.getElementById('action-queue');
|
| 256 |
+
|
| 257 |
+
if (r.action === 'multi_route') {
|
| 258 |
+
badge.textContent = 'MULTI-ROUTE';
|
| 259 |
+
badge.className = 'action-badge';
|
| 260 |
+
badge.style.background = 'linear-gradient(90deg, var(--primary), var(--accent))';
|
| 261 |
+
queue.innerHTML = `
|
| 262 |
+
<div style="display: flex; gap: 8px; margin-top: 4px;">
|
| 263 |
+
<span class="tech-tag" style="background: rgba(192, 193, 255, 0.2)">Primary: ${r.primary_queue}</span>
|
| 264 |
+
<span class="tech-tag" style="background: rgba(255, 255, 255, 0.1)">Secondary: ${r.secondary_queue}</span>
|
| 265 |
+
</div>
|
| 266 |
+
`;
|
| 267 |
+
} else {
|
| 268 |
+
badge.textContent = r.action.toUpperCase();
|
| 269 |
+
badge.className = `action-badge ${r.action}`;
|
| 270 |
+
queue.textContent = r.action === 'route' ? `-> ${r.queue || r.top_category} queue` :
|
| 271 |
+
r.action === 'clarify' ? 'Needs 1 clarification question' : 'Immediate human triage';
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
// Gauges
|
| 275 |
+
const confPct = Math.min(r.confidence * 100, 100);
|
| 276 |
+
document.getElementById('conf-fill').style.width = confPct + '%';
|
| 277 |
+
document.getElementById('conf-value').textContent = r.confidence.toFixed(4);
|
| 278 |
+
const maxEnt = Math.log(8);
|
| 279 |
+
const entPct = Math.min((r.entropy / maxEnt) * 100, 100);
|
| 280 |
+
document.getElementById('ent-fill').style.width = entPct + '%';
|
| 281 |
+
document.getElementById('ent-value').textContent = r.entropy.toFixed(4);
|
| 282 |
+
if (r.margin !== undefined && document.getElementById('margin-value')) {
|
| 283 |
+
document.getElementById('margin-value').textContent = r.margin.toFixed(4);
|
| 284 |
+
}
|
| 285 |
+
|
| 286 |
+
|
| 287 |
+
// Prob chart
|
| 288 |
+
const chart = document.getElementById('prob-chart');
|
| 289 |
+
chart.innerHTML = '';
|
| 290 |
+
const probs = r.all_probs || {};
|
| 291 |
+
const sorted = Object.entries(probs).sort((a, b) => b[1] - a[1]);
|
| 292 |
+
const maxProb = sorted.length ? sorted[0][1] : 1;
|
| 293 |
+
sorted.forEach(([cat, prob]) => {
|
| 294 |
+
const row = document.createElement('div');
|
| 295 |
+
row.className = 'prob-row';
|
| 296 |
+
const pct = (prob / Math.max(maxProb, 0.01)) * 100;
|
| 297 |
+
row.innerHTML = `
|
| 298 |
+
<span class="prob-label">${cat.replace(/_/g, ' ')}</span>
|
| 299 |
+
<div class="prob-bar-track"><div class="prob-bar-fill" style="width:${pct}%;background:${CAT_COLORS[cat] || '#6366f1'}"></div></div>
|
| 300 |
+
<span class="prob-val">${(prob * 100).toFixed(1)}%</span>`;
|
| 301 |
+
chart.appendChild(row);
|
| 302 |
+
});
|
| 303 |
+
|
| 304 |
+
// Clarification
|
| 305 |
+
const clarBox = document.getElementById('clarification-box');
|
| 306 |
+
if (r.action === 'clarify' && r.clarification) {
|
| 307 |
+
clarBox.style.display = 'block';
|
| 308 |
+
document.getElementById('clarify-question').textContent = r.clarification.question_text;
|
| 309 |
+
const optEl = document.getElementById('clarify-options');
|
| 310 |
+
optEl.innerHTML = '';
|
| 311 |
+
const optionTargets = r.clarification.option_targets || [];
|
| 312 |
+
(r.clarification.options || []).forEach((o, index) => {
|
| 313 |
+
const btn = document.createElement('button');
|
| 314 |
+
btn.className = 'option-btn';
|
| 315 |
+
btn.textContent = o;
|
| 316 |
+
btn.onclick = () => {
|
| 317 |
+
// Provide visual feedback
|
| 318 |
+
document.querySelectorAll('#clarify-options .option-btn').forEach(b => b.disabled = true);
|
| 319 |
+
btn.style.background = 'var(--primary)';
|
| 320 |
+
btn.style.color = '#fff';
|
| 321 |
+
|
| 322 |
+
const target = optionTargets[index]
|
| 323 |
+
|| inferClarificationTarget(o, r.clarification.relevant_classes || r.top_two_classes || [], index);
|
| 324 |
+
|
| 325 |
+
// Keep the selection visible and machine-readable for repeat manual runs.
|
| 326 |
+
const input = document.getElementById('ticket-input');
|
| 327 |
+
input.value = input.value.trim() + '\n\n[Clarification: ' + target + ' - ' + o + ']';
|
| 328 |
+
|
| 329 |
+
// Re-route with new context after a short delay
|
| 330 |
+
setTimeout(() => {
|
| 331 |
+
routeTicket({
|
| 332 |
+
clarification_choice: o,
|
| 333 |
+
clarification_target: target,
|
| 334 |
+
clarification_question_id: r.clarification.question_id,
|
| 335 |
+
});
|
| 336 |
+
}, 800);
|
| 337 |
+
};
|
| 338 |
+
optEl.appendChild(btn);
|
| 339 |
+
});
|
| 340 |
+
|
| 341 |
+
// Remove existing badge if any
|
| 342 |
+
const existingBadge = document.getElementById('source-badge');
|
| 343 |
+
if (existingBadge) existingBadge.remove();
|
| 344 |
+
|
| 345 |
+
// After displaying the question, add source badge
|
| 346 |
+
const sourceBadge = document.createElement('div');
|
| 347 |
+
sourceBadge.id = 'source-badge';
|
| 348 |
+
sourceBadge.style.cssText = 'font-size:11px;margin-top:8px;opacity:0.6;';
|
| 349 |
+
sourceBadge.textContent = r.clarification.source === 'llm_groq'
|
| 350 |
+
? 'Generated by LLaMA3 via Groq'
|
| 351 |
+
: 'Template: Selected from template bank';
|
| 352 |
+
document.getElementById('clarification-box').appendChild(sourceBadge);
|
| 353 |
+
|
| 354 |
+
document.getElementById('clarify-gain').textContent =
|
| 355 |
+
`Expected information gain: ${r.clarification.expected_gain?.toFixed(4) || 'N/A'}`;
|
| 356 |
+
} else {
|
| 357 |
+
clarBox.style.display = 'none';
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
// Signals
|
| 361 |
+
const slaRiskVal = r.sla_risk || r.sla_breach_probability || 0;
|
| 362 |
+
const slaPct = slaRiskVal * 100;
|
| 363 |
+
document.getElementById('sla-value').textContent = slaPct.toFixed(1) + '%';
|
| 364 |
+
document.getElementById('sla-fill').style.width = slaPct + '%';
|
| 365 |
+
document.getElementById('sla-fill').style.background =
|
| 366 |
+
slaPct > 65 ? 'var(--red)' : slaPct > 35 ? 'var(--yellow)' : 'var(--green)';
|
| 367 |
+
|
| 368 |
+
const feat = r.features || {};
|
| 369 |
+
const sent = feat.sentiment_score;
|
| 370 |
+
const sentLabel = feat.sentiment_label || sentimentLabelFromScore(sent);
|
| 371 |
+
const sentimentValue = document.getElementById('sentiment-value');
|
| 372 |
+
sentimentValue.textContent = sentLabel ? sentLabel.toUpperCase() : '-';
|
| 373 |
+
sentimentValue.style.color = sentimentColor(sentLabel, sent);
|
| 374 |
+
const sentimentScore = document.getElementById('sentiment-score');
|
| 375 |
+
if (sentimentScore) {
|
| 376 |
+
const raw = typeof feat.sentiment_raw_score === 'number'
|
| 377 |
+
? ` raw ${feat.sentiment_raw_score.toFixed(2)}`
|
| 378 |
+
: '';
|
| 379 |
+
sentimentScore.textContent = sent !== undefined ? `score ${sent.toFixed(2)}${raw}` : '-';
|
| 380 |
+
}
|
| 381 |
+
|
| 382 |
+
const urgScore = numericValue(r.urgency_score, feat.urgency_score, 0);
|
| 383 |
+
const urgLevel = feat.urgency_level || urgencyLevelFromScore(urgScore);
|
| 384 |
+
const urgencyCard = document.getElementById('urgency-value').parentElement;
|
| 385 |
+
const urgencyValue = document.getElementById('urgency-value');
|
| 386 |
+
urgencyValue.textContent = urgLevel.toUpperCase();
|
| 387 |
+
urgencyValue.style.color = urgencyColor(urgLevel);
|
| 388 |
+
const urgencyScore = document.getElementById('urgency-score');
|
| 389 |
+
if (urgencyScore) urgencyScore.textContent = `score ${urgScore.toFixed(2)}`;
|
| 390 |
+
|
| 391 |
+
if (urgLevel === 'critical') {
|
| 392 |
+
urgencyCard.style.border = '1px solid var(--red)';
|
| 393 |
+
urgencyCard.style.boxShadow = '0 0 15px rgba(248, 113, 113, 0.2)';
|
| 394 |
+
} else if (urgLevel === 'high') {
|
| 395 |
+
urgencyCard.style.border = '1px solid var(--yellow)';
|
| 396 |
+
urgencyCard.style.boxShadow = '';
|
| 397 |
+
} else {
|
| 398 |
+
urgencyCard.style.border = '';
|
| 399 |
+
urgencyCard.style.boxShadow = '';
|
| 400 |
+
}
|
| 401 |
+
|
| 402 |
+
renderEvidenceList('urgency-evidence-list', feat.urgency_evidence || []);
|
| 403 |
+
renderEvidenceList('sentiment-evidence-list', feat.sentiment_evidence || []);
|
| 404 |
+
|
| 405 |
+
document.getElementById('latency-value').textContent =
|
| 406 |
+
r.latency_ms ? r.latency_ms + 'ms' : '-';
|
| 407 |
+
|
| 408 |
+
// Reason
|
| 409 |
+
let decisionReason = '';
|
| 410 |
+
if (r.clarification_applied) {
|
| 411 |
+
decisionReason = `Clarification answer applied: <strong>${escapeHtml(r.clarification_choice || r.top_category)}</strong>. Routing to <strong>${r.top_category}</strong> without asking another question.`;
|
| 412 |
+
} else if (r.action === 'multi_route') {
|
| 413 |
+
decisionReason = `Multiple distinct intents detected in the request. Primary intent is <strong>${r.primary_queue}</strong>, secondary is <strong>${r.secondary_queue}</strong>.`;
|
| 414 |
+
} else if (r.action === 'clarify') {
|
| 415 |
+
decisionReason = `Model uncertainty is high (entropy: ${r.entropy.toFixed(3)}) or the top two classes are too close (margin: ${r.margin?.toFixed(3)}). A clarification question was generated to refine the intent.`;
|
| 416 |
+
} else if (r.action === 'escalate') {
|
| 417 |
+
decisionReason = `Low model confidence detected (${(r.confidence * 100).toFixed(1)}%). Routing directly to human experts to ensure accuracy.`;
|
| 418 |
+
} else {
|
| 419 |
+
decisionReason = `High-confidence intent detected: <strong>${r.top_category}</strong>. Automatically routing to specialized queue.`;
|
| 420 |
+
}
|
| 421 |
+
|
| 422 |
+
document.getElementById('result-reason').innerHTML = `
|
| 423 |
+
<div style="padding: 12px; background: rgba(192, 193, 255, 0.05); border: 1px solid rgba(192, 193, 255, 0.1); border-radius: 8px; margin-top: 16px;">
|
| 424 |
+
<div style="font-size: 11px; text-transform: uppercase; color: var(--primary); margin-bottom: 8px; font-weight: 600;">Decision Reason</div>
|
| 425 |
+
<div style="font-size: 13px; color: var(--on-surface-variant); line-height: 1.5;">${decisionReason}</div>
|
| 426 |
+
</div>
|
| 427 |
+
`;
|
| 428 |
+
|
| 429 |
+
// Show explain button for valid input
|
| 430 |
+
const explainBtn = document.getElementById('explain-btn');
|
| 431 |
+
if (explainBtn) {
|
| 432 |
+
explainBtn.style.display = 'flex';
|
| 433 |
+
explainBtn.dataset.text = routedText || document.getElementById('ticket-input').value;
|
| 434 |
+
explainBtn.dataset.category = r.top_category;
|
| 435 |
+
}
|
| 436 |
+
document.getElementById('explanation-box').style.display = 'none';
|
| 437 |
+
}
|
| 438 |
+
|
| 439 |
+
// -- Explain Decision (SHAP) ---------------------------
|
| 440 |
+
async function explainDecision() {
|
| 441 |
+
const btn = document.getElementById('explain-btn');
|
| 442 |
+
const text = btn.dataset.text;
|
| 443 |
+
const targetClass = btn.dataset.category;
|
| 444 |
+
|
| 445 |
+
btn.innerHTML = '<span class="spinner"></span> Analyzing tokens...';
|
| 446 |
+
btn.disabled = true;
|
| 447 |
+
|
| 448 |
+
try {
|
| 449 |
+
let result;
|
| 450 |
+
if (apiOnline) {
|
| 451 |
+
const res = await fetch(`${API_BASE}/explain`, {
|
| 452 |
+
method: 'POST',
|
| 453 |
+
headers: { 'Content-Type': 'application/json' },
|
| 454 |
+
body: JSON.stringify({ text, target_class: targetClass }),
|
| 455 |
+
});
|
| 456 |
+
if (!res.ok) throw new Error(`Explain API returned ${res.status}`);
|
| 457 |
+
result = await res.json();
|
| 458 |
+
} else {
|
| 459 |
+
// Simulate SHAP for demo mode
|
| 460 |
+
result = simulateSHAP(text);
|
| 461 |
+
}
|
| 462 |
+
|
| 463 |
+
renderSHAP(result);
|
| 464 |
+
} catch (err) {
|
| 465 |
+
console.error('SHAP failed:', err);
|
| 466 |
+
renderSHAP(simulateSHAP(text));
|
| 467 |
+
}
|
| 468 |
+
|
| 469 |
+
btn.innerHTML = '<span class="material-symbols-outlined btn-icon">query_stats</span> Analyze Decision';
|
| 470 |
+
btn.disabled = false;
|
| 471 |
+
}
|
| 472 |
+
|
| 473 |
+
function renderSHAP(data) {
|
| 474 |
+
const box = document.getElementById('explanation-box');
|
| 475 |
+
const textEl = document.getElementById('explain-text');
|
| 476 |
+
box.style.display = 'block';
|
| 477 |
+
textEl.innerHTML = '';
|
| 478 |
+
|
| 479 |
+
if (data.error) {
|
| 480 |
+
textEl.textContent = 'Error generating explanation: ' + data.error;
|
| 481 |
+
return;
|
| 482 |
+
}
|
| 483 |
+
|
| 484 |
+
const source = document.createElement('div');
|
| 485 |
+
source.className = 'explain-source';
|
| 486 |
+
source.textContent = data.source === 'shap_transformer'
|
| 487 |
+
? 'Transformer SHAP explanation'
|
| 488 |
+
: 'Keyword evidence fallback';
|
| 489 |
+
if (data.note) source.title = data.note;
|
| 490 |
+
textEl.appendChild(source);
|
| 491 |
+
|
| 492 |
+
const tokens = data.tokens || [];
|
| 493 |
+
const values = data.values || [];
|
| 494 |
+
|
| 495 |
+
tokens.forEach((token, i) => {
|
| 496 |
+
const val = values[i];
|
| 497 |
+
const span = document.createElement('span');
|
| 498 |
+
span.className = 'shap-token';
|
| 499 |
+
span.textContent = token.replace('##', ''); // Simple handling for subwords
|
| 500 |
+
|
| 501 |
+
// Normalize opacity based on value
|
| 502 |
+
const absVal = Math.abs(val);
|
| 503 |
+
const opacity = Math.min(absVal * 5, 0.8); // Scale for visibility
|
| 504 |
+
|
| 505 |
+
if (val > 0) {
|
| 506 |
+
span.style.background = `rgba(74, 222, 128, ${opacity})`;
|
| 507 |
+
span.style.borderBottom = `2px solid rgba(74, 222, 128, ${opacity + 0.2})`;
|
| 508 |
+
} else if (val < 0) {
|
| 509 |
+
span.style.background = `rgba(248, 113, 113, ${opacity})`;
|
| 510 |
+
span.style.borderBottom = `2px solid rgba(248, 113, 113, ${opacity + 0.2})`;
|
| 511 |
+
}
|
| 512 |
+
|
| 513 |
+
textEl.appendChild(span);
|
| 514 |
+
textEl.appendChild(document.createTextNode(' '));
|
| 515 |
+
});
|
| 516 |
+
|
| 517 |
+
box.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
| 518 |
+
}
|
| 519 |
+
|
| 520 |
+
function simulateSHAP(text) {
|
| 521 |
+
const tokens = text.split(/\s+/);
|
| 522 |
+
const values = tokens.map(() => (Math.random() - 0.4) * 0.2);
|
| 523 |
+
return { tokens, values, source: 'demo_simulated' };
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
function numericValue(...values) {
|
| 527 |
+
for (const value of values) {
|
| 528 |
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
| 529 |
+
}
|
| 530 |
+
return 0;
|
| 531 |
+
}
|
| 532 |
+
|
| 533 |
+
function sentimentLabelFromScore(score) {
|
| 534 |
+
if (typeof score !== 'number') return null;
|
| 535 |
+
if (score <= -0.55) return 'frustrated';
|
| 536 |
+
if (score <= -0.2) return 'concerned';
|
| 537 |
+
if (score >= 0.3) return 'positive';
|
| 538 |
+
return 'neutral';
|
| 539 |
+
}
|
| 540 |
+
|
| 541 |
+
function sentimentColor(label, score) {
|
| 542 |
+
const normalized = (label || sentimentLabelFromScore(score) || '').toLowerCase();
|
| 543 |
+
if (normalized === 'frustrated') return 'var(--red)';
|
| 544 |
+
if (normalized === 'concerned') return 'var(--yellow)';
|
| 545 |
+
if (normalized === 'positive') return 'var(--green)';
|
| 546 |
+
return 'var(--text)';
|
| 547 |
+
}
|
| 548 |
+
|
| 549 |
+
function urgencyLevelFromScore(score) {
|
| 550 |
+
if (score >= 0.75) return 'critical';
|
| 551 |
+
if (score >= 0.5) return 'high';
|
| 552 |
+
if (score >= 0.25) return 'medium';
|
| 553 |
+
return 'low';
|
| 554 |
+
}
|
| 555 |
+
|
| 556 |
+
function urgencyColor(level) {
|
| 557 |
+
const normalized = (level || '').toLowerCase();
|
| 558 |
+
if (normalized === 'critical') return 'var(--red)';
|
| 559 |
+
if (normalized === 'high' || normalized === 'medium') return 'var(--yellow)';
|
| 560 |
+
return 'var(--green)';
|
| 561 |
+
}
|
| 562 |
+
|
| 563 |
+
function escapeHtml(value) {
|
| 564 |
+
return String(value)
|
| 565 |
+
.replace(/&/g, '&')
|
| 566 |
+
.replace(/</g, '<')
|
| 567 |
+
.replace(/>/g, '>')
|
| 568 |
+
.replace(/"/g, '"')
|
| 569 |
+
.replace(/'/g, ''');
|
| 570 |
+
}
|
| 571 |
+
|
| 572 |
+
function renderEvidenceList(elementId, evidence) {
|
| 573 |
+
const list = document.getElementById(elementId);
|
| 574 |
+
if (!list) return;
|
| 575 |
+
|
| 576 |
+
const items = Array.isArray(evidence) ? evidence.filter(Boolean) : [];
|
| 577 |
+
if (!items.length) {
|
| 578 |
+
list.innerHTML = '<div class="evidence-empty">No contextual evidence triggered.</div>';
|
| 579 |
+
return;
|
| 580 |
+
}
|
| 581 |
+
|
| 582 |
+
list.innerHTML = items.slice(0, 5).map(item => {
|
| 583 |
+
const [rawType, ...phraseParts] = String(item).split(':');
|
| 584 |
+
const type = rawType ? rawType.replace(/_/g, ' ') : 'signal';
|
| 585 |
+
const phrase = phraseParts.join(':').trim() || item;
|
| 586 |
+
return `
|
| 587 |
+
<div class="evidence-item">
|
| 588 |
+
<span class="evidence-type">${escapeHtml(type)}</span>
|
| 589 |
+
<span class="evidence-phrase">${escapeHtml(phrase)}</span>
|
| 590 |
+
</div>
|
| 591 |
+
`;
|
| 592 |
+
}).join('');
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
function inferClarificationTarget(option, relevantClasses, index) {
|
| 596 |
+
const optionLow = String(option || '').toLowerCase();
|
| 597 |
+
const keywordTargets = [
|
| 598 |
+
['billing', ['billing', 'invoice', 'payment', 'charge', 'refund', 'credit', 'pricing', 'cost', 'bill']],
|
| 599 |
+
['technical_support', ['software', 'error', 'technical', 'broken', 'malfunction', 'functionality', 'api', 'integration', 'performance', 'specific issue', 'data movement']],
|
| 600 |
+
['account_management', ['account', 'plan', 'subscription', 'administrator', 'admin', 'user management', 'regular user', 'settings']],
|
| 601 |
+
['feature_request', ['new capability', 'feature', 'request', 'enhancement']],
|
| 602 |
+
['compliance_legal', ['compliance', 'regulatory', 'audit', 'gdpr', 'security', 'data affected']],
|
| 603 |
+
['onboarding', ['new user', 'onboarding', 'guidance', 'training', 'walkthrough', 'setting up']],
|
| 604 |
+
['churn_risk', ['continuing', 'switching', 'evaluating options', 'mostly negative']],
|
| 605 |
+
['general_inquiry', ['general', 'guidance', 'not urgent', 'no specific deadline', 'positive']],
|
| 606 |
+
];
|
| 607 |
+
|
| 608 |
+
for (const [category, keywords] of keywordTargets) {
|
| 609 |
+
if (keywords.some(keyword => optionLow.includes(keyword))) return category;
|
| 610 |
+
}
|
| 611 |
+
return relevantClasses[index] || relevantClasses[0] || 'general_inquiry';
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
function firstPatternHit(text, patterns) {
|
| 615 |
+
for (const pattern of patterns) {
|
| 616 |
+
const match = text.match(pattern);
|
| 617 |
+
if (match) return match[0];
|
| 618 |
+
}
|
| 619 |
+
return null;
|
| 620 |
+
}
|
| 621 |
+
|
| 622 |
+
function inferDemoSignals(t) {
|
| 623 |
+
const urgencySpecs = [
|
| 624 |
+
['business_impact', 0.30, [
|
| 625 |
+
/\b(?:affecting|impacting|blocking)\s+(?:our\s+)?(?:customers|users|team|business|operations|sales|revenue|payroll|launch|production)\b/,
|
| 626 |
+
/\b(?:customers?|clients?)\s+(?:(?:are|is)\s+)?(?:waiting|blocked|affected|unable)\b/,
|
| 627 |
+
/\b(?:cannot|can't|unable to)\s+(?:process|ship|launch|serve|sell|invoice|onboard|work|access)\b/,
|
| 628 |
+
]],
|
| 629 |
+
['deadline_pressure', 0.25, [
|
| 630 |
+
/\b(?:in|within)\s+\d+\s*(?:min|mins|minutes|hour|hours|hrs|days?)\b/,
|
| 631 |
+
/\b(?:by|before)\s+(?:today|tomorrow|eod|end of day|tonight|monday|tuesday|wednesday|thursday|friday|saturday|sunday)\b/,
|
| 632 |
+
/\b(?:launch|demo|go-live|renewal|payroll|board meeting|presentation)\b/,
|
| 633 |
+
]],
|
| 634 |
+
['production_outage', 0.40, [
|
| 635 |
+
/\bproduction\s+(?:is\s+)?(?:down|blocked|broken|failing|impacted)\b/,
|
| 636 |
+
/\b(?:all|multiple|many)\s+(?:users|customers|accounts|teams)\s+(?:are\s+)?(?:affected|blocked|down|unable)\b/,
|
| 637 |
+
/\b(?:system|service|platform|dashboard|api)\s+(?:is\s+)?(?:down|unavailable|not responding)\b/,
|
| 638 |
+
]],
|
| 639 |
+
['access_loss', 0.25, [
|
| 640 |
+
/\b(?:locked out|cannot access|can't access|unable to access|access is blocked)\b/,
|
| 641 |
+
/\b(?:login|sso|authentication)\s+(?:is\s+)?(?:broken|failing|down|not working)\b/,
|
| 642 |
+
]],
|
| 643 |
+
['repeat_issue', 0.20, [
|
| 644 |
+
/\b(?:again|still|keeps?|repeated|recurring)\b/,
|
| 645 |
+
/\b(?:second|third|fourth)\s+time\b/,
|
| 646 |
+
/\b(?:raised|reported|opened)\s+(?:this\s+)?(?:before|multiple times|again)\b/,
|
| 647 |
+
]],
|
| 648 |
+
];
|
| 649 |
+
|
| 650 |
+
const explicitCritical = ['crash', 'blocked', 'down', 'failing', 'cannot access', 'production issue', 'outage', 'emergency', 'critical', 'urgent', 'immediately', 'blocking', 'locked out'];
|
| 651 |
+
const explicitGeneral = ['asap', 'deadline', 'sla', 'escalate', 'priority', 'time-sensitive', 'showstopper', 'presentation'];
|
| 652 |
+
|
| 653 |
+
const urgencyEvidence = [];
|
| 654 |
+
const urgencyFlags = [];
|
| 655 |
+
explicitCritical.forEach(word => {
|
| 656 |
+
if (t.includes(word)) {
|
| 657 |
+
urgencyEvidence.push(`explicit_critical: ${word}`);
|
| 658 |
+
urgencyFlags.push(word);
|
| 659 |
+
}
|
| 660 |
+
});
|
| 661 |
+
explicitGeneral.forEach(word => {
|
| 662 |
+
if (t.includes(word)) {
|
| 663 |
+
urgencyEvidence.push(`explicit_general: ${word}`);
|
| 664 |
+
urgencyFlags.push(word);
|
| 665 |
+
}
|
| 666 |
+
});
|
| 667 |
+
|
| 668 |
+
let urgencyScore = (explicitCritical.length ? 0 : 0);
|
| 669 |
+
urgencyScore += explicitCritical.filter(word => t.includes(word)).length * 0.25;
|
| 670 |
+
urgencyScore += explicitGeneral.filter(word => t.includes(word)).length * 0.12;
|
| 671 |
+
urgencySpecs.forEach(([label, weight, patterns]) => {
|
| 672 |
+
const phrase = firstPatternHit(t, patterns);
|
| 673 |
+
if (phrase) {
|
| 674 |
+
urgencyScore += weight;
|
| 675 |
+
urgencyEvidence.push(`${label}: ${phrase}`);
|
| 676 |
+
urgencyFlags.push(label);
|
| 677 |
+
}
|
| 678 |
+
});
|
| 679 |
+
|
| 680 |
+
if (/\b(?:not urgent|no rush|whenever you can|when you have time)\b/.test(t)) {
|
| 681 |
+
urgencyScore = Math.min(urgencyScore, 0.35);
|
| 682 |
+
urgencyEvidence.push('deescalation: no immediate pressure');
|
| 683 |
+
}
|
| 684 |
+
|
| 685 |
+
urgencyScore = Math.max(0, Math.min(1, urgencyScore));
|
| 686 |
+
|
| 687 |
+
const sentimentSpecs = [
|
| 688 |
+
['frustration', -0.30, [
|
| 689 |
+
/\bfrustrat(?:ed|ing|ion)\b/,
|
| 690 |
+
/\bnot happy\b/,
|
| 691 |
+
/\bdisappoint(?:ed|ing|ment)\b/,
|
| 692 |
+
/\bthis is becoming difficult\b/,
|
| 693 |
+
/\bnot ideal\b/,
|
| 694 |
+
/\bunacceptable\b/,
|
| 695 |
+
/\bterrible\b/,
|
| 696 |
+
/\bawful\b/,
|
| 697 |
+
]],
|
| 698 |
+
['trust_risk', -0.25, [
|
| 699 |
+
/\b(?:losing|lost)\s+(?:trust|confidence)\b/,
|
| 700 |
+
/\b(?:considering|thinking about)\s+(?:switching|leaving|cancelling|canceling)\b/,
|
| 701 |
+
]],
|
| 702 |
+
['polite_negative', -0.22, [
|
| 703 |
+
/\b(?:this|it)\s+is\s+(?:affecting|impacting|blocking)\b/,
|
| 704 |
+
/\b(?:could you please|please)\b.*\b(?:fix|resolve|help)\b.*\b(?:blocking|affecting|stuck|broken|failing)\b/,
|
| 705 |
+
/\b(?:becoming|getting)\s+(?:difficult|hard|painful)\b/,
|
| 706 |
+
]],
|
| 707 |
+
];
|
| 708 |
+
|
| 709 |
+
const negWords = ['frustrated','broken','terrible','angry','worst','cancel','bad','issue','error', 'invalid', 'locked out'];
|
| 710 |
+
const posWords = ['great','thanks','love','good','happy','please'];
|
| 711 |
+
let rawSentiment = 0;
|
| 712 |
+
negWords.forEach(w => { if (t.includes(w)) rawSentiment -= 0.18; });
|
| 713 |
+
posWords.forEach(w => { if (t.includes(w)) rawSentiment += 0.12; });
|
| 714 |
+
rawSentiment = Math.max(-1, Math.min(1, rawSentiment));
|
| 715 |
+
|
| 716 |
+
const sentimentEvidence = [];
|
| 717 |
+
let sentimentScore = rawSentiment;
|
| 718 |
+
sentimentSpecs.forEach(([label, weight, patterns]) => {
|
| 719 |
+
const phrase = firstPatternHit(t, patterns);
|
| 720 |
+
if (phrase) {
|
| 721 |
+
sentimentScore += weight;
|
| 722 |
+
sentimentEvidence.push(`${label}: ${phrase}`);
|
| 723 |
+
}
|
| 724 |
+
});
|
| 725 |
+
sentimentScore = Math.max(-1, Math.min(1, sentimentScore));
|
| 726 |
+
|
| 727 |
+
return {
|
| 728 |
+
urgency_score: Math.round(urgencyScore * 10000) / 10000,
|
| 729 |
+
urgency_level: urgencyLevelFromScore(urgencyScore),
|
| 730 |
+
urgency_flags: Array.from(new Set(urgencyFlags)),
|
| 731 |
+
urgency_evidence: urgencyEvidence,
|
| 732 |
+
sentiment_score: Math.round(sentimentScore * 10000) / 10000,
|
| 733 |
+
sentiment_raw_score: Math.round(rawSentiment * 10000) / 10000,
|
| 734 |
+
sentiment_label: sentimentLabelFromScore(sentimentScore),
|
| 735 |
+
sentiment_evidence: sentimentEvidence,
|
| 736 |
+
};
|
| 737 |
+
}
|
| 738 |
+
|
| 739 |
+
|
| 740 |
+
// -- Seeded PRNG (deterministic per text) --------------
|
| 741 |
+
function hashText(str) {
|
| 742 |
+
let h = 0;
|
| 743 |
+
for (let i = 0; i < str.length; i++) {
|
| 744 |
+
h = ((h << 5) - h + str.charCodeAt(i)) | 0;
|
| 745 |
+
}
|
| 746 |
+
return Math.abs(h);
|
| 747 |
+
}
|
| 748 |
+
|
| 749 |
+
function seededRandom(seed) {
|
| 750 |
+
let s = seed;
|
| 751 |
+
return function() {
|
| 752 |
+
s = (s * 1664525 + 1013904223) & 0xffffffff;
|
| 753 |
+
return (s >>> 0) / 0xffffffff;
|
| 754 |
+
};
|
| 755 |
+
}
|
| 756 |
+
|
| 757 |
+
// -- Simulation (when API is offline) ------------------
|
| 758 |
+
function simulateRouting(text, extraPayload = {}) {
|
| 759 |
+
const t = text.toLowerCase().trim();
|
| 760 |
+
const marker = t.match(/\[clarification:\s*([a-z_]+)\s*-\s*([^\]]+)\]/);
|
| 761 |
+
const clarificationTarget = extraPayload.clarification_target || (marker && marker[1]);
|
| 762 |
+
const clarificationChoice = extraPayload.clarification_choice || (marker && marker[2]);
|
| 763 |
+
const validTargets = Object.keys(CAT_COLORS);
|
| 764 |
+
|
| 765 |
+
if (clarificationTarget && validTargets.includes(clarificationTarget)) {
|
| 766 |
+
const allProbs = {};
|
| 767 |
+
validTargets.forEach(cat => { allProbs[cat] = cat === clarificationTarget ? 0.9 : 0.0143; });
|
| 768 |
+
const demoSignals = inferDemoSignals(t);
|
| 769 |
+
return {
|
| 770 |
+
action: 'route',
|
| 771 |
+
confidence: 0.9,
|
| 772 |
+
entropy: 0.35,
|
| 773 |
+
margin: 0.75,
|
| 774 |
+
top_category: clarificationTarget,
|
| 775 |
+
all_probs: allProbs,
|
| 776 |
+
top_two_classes: [clarificationTarget, validTargets.find(cat => cat !== clarificationTarget)],
|
| 777 |
+
queue: clarificationTarget,
|
| 778 |
+
reason: `Clarification answer resolved the ambiguity toward ${clarificationTarget}.`,
|
| 779 |
+
clarification_applied: true,
|
| 780 |
+
clarification_choice: clarificationChoice,
|
| 781 |
+
sla_breach_probability: Math.min(0.95, 0.15 + (demoSignals.urgency_score * 0.45)),
|
| 782 |
+
urgency_score: demoSignals.urgency_score,
|
| 783 |
+
features: {
|
| 784 |
+
...demoSignals,
|
| 785 |
+
text_complexity_score: Math.round(text.split(' ').length / 5 * 100) / 100,
|
| 786 |
+
},
|
| 787 |
+
latency_ms: 28 + (hashText(t) % 20),
|
| 788 |
+
};
|
| 789 |
+
}
|
| 790 |
+
|
| 791 |
+
// Basic validation in simulation to match real API behavior
|
| 792 |
+
if (t.length < 10) {
|
| 793 |
+
const greetings = ['hi', 'hello', 'hey', 'test'];
|
| 794 |
+
if (greetings.some(g => t.startsWith(g))) {
|
| 795 |
+
return {
|
| 796 |
+
action: 'invalid_input',
|
| 797 |
+
error_type: 'greeting',
|
| 798 |
+
response: "Hi there! Could you describe the issue you're experiencing? We're here to help."
|
| 799 |
+
};
|
| 800 |
+
}
|
| 801 |
+
return {
|
| 802 |
+
action: 'invalid_input',
|
| 803 |
+
error_type: 'too_short',
|
| 804 |
+
response: "Could you share a bit more detail about your issue? We're here to help."
|
| 805 |
+
};
|
| 806 |
+
}
|
| 807 |
+
|
| 808 |
+
const rng = seededRandom(hashText(t)); // deterministic per text
|
| 809 |
+
|
| 810 |
+
const scores = {
|
| 811 |
+
billing: 0.02, technical_support: 0.02, account_management: 0.02,
|
| 812 |
+
feature_request: 0.02, compliance_legal: 0.02, onboarding: 0.02,
|
| 813 |
+
general_inquiry: 0.02, churn_risk: 0.02,
|
| 814 |
+
};
|
| 815 |
+
|
| 816 |
+
// Simple keyword scoring
|
| 817 |
+
const kw = {
|
| 818 |
+
billing: ['invoice','billing','payment','charge','refund','price','cost','subscription','plan','pricing','credit'],
|
| 819 |
+
technical_support: ['error','bug','broken','crash','fix','api','endpoint','500','timeout','issue','not working','failed'],
|
| 820 |
+
account_management: ['account','user','access','permission','settings','profile','password','role'],
|
| 821 |
+
feature_request: ['feature','add','implement','suggest','request','capability','enhancement','wish','could you'],
|
| 822 |
+
compliance_legal: ['gdpr','compliance','audit','regulation','privacy','security','data protection','legal'],
|
| 823 |
+
onboarding: ['new user','setup','getting started','onboarding','first time','just signed up','configure','install'],
|
| 824 |
+
general_inquiry: ['how do','what is','question','information','help','guide','documentation'],
|
| 825 |
+
churn_risk: ['cancel','switch','competitor','alternative','frustrated','unacceptable','leaving','terminate','fed up','last straw'],
|
| 826 |
+
};
|
| 827 |
+
|
| 828 |
+
Object.entries(kw).forEach(([cat, words]) => {
|
| 829 |
+
words.forEach(w => { if (t.includes(w)) scores[cat] += 0.15 + rng() * 0.05; });
|
| 830 |
+
});
|
| 831 |
+
|
| 832 |
+
// Normalize
|
| 833 |
+
const total = Object.values(scores).reduce((a, b) => a + b, 0);
|
| 834 |
+
Object.keys(scores).forEach(k => scores[k] /= total);
|
| 835 |
+
|
| 836 |
+
// Add small deterministic noise (simulate MC Dropout variance)
|
| 837 |
+
Object.keys(scores).forEach(k => {
|
| 838 |
+
scores[k] += (rng() - 0.5) * 0.03;
|
| 839 |
+
scores[k] = Math.max(0.001, scores[k]);
|
| 840 |
+
});
|
| 841 |
+
const total2 = Object.values(scores).reduce((a, b) => a + b, 0);
|
| 842 |
+
Object.keys(scores).forEach(k => scores[k] /= total2);
|
| 843 |
+
|
| 844 |
+
const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
|
| 845 |
+
const confidence = sorted[0][1];
|
| 846 |
+
const entropy = -Object.values(scores).reduce((s, p) => s + p * Math.log(p + 1e-9), 0);
|
| 847 |
+
const topCat = sorted[0][0];
|
| 848 |
+
const topTwo = [sorted[0][0], sorted[1][0]];
|
| 849 |
+
const margin = sorted[0][1] - sorted[1][1];
|
| 850 |
+
|
| 851 |
+
let action, reason;
|
| 852 |
+
const critical_labels = ['compliance_legal', 'account_management'];
|
| 853 |
+
|
| 854 |
+
if (critical_labels.includes(topCat)) {
|
| 855 |
+
if (confidence >= 0.90 && margin >= 0.35 && entropy < 0.60) {
|
| 856 |
+
action = 'route';
|
| 857 |
+
reason = `- Safe to auto-route sensitive intent<br>- Confidence: ${(confidence*100).toFixed(1)}%<br>- Margin: ${margin.toFixed(2)}`;
|
| 858 |
+
} else {
|
| 859 |
+
action = 'escalate';
|
| 860 |
+
reason = `- Escalated sensitive intent (${topCat.replace(/_/g,' ')})<br>- Strict confidence/margin threshold not met`;
|
| 861 |
+
}
|
| 862 |
+
} else {
|
| 863 |
+
if (confidence >= 0.85 && margin >= 0.25 && entropy < 0.70) {
|
| 864 |
+
action = 'route';
|
| 865 |
+
reason = `- Strong dominant intent<br>- Confidence: ${(confidence*100).toFixed(1)}%<br>- Margin: ${margin.toFixed(2)}<br>- Safe to auto-route`;
|
| 866 |
+
} else if (confidence >= 0.60 && entropy < 1.05) {
|
| 867 |
+
action = 'clarify';
|
| 868 |
+
reason = `- Medium ambiguity detected<br>- Clarification needed between ${topTwo[0].replace(/_/g,' ')} and ${topTwo[1].replace(/_/g,' ')}<br>- Margin: ${margin.toFixed(2)}`;
|
| 869 |
+
} else {
|
| 870 |
+
action = 'escalate';
|
| 871 |
+
reason = `- High ambiguity / Low confidence (${(confidence*100).toFixed(1)}%)<br>- Multiple overlapping intents detected<br>- Human triage needed`;
|
| 872 |
+
}
|
| 873 |
+
}
|
| 874 |
+
|
| 875 |
+
// Clarification question
|
| 876 |
+
let clarification = null;
|
| 877 |
+
if (action === 'clarify') {
|
| 878 |
+
const questions = {
|
| 879 |
+
'billing+technical_support': { question_text: 'Is the main issue related to (A) a software error, or (B) your billing or invoice?', options: ['Software error','Billing/invoice'], expected_gain: 0.71 },
|
| 880 |
+
'technical_support+billing': { question_text: 'Is the main issue related to (A) a software error, or (B) your billing or invoice?', options: ['Software error','Billing/invoice'], expected_gain: 0.71 },
|
| 881 |
+
'feature_request+technical_support': { question_text: 'Are you reporting something broken, or requesting a new capability?', options: ['Something broken','New feature'], expected_gain: 0.68 },
|
| 882 |
+
'technical_support+feature_request': { question_text: 'Are you reporting something broken, or requesting a new capability?', options: ['Something broken','New feature'], expected_gain: 0.68 },
|
| 883 |
+
'churn_risk+account_management': { question_text: 'Are you looking to change your plan, or do you have concerns about continuing?', options: ['Change plan','Concerns about continuing'], expected_gain: 0.74 },
|
| 884 |
+
'account_management+churn_risk': { question_text: 'Are you looking to change your plan, or do you have concerns about continuing?', options: ['Change plan','Concerns about continuing'], expected_gain: 0.74 },
|
| 885 |
+
'onboarding+technical_support': { question_text: 'Is this affecting a new user, or an existing user?', options: ['New user','Existing user'], expected_gain: 0.65 },
|
| 886 |
+
'technical_support+onboarding': { question_text: 'Is this affecting a new user, or an existing user?', options: ['New user','Existing user'], expected_gain: 0.65 },
|
| 887 |
+
'compliance_legal+billing': { question_text: 'Does this relate to a regulatory requirement, or to payment/invoicing?', options: ['Regulatory','Payment'], expected_gain: 0.72 },
|
| 888 |
+
'billing+compliance_legal': { question_text: 'Does this relate to a regulatory requirement, or to payment/invoicing?', options: ['Regulatory','Payment'], expected_gain: 0.72 },
|
| 889 |
+
'technical_support+general_inquiry': { question_text: 'Is this a specific technical problem, or a general question about how something works?', options: ['Specific problem','General question'], expected_gain: 0.66 },
|
| 890 |
+
'general_inquiry+technical_support': { question_text: 'Is this a specific technical problem, or a general question about how something works?', options: ['Specific problem','General question'], expected_gain: 0.66 },
|
| 891 |
+
'billing+general_inquiry': { question_text: 'Is your question about a specific charge on your account, or general pricing information?', options: ['Specific charge','General pricing'], expected_gain: 0.64 },
|
| 892 |
+
'general_inquiry+billing': { question_text: 'Is your question about a specific charge on your account, or general pricing information?', options: ['Specific charge','General pricing'], expected_gain: 0.64 },
|
| 893 |
+
'churn_risk+technical_support': { question_text: 'Is the main concern a technical problem you need fixed, or are you considering leaving the platform?', options: ['Technical problem','Considering leaving'], expected_gain: 0.76 },
|
| 894 |
+
'technical_support+churn_risk': { question_text: 'Is the main concern a technical problem you need fixed, or are you considering leaving the platform?', options: ['Technical problem','Considering leaving'], expected_gain: 0.76 },
|
| 895 |
+
};
|
| 896 |
+
const key = topTwo[0] + '+' + topTwo[1];
|
| 897 |
+
clarification = questions[key] || {
|
| 898 |
+
question_text: 'Could you specify whether this is about a technical issue or an account/billing matter?',
|
| 899 |
+
options: ['Technical issue', 'Account/billing'], expected_gain: 0.62,
|
| 900 |
+
};
|
| 901 |
+
clarification.question_id = 'Q_SIM';
|
| 902 |
+
}
|
| 903 |
+
|
| 904 |
+
const demoSignals = inferDemoSignals(t);
|
| 905 |
+
|
| 906 |
+
// SLA - deterministic based on text features
|
| 907 |
+
const outageWords = ['down', 'outage', 'crash', 'failing', 'blocked'];
|
| 908 |
+
const outageFlags = outageWords.filter(w => t.includes(w));
|
| 909 |
+
const slaBase = 0.15
|
| 910 |
+
+ (demoSignals.sentiment_score < -0.3 ? 0.2 : 0)
|
| 911 |
+
+ (demoSignals.urgency_score * 0.45)
|
| 912 |
+
+ (outageFlags.length * 0.15);
|
| 913 |
+
const slaBreach = Math.min(Math.round(slaBase * 1000) / 1000, 0.95);
|
| 914 |
+
|
| 915 |
+
return {
|
| 916 |
+
action, confidence: Math.round(confidence * 10000) / 10000,
|
| 917 |
+
entropy: Math.round(entropy * 10000) / 10000,
|
| 918 |
+
margin: Math.round(margin * 10000) / 10000,
|
| 919 |
+
top_category: topCat, all_probs: scores,
|
| 920 |
+
top_two_classes: topTwo, queue: topCat,
|
| 921 |
+
reason, clarification,
|
| 922 |
+
sla_breach_probability: slaBreach,
|
| 923 |
+
urgency_score: demoSignals.urgency_score,
|
| 924 |
+
features: {
|
| 925 |
+
...demoSignals,
|
| 926 |
+
text_complexity_score: Math.round(text.split(' ').length / 5 * 100) / 100,
|
| 927 |
+
},
|
| 928 |
+
latency_ms: 38 + (hashText(t) % 30),
|
| 929 |
+
};
|
| 930 |
+
}
|