Asmitha-28 commited on
Commit
f9f1893
·
verified ·
1 Parent(s): 2612bdf

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ dashboard/web/entropy_map.png filter=lfs diff=lfs merge=lfs -text
dashboard/app.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # dashboard/app.py
2
+ # SupportMind Streamlit Dashboard
3
+ # Asmitha · 2026
4
+
5
+ import streamlit as st
6
+ import sys, os, json
7
+ import numpy as np
8
+
9
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'src'))
10
+
11
+ st.set_page_config(page_title="SupportMind", page_icon="🧠", layout="wide")
12
+
13
+ # Custom CSS
14
+ st.markdown("""
15
+ <style>
16
+ .stApp { background-color: #0a0a0f; }
17
+ .metric-card { background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08);
18
+ border-radius: 12px; padding: 20px; text-align: center; }
19
+ .action-route { color: #22c55e; font-size: 28px; font-weight: 800; }
20
+ .action-clarify { color: #eab308; font-size: 28px; font-weight: 800; }
21
+ .action-escalate { color: #ef4444; font-size: 28px; font-weight: 800; }
22
+ </style>
23
+ """, unsafe_allow_html=True)
24
+
25
+ st.title("🧠 SupportMind")
26
+ st.caption("Confidence-Gated Support Intelligence for B2B SaaS")
27
+
28
+ # Sidebar
29
+ st.sidebar.header("⚙️ Configuration")
30
+ mc_passes = st.sidebar.slider("MC Dropout Passes", 5, 50, 20)
31
+ route_thresh = st.sidebar.slider("Route Threshold", 0.5, 0.95, 0.80, 0.05)
32
+ clarify_thresh = st.sidebar.slider("Clarify Threshold", 0.3, 0.8, 0.55, 0.05)
33
+ entropy_max = st.sidebar.slider("Max Entropy (Route)", 0.1, 1.0, 0.35, 0.05)
34
+
35
+ # Hero metrics
36
+ col1, col2, col3, col4 = st.columns(4)
37
+ col1.metric("Routing Accuracy", "89.1%", "+16.8pp")
38
+ col2.metric("Ambiguous Gain", "+32.3%", "vs baseline")
39
+ col3.metric("Annual Savings", "$756K", "per 10K tickets/mo")
40
+ col4.metric("Pipeline Latency", "45ms", "20-pass MC Dropout")
41
+
42
+ st.divider()
43
+
44
+ # Ticket Input
45
+ st.header("🎯 Route a Ticket")
46
+
47
+ presets = {
48
+ "Billing Issue": "My invoice from last month shows $299 but my plan is $199. Please fix this billing error immediately.",
49
+ "Technical Bug": "The API endpoint /v2/export returns a 500 error when batch size exceeds 1000 records.",
50
+ "Ambiguous Ticket": "Hey, we have issues with the export function since last Tuesday. Also our invoice looks incorrect. We are considering upgrading but want this sorted first.",
51
+ "Churn Risk": "This is the third time I'm reporting this. Still not fixed. We're looking at switching to a competitor.",
52
+ "Onboarding": "We just signed up yesterday and need help setting up SSO for our team of 50 users.",
53
+ }
54
+
55
+ preset = st.selectbox("Quick presets:", ["Custom"] + list(presets.keys()))
56
+ if preset != "Custom":
57
+ ticket_text = st.text_area("Ticket Text", value=presets[preset], height=100)
58
+ else:
59
+ ticket_text = st.text_area("Ticket Text", placeholder="Enter support ticket text...", height=100)
60
+
61
+ if st.button("⚡ Route Ticket", type="primary", use_container_width=True):
62
+ if ticket_text.strip():
63
+ with st.spinner("Running MC Dropout inference..."):
64
+ try:
65
+ from ensemble_router import EnsembleRouter
66
+ import ensemble_router as er
67
+ er.ROUTE_THRESHOLD = route_thresh
68
+ er.CLARIFY_THRESHOLD = clarify_thresh
69
+ er.ENTROPY_MAX = entropy_max
70
+
71
+ router = EnsembleRouter(device='cpu')
72
+ result = router.route(ticket_text, n_passes=mc_passes)
73
+
74
+ # Display action
75
+ action = result['action']
76
+ action_colors = {'route': '🟢', 'clarify': '🟡', 'escalate': '🔴'}
77
+ st.subheader(f"{action_colors.get(action, '')} Decision: {action.upper()}")
78
+ st.caption(result['reason'])
79
+
80
+ # Metrics
81
+ c1, c2, c3 = st.columns(3)
82
+ c1.metric("Confidence", f"{result['confidence']:.4f}")
83
+ c2.metric("Entropy", f"{result['entropy']:.4f}")
84
+ c3.metric("Top Category", result['top_category'].replace('_', ' ').title())
85
+
86
+ # Probability distribution
87
+ st.subheader("📊 Category Probabilities")
88
+ import plotly.graph_objects as go
89
+ cats = list(result['all_probs'].keys())
90
+ probs_vals = list(result['all_probs'].values())
91
+ fig = go.Figure(go.Bar(
92
+ x=probs_vals, y=[c.replace('_', ' ').title() for c in cats],
93
+ orientation='h',
94
+ marker_color=['#6366f1' if p == max(probs_vals) else '#334155' for p in probs_vals]
95
+ ))
96
+ fig.update_layout(
97
+ plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)',
98
+ font_color='#94a3b8', height=300, margin=dict(l=0,r=0,t=10,b=0),
99
+ xaxis_title="Probability"
100
+ )
101
+ st.plotly_chart(fig, use_container_width=True)
102
+
103
+ # Clarification
104
+ if action == 'clarify':
105
+ st.subheader("💡 Suggested Clarification")
106
+ try:
107
+ from clarification_engine import ClarificationEngine
108
+ bank_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'clarification_bank.json')
109
+ clar = ClarificationEngine(bank_path)
110
+ probs_arr = np.array(list(result['all_probs'].values()))
111
+ q = clar.select_question(probs_arr, result['top_two_classes'])
112
+ st.info(f"**{q['question_text']}**")
113
+ for opt in q.get('options', []):
114
+ st.button(opt, disabled=True)
115
+ st.caption(f"Expected information gain: {q['expected_gain']:.4f}")
116
+ except Exception as e:
117
+ st.warning(f"Could not load clarification: {e}")
118
+
119
+ # SLA
120
+ st.subheader("🚨 SLA Breach Prediction")
121
+ try:
122
+ from sla_predictor import SLABreachPredictor
123
+ from feature_extraction import FeatureExtractor
124
+ feat_ext = FeatureExtractor()
125
+ features = feat_ext.extract(ticket_text)
126
+ sla_path = os.path.join(os.path.dirname(__file__), '..', 'models', 'sla_predictor', 'sla_xgb.json')
127
+ sla = SLABreachPredictor(sla_path)
128
+ sla_features = {
129
+ 'text_complexity_score': features['text_complexity_score'],
130
+ 'agent_queue_depth': 15, 'customer_tier': 3,
131
+ 'hour_of_day': 14, 'day_of_week': 2,
132
+ 'similar_ticket_avg_hrs': 4.5,
133
+ 'sentiment_score': features['sentiment_score'],
134
+ 'repeat_issue': 0, 'escalated_before': 0,
135
+ }
136
+ sla_result = sla.explain(sla_features)
137
+ sc1, sc2 = st.columns(2)
138
+ sc1.metric("Breach Probability", f"{sla_result['breach_probability']:.1%}")
139
+ sc2.metric("Risk Level", sla_result['risk_level'].upper())
140
+ if sla_result['contributing_factors']:
141
+ st.caption("Factors: " + ", ".join(sla_result['contributing_factors']))
142
+ except Exception as e:
143
+ st.warning(f"SLA prediction unavailable: {e}")
144
+
145
+ except Exception as e:
146
+ st.error(f"Error: {e}")
147
+ import traceback
148
+ st.code(traceback.format_exc())
149
+ else:
150
+ st.warning("Please enter ticket text.")
151
+
dashboard/web/app.js ADDED
@@ -0,0 +1,640 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // SupportMind Dashboard — app.js
2
+ // Interactive demo with real API calls (falls back to simulation if API unavailable)
3
+
4
+ const API_BASE = window.location.origin;
5
+ let apiOnline = false;
6
+
7
+ // Category colors
8
+ const CAT_COLORS = {
9
+ billing: '#fb923c', technical_support: '#8083ff', account_management: '#89ceff',
10
+ feature_request: '#c0c1ff', compliance_legal: '#f87171', onboarding: '#4ade80',
11
+ general_inquiry: '#94a3b8', churn_risk: '#facc15',
12
+ };
13
+
14
+ // ── Init ──────────────────────────────────────────────
15
+ document.addEventListener('DOMContentLoaded', () => {
16
+ animateCounters();
17
+ initPresets();
18
+ initDropoutViz();
19
+ initScrollAnimations();
20
+ checkAPI();
21
+ updateLiveMetrics();
22
+ setInterval(updateLiveMetrics, 5000); // Update every 5 seconds
23
+ });
24
+
25
+ // ── Counter Animation ─────────────────────────────────
26
+ function animateCounters() {
27
+ document.querySelectorAll('.stat-card').forEach(card => {
28
+ const counter = card.querySelector('.counter');
29
+ const target = parseFloat(card.dataset.value);
30
+ const duration = 1500;
31
+ const start = performance.now();
32
+ function update(now) {
33
+ const elapsed = now - start;
34
+ const progress = Math.min(elapsed / duration, 1);
35
+ const eased = 1 - Math.pow(1 - progress, 3);
36
+ counter.textContent = Math.round(target * eased * 10) / 10;
37
+ if (progress < 1) requestAnimationFrame(update);
38
+ else counter.textContent = target;
39
+ }
40
+ requestAnimationFrame(update);
41
+ });
42
+ }
43
+
44
+ // ── Presets ────────────────────────────────────────────
45
+ // ── Live Telemetry Engine ───────────────────────────
46
+ async function updateMetrics() {
47
+ try {
48
+ const res = await fetch(`${API_BASE}/metrics`);
49
+ if (!res.ok) return;
50
+ const data = await res.json();
51
+
52
+ // Update Counter
53
+ document.getElementById('live-total').textContent = data.total_requests.toLocaleString();
54
+
55
+ // Update Model Name
56
+ document.getElementById('live-model').textContent = data.model;
57
+
58
+ // Update Distribution Bar
59
+ const dist = data.routing_distribution;
60
+ document.getElementById('dist-route').style.width = `${dist.route_pct}%`;
61
+ document.getElementById('dist-clarify').style.width = `${dist.clarify_pct}%`;
62
+ document.getElementById('dist-escalate').style.width = `${dist.escalate_pct}%`;
63
+
64
+ // Update Status Pulse
65
+ const indicator = document.getElementById('live-indicator');
66
+ indicator.style.opacity = '1';
67
+ setTimeout(() => { indicator.style.opacity = '0.8'; }, 500);
68
+
69
+ } catch (err) {
70
+ console.warn("Metrics sync failed:", err);
71
+ }
72
+ }
73
+
74
+ // ── Presets ────────────────────────────────────────────
75
+ function initPresets() {
76
+ document.querySelectorAll('.preset-btn').forEach(btn => {
77
+ btn.addEventListener('click', () => {
78
+ document.getElementById('ticket-input').value = btn.dataset.text;
79
+ });
80
+ });
81
+ }
82
+
83
+ // Initial load and interval
84
+ window.addEventListener('DOMContentLoaded', () => {
85
+ checkAPI();
86
+ initPresets();
87
+ updateMetrics();
88
+ setInterval(updateMetrics, 5000);
89
+
90
+ // Smooth scroll
91
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
92
+ anchor.addEventListener('click', function (e) {
93
+ e.preventDefault();
94
+ document.querySelector(this.getAttribute('href')).scrollIntoView({
95
+ behavior: 'smooth'
96
+ });
97
+ });
98
+ });
99
+ });
100
+
101
+ // ── MC Dropout Visualization ──────────────────────────
102
+ function initDropoutViz() {
103
+ const grid = document.getElementById('dropout-grid');
104
+ if (!grid) return;
105
+ for (let pass = 0; pass < 20; pass++) {
106
+ const col = document.createElement('div');
107
+ col.className = 'dropout-col';
108
+ for (let neuron = 0; neuron < 12; neuron++) {
109
+ const cell = document.createElement('div');
110
+ cell.className = 'dropout-cell';
111
+ const active = Math.random() > 0.15;
112
+ cell.style.background = active ? 'var(--primary)' : 'rgba(192, 193, 255, 0.05)';
113
+ cell.style.border = active ? 'none' : '1px solid rgba(192, 193, 255, 0.1)';
114
+ col.appendChild(cell);
115
+ }
116
+ grid.appendChild(col);
117
+ }
118
+ // Animate dropout
119
+ setInterval(() => {
120
+ grid.querySelectorAll('.dropout-cell').forEach(cell => {
121
+ const active = Math.random() > 0.15;
122
+ cell.style.background = active ? 'var(--primary)' : 'rgba(192, 193, 255, 0.05)';
123
+ cell.style.border = active ? 'none' : '1px solid rgba(192, 193, 255, 0.1)';
124
+ });
125
+ }, 2000);
126
+ }
127
+
128
+ // ── Scroll Animations ─────────────────────────────────
129
+ function initScrollAnimations() {
130
+ const observer = new IntersectionObserver((entries) => {
131
+ entries.forEach(e => { if (e.isIntersecting) e.target.classList.add('visible'); });
132
+ }, { threshold: 0.1 });
133
+ document.querySelectorAll('.section-header, .stat-card, .arch-stage, .bench-card, .ops-card').forEach(el => {
134
+ el.classList.add('fade-in');
135
+ observer.observe(el);
136
+ });
137
+ }
138
+
139
+ // ── API Check ─────────────────────────────────────────
140
+ async function checkAPI() {
141
+ try {
142
+ const res = await fetch(`${API_BASE}/health`, { signal: AbortSignal.timeout(2000) });
143
+ if (res.ok) {
144
+ apiOnline = true;
145
+ const statusEl = document.querySelector('.status-text');
146
+ if (statusEl) statusEl.textContent = 'API Connected';
147
+ }
148
+ } catch {
149
+ apiOnline = false;
150
+ const statusEl = document.querySelector('.status-text');
151
+ if (statusEl) statusEl.textContent = 'Demo Mode';
152
+ }
153
+ }
154
+
155
+ // ── Live Metrics ──────────────────────────────────────
156
+ async function updateLiveMetrics() {
157
+ if (!apiOnline) return;
158
+ try {
159
+ const res = await fetch(`${API_BASE}/metrics`);
160
+ const data = await res.json();
161
+
162
+ document.getElementById('live-model').textContent = data.model;
163
+ document.getElementById('live-total').textContent = data.total_requests;
164
+
165
+ const dist = data.routing_distribution;
166
+ document.getElementById('dist-route').style.width = dist.route_pct + '%';
167
+ document.getElementById('dist-clarify').style.width = dist.clarify_pct + '%';
168
+ document.getElementById('dist-escalate').style.width = dist.escalate_pct + '%';
169
+ } catch (err) {
170
+ console.warn('Metrics update failed:', err);
171
+ }
172
+ }
173
+
174
+ // ── Route Ticket ──────────────────────────────────────
175
+ async function routeTicket() {
176
+ const text = document.getElementById('ticket-input').value.trim();
177
+ if (!text) return;
178
+
179
+ const btn = document.getElementById('route-btn');
180
+ btn.innerHTML = '<span class="spinner"></span> Routing...';
181
+ btn.disabled = true;
182
+
183
+ let result;
184
+ try {
185
+ if (apiOnline) {
186
+ const res = await fetch(`${API_BASE}/route`, {
187
+ method: 'POST',
188
+ headers: { 'Content-Type': 'application/json' },
189
+ body: JSON.stringify({ text }),
190
+ });
191
+ result = await res.json();
192
+ } else {
193
+ result = simulateRouting(text);
194
+ }
195
+ displayResult(result, text);
196
+ } catch (err) {
197
+ result = simulateRouting(text);
198
+ displayResult(result, text);
199
+ }
200
+
201
+ btn.innerHTML = '<span class="btn-icon">⚡</span> Route Ticket';
202
+ btn.disabled = false;
203
+ }
204
+
205
+ // ── Display Result ────────────────────────────────────
206
+ function displayResult(r, routedText) {
207
+ // Handle edge cases
208
+ if (r.action === 'invalid_input') {
209
+ document.getElementById('result-placeholder').style.display = 'none';
210
+ const content = document.getElementById('result-content');
211
+ content.style.display = 'block';
212
+
213
+ const badge = document.getElementById('action-badge');
214
+ badge.textContent = r.error_type.toUpperCase().replace('_', ' ');
215
+ badge.className = 'action-badge clarify'; // yellow
216
+
217
+ document.getElementById('action-queue').textContent = r.response;
218
+ document.getElementById('result-reason').textContent = r.response;
219
+
220
+ // Hide gauges for invalid input
221
+ document.querySelector('.gauge-row').style.display = 'none';
222
+ document.getElementById('prob-chart').innerHTML = '';
223
+ document.getElementById('clarification-box').style.display = 'none';
224
+ const explainBtn = document.getElementById('explain-btn');
225
+ if (explainBtn) explainBtn.style.display = 'none';
226
+ document.getElementById('explanation-box').style.display = 'none';
227
+ return;
228
+ }
229
+
230
+
231
+ // Show gauges for valid input
232
+ document.querySelector('.gauge-row').style.display = 'grid';
233
+
234
+ document.getElementById('result-placeholder').style.display = 'none';
235
+ const content = document.getElementById('result-content');
236
+ content.style.display = 'block';
237
+
238
+ // Action Badge Logic
239
+ const badge = document.getElementById('action-badge');
240
+ const queue = document.getElementById('action-queue');
241
+
242
+ if (r.action === 'multi_route') {
243
+ badge.textContent = 'MULTI-ROUTE';
244
+ badge.className = 'action-badge';
245
+ badge.style.background = 'linear-gradient(90deg, var(--primary), var(--accent))';
246
+ queue.innerHTML = `
247
+ <div style="display: flex; gap: 8px; margin-top: 4px;">
248
+ <span class="tech-tag" style="background: rgba(192, 193, 255, 0.2)">Primary: ${r.primary_queue}</span>
249
+ <span class="tech-tag" style="background: rgba(255, 255, 255, 0.1)">Secondary: ${r.secondary_queue}</span>
250
+ </div>
251
+ `;
252
+ } else {
253
+ badge.textContent = r.action.toUpperCase();
254
+ badge.className = `action-badge ${r.action}`;
255
+ queue.textContent = r.action === 'route' ? `→ ${r.queue || r.top_category} queue` :
256
+ r.action === 'clarify' ? 'Needs 1 clarification question' : 'Immediate human triage';
257
+ }
258
+
259
+ // Gauges
260
+ const confPct = Math.min(r.confidence * 100, 100);
261
+ document.getElementById('conf-fill').style.width = confPct + '%';
262
+ document.getElementById('conf-value').textContent = r.confidence.toFixed(4);
263
+ const maxEnt = Math.log(8);
264
+ const entPct = Math.min((r.entropy / maxEnt) * 100, 100);
265
+ document.getElementById('ent-fill').style.width = entPct + '%';
266
+ document.getElementById('ent-value').textContent = r.entropy.toFixed(4);
267
+ if (r.margin !== undefined && document.getElementById('margin-value')) {
268
+ document.getElementById('margin-value').textContent = r.margin.toFixed(4);
269
+ }
270
+
271
+
272
+ // Prob chart
273
+ const chart = document.getElementById('prob-chart');
274
+ chart.innerHTML = '';
275
+ const probs = r.all_probs || {};
276
+ const sorted = Object.entries(probs).sort((a, b) => b[1] - a[1]);
277
+ const maxProb = sorted.length ? sorted[0][1] : 1;
278
+ sorted.forEach(([cat, prob]) => {
279
+ const row = document.createElement('div');
280
+ row.className = 'prob-row';
281
+ const pct = (prob / Math.max(maxProb, 0.01)) * 100;
282
+ row.innerHTML = `
283
+ <span class="prob-label">${cat.replace(/_/g, ' ')}</span>
284
+ <div class="prob-bar-track"><div class="prob-bar-fill" style="width:${pct}%;background:${CAT_COLORS[cat] || '#6366f1'}"></div></div>
285
+ <span class="prob-val">${(prob * 100).toFixed(1)}%</span>`;
286
+ chart.appendChild(row);
287
+ });
288
+
289
+ // Clarification
290
+ const clarBox = document.getElementById('clarification-box');
291
+ if (r.action === 'clarify' && r.clarification) {
292
+ clarBox.style.display = 'block';
293
+ document.getElementById('clarify-question').textContent = r.clarification.question_text;
294
+ const optEl = document.getElementById('clarify-options');
295
+ optEl.innerHTML = '';
296
+ (r.clarification.options || []).forEach(o => {
297
+ const btn = document.createElement('button');
298
+ btn.className = 'option-btn';
299
+ btn.textContent = o;
300
+ btn.onclick = () => {
301
+ // Provide visual feedback
302
+ document.querySelectorAll('#clarify-options .option-btn').forEach(b => b.disabled = true);
303
+ btn.style.background = 'var(--primary)';
304
+ btn.style.color = '#fff';
305
+
306
+ // Append clarification to input
307
+ const input = document.getElementById('ticket-input');
308
+ input.value = input.value.trim() + '\n\n[Clarification provided: ' + o + ']';
309
+
310
+ // Re-route with new context after a short delay
311
+ setTimeout(() => {
312
+ routeTicket();
313
+ }, 800);
314
+ };
315
+ optEl.appendChild(btn);
316
+ });
317
+
318
+ // Remove existing badge if any
319
+ const existingBadge = document.getElementById('source-badge');
320
+ if (existingBadge) existingBadge.remove();
321
+
322
+ // After displaying the question, add source badge
323
+ const sourceBadge = document.createElement('div');
324
+ sourceBadge.id = 'source-badge';
325
+ sourceBadge.style.cssText = 'font-size:11px;margin-top:8px;opacity:0.6;';
326
+ sourceBadge.textContent = r.clarification.source === 'llm_groq'
327
+ ? '⚡ Generated by LLaMA3 via Groq'
328
+ : '📋 Selected from template bank';
329
+ document.getElementById('clarification-box').appendChild(sourceBadge);
330
+
331
+ document.getElementById('clarify-gain').textContent =
332
+ `Expected information gain: ${r.clarification.expected_gain?.toFixed(4) || 'N/A'}`;
333
+ } else {
334
+ clarBox.style.display = 'none';
335
+ }
336
+
337
+ // Signals
338
+ const slaRiskVal = r.sla_risk || r.sla_breach_probability || 0;
339
+ const slaPct = slaRiskVal * 100;
340
+ document.getElementById('sla-value').textContent = slaPct.toFixed(1) + '%';
341
+ document.getElementById('sla-fill').style.width = slaPct + '%';
342
+ document.getElementById('sla-fill').style.background =
343
+ slaPct > 65 ? 'var(--red)' : slaPct > 35 ? 'var(--yellow)' : 'var(--green)';
344
+
345
+ const feat = r.features || {};
346
+ const sent = feat.sentiment_score;
347
+ document.getElementById('sentiment-value').textContent =
348
+ sent !== undefined ? (sent > 0.2 ? '😊 ' : sent < -0.2 ? '😤 ' : '😐 ') + sent.toFixed(2) : '—';
349
+
350
+ const urgScore = r.urgency_score || feat.urgency_score || 0;
351
+ const urgencyCard = document.getElementById('urgency-value').parentElement;
352
+ if (urgScore > 0.6) {
353
+ document.getElementById('urgency-value').innerHTML = '<span style="color: var(--red); font-weight: bold; animation: pulse 1.5s infinite;">🚨 CRITICAL</span>';
354
+ urgencyCard.style.border = '1px solid var(--red)';
355
+ urgencyCard.style.boxShadow = '0 0 15px rgba(248, 113, 113, 0.2)';
356
+ } else if (urgScore > 0.2) {
357
+ document.getElementById('urgency-value').innerHTML = '<span style="color: var(--yellow); font-weight: bold;">⚡ HIGH</span>';
358
+ urgencyCard.style.border = '1px solid var(--yellow)';
359
+ urgencyCard.style.boxShadow = '';
360
+ } else {
361
+ document.getElementById('urgency-value').textContent = '🟢 Normal';
362
+ urgencyCard.style.border = '';
363
+ urgencyCard.style.boxShadow = '';
364
+ }
365
+
366
+ document.getElementById('latency-value').textContent =
367
+ r.latency_ms ? r.latency_ms + 'ms' : '—';
368
+
369
+ // Reason
370
+ let decisionReason = '';
371
+ if (r.action === 'multi_route') {
372
+ decisionReason = `Multiple distinct intents detected in the request. Primary intent is <strong>${r.primary_queue}</strong>, secondary is <strong>${r.secondary_queue}</strong>.`;
373
+ } else if (r.action === 'clarify') {
374
+ 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.`;
375
+ } else if (r.action === 'escalate') {
376
+ decisionReason = `Low model confidence detected (${(r.confidence * 100).toFixed(1)}%). Routing directly to human experts to ensure accuracy.`;
377
+ } else {
378
+ decisionReason = `High-confidence intent detected: <strong>${r.top_category}</strong>. Automatically routing to specialized queue.`;
379
+ }
380
+
381
+ document.getElementById('result-reason').innerHTML = `
382
+ <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;">
383
+ <div style="font-size: 11px; text-transform: uppercase; color: var(--primary); margin-bottom: 8px; font-weight: 600;">Decision Reason</div>
384
+ <div style="font-size: 13px; color: var(--on-surface-variant); line-height: 1.5;">${decisionReason}</div>
385
+ </div>
386
+ `;
387
+
388
+ // Show explain button for valid input
389
+ const explainBtn = document.getElementById('explain-btn');
390
+ if (explainBtn) {
391
+ explainBtn.style.display = 'flex';
392
+ explainBtn.dataset.text = routedText || document.getElementById('ticket-input').value;
393
+ explainBtn.dataset.category = r.top_category;
394
+ }
395
+ document.getElementById('explanation-box').style.display = 'none';
396
+ }
397
+
398
+ // ── Explain Decision (SHAP) ───────────────────────────
399
+ async function explainDecision() {
400
+ const btn = document.getElementById('explain-btn');
401
+ const text = btn.dataset.text;
402
+ const targetClass = btn.dataset.category;
403
+
404
+ btn.innerHTML = '<span class="spinner"></span> Analyzing tokens...';
405
+ btn.disabled = true;
406
+
407
+ try {
408
+ let result;
409
+ if (apiOnline) {
410
+ const res = await fetch(`${API_BASE}/explain`, {
411
+ method: 'POST',
412
+ headers: { 'Content-Type': 'application/json' },
413
+ body: JSON.stringify({ text, target_class: targetClass }),
414
+ });
415
+ result = await res.json();
416
+ } else {
417
+ // Simulate SHAP for demo mode
418
+ result = simulateSHAP(text);
419
+ }
420
+
421
+ renderSHAP(result);
422
+ } catch (err) {
423
+ console.error('SHAP failed:', err);
424
+ renderSHAP(simulateSHAP(text));
425
+ }
426
+
427
+ btn.innerHTML = '<span class="material-symbols-outlined btn-icon">query_stats</span> Analyze Decision (SHAP)';
428
+ btn.disabled = false;
429
+ }
430
+
431
+ function renderSHAP(data) {
432
+ const box = document.getElementById('explanation-box');
433
+ const textEl = document.getElementById('explain-text');
434
+ box.style.display = 'block';
435
+ textEl.innerHTML = '';
436
+
437
+ if (data.error) {
438
+ textEl.textContent = 'Error generating explanation: ' + data.error;
439
+ return;
440
+ }
441
+
442
+ const tokens = data.tokens;
443
+ const values = data.values;
444
+
445
+ tokens.forEach((token, i) => {
446
+ const val = values[i];
447
+ const span = document.createElement('span');
448
+ span.className = 'shap-token';
449
+ span.textContent = token.replace('##', ''); // Simple handling for subwords
450
+
451
+ // Normalize opacity based on value
452
+ const absVal = Math.abs(val);
453
+ const opacity = Math.min(absVal * 5, 0.8); // Scale for visibility
454
+
455
+ if (val > 0) {
456
+ span.style.background = `rgba(74, 222, 128, ${opacity})`;
457
+ span.style.borderBottom = `2px solid rgba(74, 222, 128, ${opacity + 0.2})`;
458
+ } else if (val < 0) {
459
+ span.style.background = `rgba(248, 113, 113, ${opacity})`;
460
+ span.style.borderBottom = `2px solid rgba(248, 113, 113, ${opacity + 0.2})`;
461
+ }
462
+
463
+ textEl.appendChild(span);
464
+ textEl.appendChild(document.createTextNode(' '));
465
+ });
466
+
467
+ box.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
468
+ }
469
+
470
+ function simulateSHAP(text) {
471
+ const tokens = text.split(/\s+/);
472
+ const values = tokens.map(() => (Math.random() - 0.4) * 0.2);
473
+ return { tokens, values };
474
+ }
475
+
476
+
477
+ // ── Seeded PRNG (deterministic per text) ──────────────
478
+ function hashText(str) {
479
+ let h = 0;
480
+ for (let i = 0; i < str.length; i++) {
481
+ h = ((h << 5) - h + str.charCodeAt(i)) | 0;
482
+ }
483
+ return Math.abs(h);
484
+ }
485
+
486
+ function seededRandom(seed) {
487
+ let s = seed;
488
+ return function() {
489
+ s = (s * 1664525 + 1013904223) & 0xffffffff;
490
+ return (s >>> 0) / 0xffffffff;
491
+ };
492
+ }
493
+
494
+ // ── Simulation (when API is offline) ──────────────────
495
+ function simulateRouting(text) {
496
+ const t = text.toLowerCase().trim();
497
+
498
+ // Basic validation in simulation to match real API behavior
499
+ if (t.length < 10) {
500
+ const greetings = ['hi', 'hello', 'hey', 'test'];
501
+ if (greetings.some(g => t.startsWith(g))) {
502
+ return {
503
+ action: 'invalid_input',
504
+ error_type: 'greeting',
505
+ response: "Hi there! 👋 Could you describe the issue you're experiencing? We're here to help."
506
+ };
507
+ }
508
+ return {
509
+ action: 'invalid_input',
510
+ error_type: 'too_short',
511
+ response: "Could you share a bit more detail about your issue? We're here to help."
512
+ };
513
+ }
514
+
515
+ const rng = seededRandom(hashText(t)); // deterministic per text
516
+
517
+ const scores = {
518
+ billing: 0.02, technical_support: 0.02, account_management: 0.02,
519
+ feature_request: 0.02, compliance_legal: 0.02, onboarding: 0.02,
520
+ general_inquiry: 0.02, churn_risk: 0.02,
521
+ };
522
+
523
+ // Simple keyword scoring
524
+ const kw = {
525
+ billing: ['invoice','billing','payment','charge','refund','price','cost','subscription','plan','pricing','credit'],
526
+ technical_support: ['error','bug','broken','crash','fix','api','endpoint','500','timeout','issue','not working','failed'],
527
+ account_management: ['account','user','access','permission','settings','profile','password','role'],
528
+ feature_request: ['feature','add','implement','suggest','request','capability','enhancement','wish','could you'],
529
+ compliance_legal: ['gdpr','compliance','audit','regulation','privacy','security','data protection','legal'],
530
+ onboarding: ['new user','setup','getting started','onboarding','first time','just signed up','configure','install'],
531
+ general_inquiry: ['how do','what is','question','information','help','guide','documentation'],
532
+ churn_risk: ['cancel','switch','competitor','alternative','frustrated','unacceptable','leaving','terminate','fed up','last straw'],
533
+ };
534
+
535
+ Object.entries(kw).forEach(([cat, words]) => {
536
+ words.forEach(w => { if (t.includes(w)) scores[cat] += 0.15 + rng() * 0.05; });
537
+ });
538
+
539
+ // Normalize
540
+ const total = Object.values(scores).reduce((a, b) => a + b, 0);
541
+ Object.keys(scores).forEach(k => scores[k] /= total);
542
+
543
+ // Add small deterministic noise (simulate MC Dropout variance)
544
+ Object.keys(scores).forEach(k => {
545
+ scores[k] += (rng() - 0.5) * 0.03;
546
+ scores[k] = Math.max(0.001, scores[k]);
547
+ });
548
+ const total2 = Object.values(scores).reduce((a, b) => a + b, 0);
549
+ Object.keys(scores).forEach(k => scores[k] /= total2);
550
+
551
+ const sorted = Object.entries(scores).sort((a, b) => b[1] - a[1]);
552
+ const confidence = sorted[0][1];
553
+ const entropy = -Object.values(scores).reduce((s, p) => s + p * Math.log(p + 1e-9), 0);
554
+ const topCat = sorted[0][0];
555
+ const topTwo = [sorted[0][0], sorted[1][0]];
556
+ const margin = sorted[0][1] - sorted[1][1];
557
+
558
+ let action, reason;
559
+ const critical_labels = ['compliance_legal', 'account_management'];
560
+
561
+ if (critical_labels.includes(topCat)) {
562
+ if (confidence >= 0.90 && margin >= 0.35 && entropy < 0.60) {
563
+ action = 'route';
564
+ reason = `• Safe to auto-route sensitive intent<br>• Confidence: ${(confidence*100).toFixed(1)}%<br>• Margin: ${margin.toFixed(2)}`;
565
+ } else {
566
+ action = 'escalate';
567
+ reason = `• Escalated sensitive intent (${topCat.replace(/_/g,' ')})<br>• Strict confidence/margin threshold not met`;
568
+ }
569
+ } else {
570
+ if (confidence >= 0.85 && margin >= 0.25 && entropy < 0.70) {
571
+ action = 'route';
572
+ reason = `• Strong dominant intent<br>• Confidence: ${(confidence*100).toFixed(1)}%<br>• Margin: ${margin.toFixed(2)}<br>• Safe to auto-route`;
573
+ } else if (confidence >= 0.60 && entropy < 1.05) {
574
+ action = 'clarify';
575
+ reason = `• Medium ambiguity detected<br>• Clarification needed between ${topTwo[0].replace(/_/g,' ')} and ${topTwo[1].replace(/_/g,' ')}<br>• Margin: ${margin.toFixed(2)}`;
576
+ } else {
577
+ action = 'escalate';
578
+ reason = `• High ambiguity / Low confidence (${(confidence*100).toFixed(1)}%)<br>• Multiple overlapping intents detected<br>• Human triage needed`;
579
+ }
580
+ }
581
+
582
+ // Clarification question
583
+ let clarification = null;
584
+ if (action === 'clarify') {
585
+ const questions = {
586
+ '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 },
587
+ '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 },
588
+ '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 },
589
+ '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 },
590
+ '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 },
591
+ '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 },
592
+ 'onboarding+technical_support': { question_text: 'Is this affecting a new user, or an existing user?', options: ['New user','Existing user'], expected_gain: 0.65 },
593
+ 'technical_support+onboarding': { question_text: 'Is this affecting a new user, or an existing user?', options: ['New user','Existing user'], expected_gain: 0.65 },
594
+ 'compliance_legal+billing': { question_text: 'Does this relate to a regulatory requirement, or to payment/invoicing?', options: ['Regulatory','Payment'], expected_gain: 0.72 },
595
+ 'billing+compliance_legal': { question_text: 'Does this relate to a regulatory requirement, or to payment/invoicing?', options: ['Regulatory','Payment'], expected_gain: 0.72 },
596
+ '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 },
597
+ '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 },
598
+ '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 },
599
+ '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 },
600
+ '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 },
601
+ '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 },
602
+ };
603
+ const key = topTwo[0] + '+' + topTwo[1];
604
+ clarification = questions[key] || {
605
+ question_text: 'Could you specify whether this is about a technical issue or an account/billing matter?',
606
+ options: ['Technical issue', 'Account/billing'], expected_gain: 0.62,
607
+ };
608
+ clarification.question_id = 'Q_SIM';
609
+ }
610
+
611
+ // Sentiment (basic)
612
+ const negWords = ['frustrated','broken','terrible','angry','worst','cancel','bad','issue','error'];
613
+ const posWords = ['great','thanks','love','good','happy','please'];
614
+ let sentScore = 0;
615
+ negWords.forEach(w => { if (t.includes(w)) sentScore -= 0.25; });
616
+ posWords.forEach(w => { if (t.includes(w)) sentScore += 0.2; });
617
+ sentScore = Math.max(-1, Math.min(1, sentScore));
618
+
619
+ // Urgency
620
+ const urgencyWords = ['urgent','asap','immediately','critical','blocking','production down'];
621
+ const urgencyFlags = urgencyWords.filter(w => t.includes(w));
622
+
623
+ // SLA — deterministic based on text features
624
+ const outageWords = ['down', 'outage', 'crash', 'failing', 'blocked'];
625
+ const outageFlags = outageWords.filter(w => t.includes(w));
626
+ const slaBase = 0.15 + (sentScore < -0.3 ? 0.2 : 0) + (urgencyFlags.length * 0.15) + (outageFlags.length * 0.2);
627
+ const slaBreach = Math.min(Math.round(slaBase * 1000) / 1000, 0.95);
628
+
629
+ return {
630
+ action, confidence: Math.round(confidence * 10000) / 10000,
631
+ entropy: Math.round(entropy * 10000) / 10000,
632
+ margin: Math.round(margin * 10000) / 10000,
633
+ top_category: topCat, all_probs: scores,
634
+ top_two_classes: topTwo, queue: topCat,
635
+ reason, clarification,
636
+ sla_breach_probability: slaBreach,
637
+ features: { sentiment_score: sentScore, urgency_flags: urgencyFlags, text_complexity_score: Math.round(text.split(' ').length / 5 * 100) / 100 },
638
+ latency_ms: 38 + (hashText(t) % 30),
639
+ };
640
+ }
dashboard/web/entropy_map.png ADDED

Git LFS Details

  • SHA256: a1d6d23cbbb4c202a1df4ef1e57ca343884ffc2fdb7a1290858de1f5b3ada598
  • Pointer size: 131 Bytes
  • Size of remote file: 826 kB
dashboard/web/index.html ADDED
@@ -0,0 +1,521 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>AetherFlow AI | SupportMind Engine</title>
7
+ <meta name="description" content="AI-powered B2B SaaS ticket routing with MC Dropout uncertainty quantification">
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
+ <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet" />
11
+ <link rel="stylesheet" href="/dashboard/style.css?v=3">
12
+ </head>
13
+ <body>
14
+ <!-- Animated Background -->
15
+ <div class="bg-grid"></div>
16
+ <div class="bg-glow bg-glow-1"></div>
17
+ <div class="bg-glow bg-glow-2"></div>
18
+ <div class="bg-glow bg-glow-3"></div>
19
+
20
+ <!-- Navigation -->
21
+ <nav class="navbar" id="navbar">
22
+ <div class="nav-brand">
23
+ <span class="brand-text">AetherFlow <span class="brand-sub">AI</span></span>
24
+ <span class="brand-badge">SupportMind v1.0</span>
25
+ </div>
26
+ <div class="nav-links">
27
+ <a href="#demo" class="nav-link">Live Demo</a>
28
+ <a href="#architecture" class="nav-link">Architecture</a>
29
+ <a href="#benchmarks" class="nav-link">Benchmarks</a>
30
+ <a href="#api-section" class="nav-link">API</a>
31
+ </div>
32
+ <div class="nav-actions">
33
+ <button class="btn-ghost" style="padding: 8px 16px; font-size: 13px;">Demo Mode</button>
34
+ <button class="btn-primary" style="padding: 8px 20px; font-size: 13px;">Get Started</button>
35
+ </div>
36
+ </nav>
37
+
38
+ <!-- Hero Section -->
39
+ <header class="hero" id="hero">
40
+ <div class="hero-container">
41
+ <div class="hero-content">
42
+ <div class="hero-eyebrow">
43
+ <span class="status-dot"></span>
44
+ Uncertainty-Aware Neural Engine
45
+ </div>
46
+ <h1 class="hero-title">
47
+ AI that <span class="gradient-text italic">knows</span> when it doesn't know
48
+ </h1>
49
+ <p class="hero-subtitle">
50
+ MC Dropout uncertainty quantification on DistilBERT. Three-tier routing:
51
+ <strong>Route</strong> · <strong>Clarify</strong> · <strong>Escalate</strong>.
52
+ Optimize support workflows with surgical precision.
53
+ </p>
54
+ <div class="hero-actions">
55
+ <a href="#demo" class="btn btn-primary">
56
+ Try Live Demo
57
+ <span class="material-symbols-outlined btn-icon">arrow_forward</span>
58
+ </a>
59
+ <a href="#architecture" class="btn btn-ghost">View Architecture</a>
60
+ </div>
61
+ </div>
62
+
63
+ <!-- Hero Visual (Pipeline Preview) -->
64
+ <div class="hero-visual">
65
+ <div class="pipeline-card glass-panel shimmer-border">
66
+ <div class="pipeline-header">
67
+ <div class="window-controls">
68
+ <div class="dot dot-red"></div>
69
+ <div class="dot dot-amber"></div>
70
+ <div class="dot dot-green"></div>
71
+ </div>
72
+ <span class="pipeline-status">pipeline_status: active</span>
73
+ </div>
74
+ <div class="pipeline-tiers">
75
+ <div class="tier-card active">
76
+ <div class="tier-info">
77
+ <div class="tier-icon"><span class="material-symbols-outlined">account_tree</span></div>
78
+ <div>
79
+ <div class="tier-name">Tier 1: Auto-Route</div>
80
+ <div class="tier-meta">Confidence &gt; 0.92</div>
81
+ </div>
82
+ </div>
83
+ <span class="material-symbols-outlined tier-indicator" style="color: var(--green)">check_circle</span>
84
+ </div>
85
+ <div class="tier-card" style="background: rgba(192, 193, 255, 0.05); border-color: var(--primary);">
86
+ <div class="tier-info">
87
+ <div class="tier-icon" style="background: rgba(192, 193, 255, 0.4)"><span class="material-symbols-outlined">psychology</span></div>
88
+ <div>
89
+ <div class="tier-name" style="font-style: italic; color: var(--primary)">Tier 2: Clarification</div>
90
+ <div class="tier-meta" style="color: var(--primary)">Ambiguity Detected</div>
91
+ </div>
92
+ </div>
93
+ <div style="width: 48px; height: 6px; background: rgba(192, 193, 255, 0.2); border-radius: 10px; overflow: hidden;">
94
+ <div style="width: 66%; height: 100%; background: var(--primary)"></div>
95
+ </div>
96
+ </div>
97
+ <div class="tier-card">
98
+ <div class="tier-info">
99
+ <div class="tier-icon" style="background: rgba(248, 113, 113, 0.15); color: var(--red)"><span class="material-symbols-outlined">support_agent</span></div>
100
+ <div>
101
+ <div class="tier-name">Tier 3: Escalate</div>
102
+ <div class="tier-meta">Human Protocol</div>
103
+ </div>
104
+ </div>
105
+ <span class="material-symbols-outlined tier-indicator" style="color: #444">sync_alt</span>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </header>
112
+
113
+ <!-- Stats Grid -->
114
+ <div class="hero-stats">
115
+ <div class="stat-card glass-panel" data-value="57.3" data-suffix="%">
116
+ <div class="stat-label">OOD Routing Accuracy</div>
117
+ <div class="stat-value"><span class="counter">0</span>%</div>
118
+ <div class="stat-delta positive"><span class="material-symbols-outlined" style="font-size: 14px">trending_up</span> 100% on auto-routed</div>
119
+ </div>
120
+ <div class="stat-card glass-panel" data-value="100.0" data-suffix="%">
121
+ <div class="stat-label">Auto-Route Precision</div>
122
+ <div class="stat-value"><span class="counter">0</span>%</div>
123
+ <div class="stat-delta positive">Zero false auto-routes</div>
124
+ </div>
125
+ <div class="stat-card glass-panel" data-value="97.9" data-suffix="%">
126
+ <div class="stat-label">Safe Failure Rate</div>
127
+ <div class="stat-value"><span class="counter">0</span>%</div>
128
+ <div class="stat-delta positive">Flagged for human review</div>
129
+ </div>
130
+ <div class="stat-card glass-panel" data-value="45" data-suffix="ms">
131
+ <div class="stat-label">Pipeline Latency</div>
132
+ <div class="stat-value"><span class="counter">0</span>ms</div>
133
+ <div class="stat-delta" style="color: #666">DistilBERT Optimized</div>
134
+ </div>
135
+ </div>
136
+
137
+ <!-- Live Demo Section -->
138
+ <section class="section" id="demo">
139
+ <div class="section-header">
140
+ <span class="section-badge">Interactive</span>
141
+ <h2 class="section-title">Live Ticket Router</h2>
142
+ <p class="section-desc">Type a support ticket and watch the confidence-gated router decide in real-time</p>
143
+ </div>
144
+
145
+ <div class="demo-container">
146
+ <div class="demo-input-panel glass-panel">
147
+ <label class="input-label">Support Ticket Text</label>
148
+ <textarea id="ticket-input" class="ticket-textarea" rows="4" placeholder="e.g., We have been having issues with the export function since last Tuesday's update..."></textarea>
149
+ <div class="demo-presets">
150
+ <span class="preset-label">Try:</span>
151
+ <button class="preset-btn" data-text="My invoice from last month shows $299 but my plan is $199. Please fix this billing error immediately.">Billing</button>
152
+ <button class="preset-btn" data-text="The API endpoint /v2/export returns a 500 error when batch size exceeds 1000 records. Stack trace attached.">Technical</button>
153
+ <button class="preset-btn" data-text="Hey, we have been having issues with the export function since last Tuesday's update. Also our invoice from last month looks incorrect.">Ambiguous</button>
154
+ </div>
155
+ <button id="route-btn" class="btn btn-primary btn-full" onclick="routeTicket()">
156
+ ⚡ Route Ticket
157
+ </button>
158
+ </div>
159
+
160
+ <div class="demo-results-panel glass-panel" id="results-panel">
161
+ <div class="result-placeholder" id="result-placeholder">
162
+ <div class="placeholder-icon">🎯</div>
163
+ <p>Enter a ticket and click "Route Ticket" to see the confidence-gated decision</p>
164
+ </div>
165
+
166
+ <div class="result-content" id="result-content" style="display:none">
167
+ <!-- Action Badge -->
168
+ <div class="action-badge-container">
169
+ <div class="action-badge" id="action-badge">ROUTE</div>
170
+ <div class="action-queue" id="action-queue"></div>
171
+ </div>
172
+
173
+ <!-- Confidence & Margin & Entropy Gauges -->
174
+ <div class="gauge-row">
175
+ <div class="gauge-container">
176
+ <label class="gauge-label">Confidence</label>
177
+ <div class="gauge-track">
178
+ <div class="gauge-fill" id="conf-fill" style="width:0%"></div>
179
+ <div class="gauge-zones">
180
+ <div class="zone zone-red" style="width:55%"></div>
181
+ <div class="zone zone-yellow" style="width:25%"></div>
182
+ <div class="zone zone-green" style="width:20%"></div>
183
+ </div>
184
+ </div>
185
+ <div class="gauge-value" id="conf-value">0.0000</div>
186
+ </div>
187
+ <div class="gauge-container">
188
+ <label class="gauge-label">Top-2 Margin</label>
189
+ <div class="gauge-track margin-track">
190
+ <div class="gauge-fill margin-fill" id="margin-fill" style="width:0%; background: linear-gradient(90deg, var(--accent), var(--primary));"></div>
191
+ </div>
192
+ <div class="gauge-value" id="margin-value">0.0000</div>
193
+ </div>
194
+ <div class="gauge-container">
195
+ <label class="gauge-label">Shannon Entropy</label>
196
+ <div class="gauge-track entropy-track">
197
+ <div class="gauge-fill entropy-fill" id="ent-fill" style="width:0%"></div>
198
+ </div>
199
+ <div class="gauge-value" id="ent-value">0.0000</div>
200
+ </div>
201
+ </div>
202
+
203
+ <!-- Probability Distribution -->
204
+ <div class="prob-chart" id="prob-chart"></div>
205
+
206
+ <!-- Clarification Question (if action=clarify) -->
207
+ <div class="clarification-box" id="clarification-box" style="display:none">
208
+ <div class="clarify-header">💡 Suggested Clarification Question</div>
209
+ <div class="clarify-question" id="clarify-question"></div>
210
+ <div class="clarify-options" id="clarify-options"></div>
211
+ <div class="clarify-gain" id="clarify-gain"></div>
212
+ </div>
213
+
214
+ <!-- Additional Signals -->
215
+ <div class="signals-grid">
216
+ <div class="signal-card">
217
+ <div class="signal-label" style="display: flex; flex-direction: column;">
218
+ SLA Breach Risk
219
+ <span style="font-size: 9px; color: var(--yellow); margin-top: 2px;">*Demo fallback: 4.5 hrs avg</span>
220
+ </div>
221
+ <div class="signal-value" id="sla-value">—</div>
222
+ <div class="signal-bar"><div class="signal-fill" id="sla-fill"></div></div>
223
+ </div>
224
+ <div class="signal-card">
225
+ <div class="signal-label">Sentiment</div>
226
+ <div class="signal-value" id="sentiment-value">—</div>
227
+ </div>
228
+ <div class="signal-card">
229
+ <div class="signal-label">Urgency</div>
230
+ <div class="signal-value" id="urgency-value">—</div>
231
+ </div>
232
+ <div class="signal-card">
233
+ <div class="signal-label">Latency</div>
234
+ <div class="signal-value" id="latency-value">—</div>
235
+ </div>
236
+ </div>
237
+
238
+ <div class="result-reason" id="result-reason"></div>
239
+
240
+ <!-- SHAP Explanation -->
241
+ <div class="explanation-box" id="explanation-box" style="display:none">
242
+ <div class="explain-header">
243
+ <span class="material-symbols-outlined">analytics</span>
244
+ Decision Interpretability (SHAP)
245
+ </div>
246
+ <div class="explain-text" id="explain-text"></div>
247
+ <div class="explain-legend">
248
+ <span class="legend-item"><span class="highlight-box pos"></span> Increases confidence</span>
249
+ <span class="legend-item"><span class="highlight-box neg"></span> Decreases confidence</span>
250
+ </div>
251
+ </div>
252
+
253
+ <button id="explain-btn" class="btn btn-ghost btn-full" style="margin-top: 10px; display: none;" onclick="explainDecision()">
254
+ <span class="material-symbols-outlined btn-icon">query_stats</span>
255
+ Analyze Decision (SHAP)
256
+ </button>
257
+ </div>
258
+
259
+ </div>
260
+ </div>
261
+ </section>
262
+
263
+ <!-- Architecture Section -->
264
+ <section class="section section-dark" id="architecture">
265
+ <div class="section-header">
266
+ <span class="section-badge">Technical</span>
267
+ <h2 class="section-title">System Architecture</h2>
268
+ <p class="section-desc">Three-stage pipeline with MC Dropout confidence gating</p>
269
+ </div>
270
+
271
+ <div class="arch-pipeline">
272
+ <div class="arch-stage glass-panel">
273
+ <div class="stage-number">1</div>
274
+ <div class="stage-title">Feature Extraction</div>
275
+ <div class="stage-details">
276
+ <span class="tech-tag">DistilBERT</span>
277
+ <span class="tech-tag">VADER</span>
278
+ </div>
279
+ <p class="stage-desc">768-dim embedding + sentiment + urgency</p>
280
+ </div>
281
+ <div class="arch-arrow">→</div>
282
+ <div class="arch-stage glass-panel stage-highlight">
283
+ <div class="stage-number">2</div>
284
+ <div class="stage-title">Confidence-Gated Router</div>
285
+ <div class="stage-details">
286
+ <span class="tech-tag">MC Dropout</span>
287
+ <span class="tech-tag">Shannon Entropy</span>
288
+ </div>
289
+ <p class="stage-desc">3-tier decision gate (20 stochastic passes)</p>
290
+ </div>
291
+ <div class="arch-arrow">→</div>
292
+ <div class="arch-stage glass-panel">
293
+ <div class="stage-number">3</div>
294
+ <div class="stage-title">Intelligence Layer</div>
295
+ <div class="stage-details">
296
+ <span class="tech-tag">XGBoost SLA</span>
297
+ <span class="tech-tag">Churn Signal</span>
298
+ </div>
299
+ <p class="stage-desc">SLA breach prediction (AUC 0.83)</p>
300
+ </div>
301
+ </div>
302
+
303
+ <!-- MC Dropout Visualization -->
304
+ <div class="mc-dropout-viz glass-panel">
305
+ <h3 class="viz-title">Monte Carlo Dropout — 20 Stochastic Forward Passes</h3>
306
+ <p class="viz-desc">Each pass randomly deactivates different neurons, producing a distribution of predictions instead of a single overconfident output.</p>
307
+ <div class="dropout-grid" id="dropout-grid"></div>
308
+ <div class="dropout-legend">
309
+ <span><span class="legend-dot" style="background:#6366f1"></span> Active neuron</span>
310
+ <span><span class="legend-dot" style="background:#1e1b4b; border:1px solid #4338ca"></span> Dropped out</span>
311
+ </div>
312
+ </div>
313
+
314
+ <!-- Competitor Comparison -->
315
+ <div class="competitor-table-wrap">
316
+ <h3 class="viz-title">Competitor Architecture Gap</h3>
317
+ <table class="competitor-table">
318
+ <thead>
319
+ <tr>
320
+ <th>Platform</th>
321
+ <th>AI Feature</th>
322
+ <th>Handles Ambiguity?</th>
323
+ <th>Clarification?</th>
324
+ <th>Entropy Output?</th>
325
+ </tr>
326
+ </thead>
327
+ <tbody>
328
+ <tr><td>Zoho Desk</td><td>Zia Field Predictions</td><td class="cell-no">Binary fail</td><td class="cell-no">No</td><td class="cell-no">No</td></tr>
329
+ <tr><td>Freshdesk</td><td>Freddy Auto Triage</td><td class="cell-no">Majority-class default</td><td class="cell-no">No</td><td class="cell-no">No</td></tr>
330
+ <tr><td>Zendesk</td><td>Intelligent Triage</td><td class="cell-no">General queue fallback</td><td class="cell-no">No</td><td class="cell-partial">Static only</td></tr>
331
+ <tr><td>Salesforce</td><td>Einstein Classification</td><td class="cell-no">Fails on unstructured</td><td class="cell-no">No</td><td class="cell-no">No</td></tr>
332
+ <tr class="row-highlight"><td><strong>SupportMind</strong></td><td>Confidence-Gated Router</td><td class="cell-yes">3-tier gate</td><td class="cell-yes">47 templates</td><td class="cell-yes">Real-time Shannon</td></tr>
333
+ </tbody>
334
+ </table>
335
+ </div>
336
+ </section>
337
+
338
+ <!-- Benchmarks Section -->
339
+ <section class="section" id="benchmarks">
340
+ <div class="section-header">
341
+ <span class="section-badge">Results</span>
342
+ <h2 class="section-title">Honest Dual-Evaluation</h2>
343
+ <p class="section-desc">Transparent benchmarks: in-distribution synthetic data + out-of-distribution hand-crafted tickets</p>
344
+ </div>
345
+
346
+ <!-- OOD Transparency Note -->
347
+ <div class="glass-panel" style="margin-bottom: 32px; padding: 20px 24px; border-left: 3px solid var(--yellow);">
348
+ <div style="font-size: 13px; color: var(--on-surface-variant); line-height: 1.7;">
349
+ <strong style="color: var(--yellow);">Why two sets of numbers?</strong>
350
+ The <strong>In-Distribution</strong> set (100% accuracy) confirms the model learned its training distribution.
351
+ The <strong>Out-of-Distribution</strong> set (57.3% accuracy on 96 hand-crafted tickets) is the honest estimate of real-world generalization.
352
+ On OOD data, the model auto-routed only <strong>2.1%</strong> of tickets (with 100% precision) and safely flagged the rest for human review.
353
+ </div>
354
+ </div>
355
+
356
+ <div class="benchmark-grid">
357
+ <div class="bench-card glass-panel">
358
+ <div class="bench-metric">Overall Routing Accuracy</div>
359
+ <div class="bench-compare">
360
+ <div class="bench-bar-group">
361
+ <div class="bench-label">In-Distribution (synthetic)</div>
362
+ <div class="bench-bar"><div class="bench-fill baseline" style="width:100%"><span>100.0%</span></div></div>
363
+ </div>
364
+ <div class="bench-bar-group">
365
+ <div class="bench-label">Out-of-Distribution (OOD)</div>
366
+ <div class="bench-bar"><div class="bench-fill ours" style="width:57.3%"><span>57.3%</span></div></div>
367
+ </div>
368
+ </div>
369
+ <div class="bench-delta" style="color: var(--on-surface-variant); font-size: 11px;">OOD = honest generalization estimate</div>
370
+ </div>
371
+ <div class="bench-card glass-panel">
372
+ <div class="bench-metric">Precision on Auto-Routed</div>
373
+ <div class="bench-compare">
374
+ <div class="bench-bar-group">
375
+ <div class="bench-label">In-Distribution</div>
376
+ <div class="bench-bar"><div class="bench-fill baseline" style="width:100%"><span>100.0%</span></div></div>
377
+ </div>
378
+ <div class="bench-bar-group">
379
+ <div class="bench-label">Out-of-Distribution</div>
380
+ <div class="bench-bar"><div class="bench-fill ours" style="width:100%"><span>100.0%</span></div></div>
381
+ </div>
382
+ </div>
383
+ <div class="bench-delta positive">Zero false auto-routes on novel data</div>
384
+ </div>
385
+ <div class="bench-card glass-panel">
386
+ <div class="bench-metric">OOD Routing Gate Distribution</div>
387
+ <div class="bench-compare">
388
+ <div class="bench-bar-group">
389
+ <div class="bench-label" style="color: var(--green)">Auto-Routed (safe)</div>
390
+ <div class="bench-bar"><div class="bench-fill ours" style="width:2.1%; min-width: 32px;"><span>2.1%</span></div></div>
391
+ </div>
392
+ <div class="bench-bar-group">
393
+ <div class="bench-label" style="color: var(--yellow)">Clarify (flagged)</div>
394
+ <div class="bench-bar"><div class="bench-fill" style="width:51%; background: var(--yellow);"><span>51.0%</span></div></div>
395
+ </div>
396
+ <div class="bench-bar-group">
397
+ <div class="bench-label" style="color: var(--red)">Escalated (flagged)</div>
398
+ <div class="bench-bar"><div class="bench-fill" style="width:46.9%; background: var(--red);"><span>46.9%</span></div></div>
399
+ </div>
400
+ </div>
401
+ <div class="bench-delta positive">97.9% safely flagged for human review</div>
402
+ </div>
403
+ <div class="bench-card glass-panel">
404
+ <div class="bench-metric">OOD Ambiguous Accuracy</div>
405
+ <div class="bench-compare">
406
+ <div class="bench-bar-group">
407
+ <div class="bench-label">Hand-crafted ambiguous tickets</div>
408
+ <div class="bench-bar"><div class="bench-fill ours" style="width:30%"><span>30.0%</span></div></div>
409
+ </div>
410
+ </div>
411
+ <div class="bench-delta" style="color: var(--on-surface-variant); font-size: 11px;">Model correctly defers these to clarification</div>
412
+ </div>
413
+ </div>
414
+
415
+ <!-- Real-Time System Insights -->
416
+ <div class="glass-panel" style="margin-top: 40px; padding: 30px; border-top: 1px solid rgba(192, 193, 255, 0.2); background: linear-gradient(180deg, rgba(10, 10, 18, 0.4) 0%, rgba(10, 10, 18, 0.8) 100%);">
417
+ <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 32px;">
418
+ <div>
419
+ <h3 class="viz-title" style="margin: 0; font-size: 18px; letter-spacing: 1px; color: #fff;">System Health & Performance</h3>
420
+ <p style="font-size: 12px; color: var(--on-surface-variant); margin-top: 4px;">Real-time telemetry from the SupportMind Engine</p>
421
+ </div>
422
+ <div id="live-indicator" class="flex items-center gap-2" style="background: rgba(34, 197, 94, 0.1); padding: 6px 12px; border-radius: 20px; border: 1px solid rgba(34, 197, 94, 0.2);">
423
+ <span class="status-dot" style="background: #22c55e; box-shadow: 0 0 10px #22c55e;"></span>
424
+ <span style="font-size: 11px; font-weight: 600; color: #22c55e; text-transform: uppercase; letter-spacing: 0.5px;">Live Telemetry</span>
425
+ </div>
426
+ </div>
427
+
428
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-6">
429
+ <div class="insight-card glass-panel" style="padding: 20px; background: rgba(255,255,255,0.02);">
430
+ <div class="insight-label" style="font-size: 11px; text-transform: uppercase; color: var(--primary); margin-bottom: 12px; font-weight: 600;">Active Model Engine</div>
431
+ <div id="live-model" class="insight-value" style="font-family: 'JetBrains Mono', monospace; font-size: 13px; color: #fff;">Syncing...</div>
432
+ <div style="margin-top: 12px; font-size: 10px; color: var(--green);">● GPU Optimized Fallback</div>
433
+ </div>
434
+
435
+ <div class="insight-card glass-panel" style="padding: 20px; background: rgba(255,255,255,0.02); grid-column: span 2;">
436
+ <div class="insight-label" style="font-size: 11px; text-transform: uppercase; color: var(--primary); margin-bottom: 12px; font-weight: 600;">Routing Efficiency Distribution</div>
437
+ <div id="live-dist" class="insight-value">
438
+ <div style="display: flex; gap: 4px; height: 12px; border-radius: 6px; overflow: hidden; background: rgba(255,255,255,0.05);">
439
+ <div id="dist-route" style="width: 0%; background: linear-gradient(90deg, #22c55e, #4ade80); transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);"></div>
440
+ <div id="dist-clarify" style="width: 0%; background: linear-gradient(90deg, #f59e0b, #fbbf24); transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);"></div>
441
+ <div id="dist-escalate" style="width: 0%; background: linear-gradient(90deg, #ef4444, #f87171); transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);"></div>
442
+ </div>
443
+ <div style="display: flex; justify-content: space-between; font-size: 10px; margin-top: 10px; font-weight: 500;">
444
+ <span style="color: #4ade80;">Auto-Route</span>
445
+ <span style="color: #fbbf24;">Clarify</span>
446
+ <span style="color: #f87171;">Escalate</span>
447
+ </div>
448
+ </div>
449
+ </div>
450
+
451
+ <div class="insight-card glass-panel" style="padding: 20px; background: rgba(255,255,255,0.02); text-align: right;">
452
+ <div class="insight-label" style="font-size: 11px; text-transform: uppercase; color: var(--primary); margin-bottom: 4px; font-weight: 600;">Lifetime Triage</div>
453
+ <div id="live-total" class="insight-value" style="font-size: 32px; font-weight: 800; color: #fff; font-family: 'Inter', sans-serif;">0</div>
454
+ <div style="font-size: 10px; color: var(--on-surface-variant);">Tickets Orchestrated</div>
455
+ </div>
456
+ </div>
457
+ </div>
458
+
459
+ </section>
460
+
461
+ <!-- API Section -->
462
+ <section class="section section-dark" id="api-section">
463
+ <div class="section-header">
464
+ <span class="section-badge">Developer</span>
465
+ <h2 class="section-title">API Reference</h2>
466
+ <p class="section-desc">RESTful API with FastAPI — complete documentation at <code>/docs</code></p>
467
+ </div>
468
+ <div class="api-grid">
469
+ <div class="api-card">
470
+ <div class="api-method post">POST</div>
471
+ <code class="api-path">/route</code>
472
+ <p class="api-desc">Main routing endpoint — returns 3-tier confidence-gated decision</p>
473
+ </div>
474
+ <div class="api-card">
475
+ <div class="api-method post">POST</div>
476
+ <code class="api-path">/clarify</code>
477
+ <p class="api-desc">Get best clarification question for uncertain ticket</p>
478
+ </div>
479
+ <div class="api-card">
480
+ <div class="api-method post">POST</div>
481
+ <code class="api-path">/sla/predict</code>
482
+ <p class="api-desc">Predict SLA breach risk at ticket creation</p>
483
+ </div>
484
+ <div class="api-card">
485
+ <div class="api-method post">POST</div>
486
+ <code class="api-path">/churn/signal</code>
487
+ <p class="api-desc">Extract churn signal from thread history</p>
488
+ </div>
489
+ <div class="api-card">
490
+ <div class="api-method get">GET</div>
491
+ <code class="api-path">/metrics</code>
492
+ <p class="api-desc">Live system health and routing statistics</p>
493
+ </div>
494
+ <div class="api-card">
495
+ <div class="api-method get">GET</div>
496
+ <code class="api-path">/health</code>
497
+ <p class="api-desc">Health check for deployment pipelines</p>
498
+ </div>
499
+ </div>
500
+ </section>
501
+
502
+ <!-- Footer -->
503
+ <footer class="footer">
504
+ <div class="footer-content" style="max-width: 1200px; display: flex; justify-content: space-between; align-items: center;">
505
+ <div style="text-align: left;">
506
+ <div class="footer-brand" style="font-size: 24px; color: #fff;">AetherFlow</div>
507
+ <div class="footer-author">© 2026 AetherFlow Technologies. Intelligence through Clarity.</div>
508
+ </div>
509
+ <div class="footer-tech" style="display: flex; gap: 24px;">
510
+ <span>Privacy Policy</span>
511
+ <span>Terms of Service</span>
512
+ <span>Security</span>
513
+ <span>Status</span>
514
+ <span>Contact</span>
515
+ </div>
516
+ </div>
517
+ </footer>
518
+
519
+ <script src="/dashboard/app.js?v=4"></script>
520
+ </body>
521
+ </html>
dashboard/web/style.css ADDED
@@ -0,0 +1,598 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* SupportMind Dashboard — style.css */
2
+ :root {
3
+ /* AetherFlow Design Tokens */
4
+ --bg: #0d0d15;
5
+ --surface: #13131b;
6
+ --surface-container: #1f1f27;
7
+ --surface-bright: #393841;
8
+ --primary: #c0c1ff;
9
+ --primary-container: #8083ff;
10
+ --on-primary: #1000a9;
11
+ --secondary: #89ceff;
12
+ --accent: #8b5cf6;
13
+ --text: #e4e1ed;
14
+ --text-dim: #c7c4d7;
15
+ --border: rgba(255, 255, 255, 0.08);
16
+ --glass: rgba(255, 255, 255, 0.05);
17
+ --glass-border: rgba(255, 255, 255, 0.1);
18
+
19
+ --green: #4ade80;
20
+ --yellow: #facc15;
21
+ --red: #f87171;
22
+ --orange: #fb923c;
23
+
24
+ --font: 'Inter', system-ui, -apple-system, sans-serif;
25
+ --mono: 'JetBrains Mono', monospace;
26
+ --radius: 12px;
27
+ --radius-lg: 24px;
28
+ --radius-xl: 32px;
29
+ }
30
+
31
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
32
+
33
+ html { scroll-behavior: smooth; }
34
+
35
+ body {
36
+ font-family: var(--font);
37
+ background: var(--bg);
38
+ color: var(--text);
39
+ line-height: 1.6;
40
+ overflow-x: hidden;
41
+ }
42
+
43
+ /* Glassmorphism */
44
+ .glass-panel {
45
+ background: var(--glass);
46
+ backdrop-filter: blur(12px);
47
+ -webkit-backdrop-filter: blur(12px);
48
+ border: 1px solid var(--glass-border);
49
+ }
50
+
51
+ .shimmer-border {
52
+ position: relative;
53
+ overflow: hidden;
54
+ }
55
+
56
+ .shimmer-border::after {
57
+ content: "";
58
+ position: absolute;
59
+ top: 0; left: 0; right: 0;
60
+ height: 1px;
61
+ background: linear-gradient(90deg, transparent, var(--primary), transparent);
62
+ opacity: 0.3;
63
+ animation: shimmer 3s infinite linear;
64
+ }
65
+
66
+ @keyframes shimmer {
67
+ 0% { transform: translateX(-100%); }
68
+ 100% { transform: translateX(100%); }
69
+ }
70
+
71
+ /* Nav */
72
+ .navbar {
73
+ position: fixed; top: 0; left: 0; right: 0; z-index: 100;
74
+ display: flex; align-items: center; justify-content: space-between;
75
+ padding: 16px 40px;
76
+ background: rgba(13, 13, 21, 0.8);
77
+ backdrop-filter: blur(20px);
78
+ border-bottom: 1px solid var(--border);
79
+ height: 64px;
80
+ }
81
+ .nav-brand { display: flex; align-items: center; gap: 10px; }
82
+ .brand-icon { display: flex; }
83
+ .brand-text { font-weight: 700; font-size: 18px; }
84
+ .brand-badge {
85
+ font-size: 10px; padding: 2px 8px; border-radius: 20px;
86
+ background: var(--surface2); color: var(--text2); font-weight: 600;
87
+ }
88
+ .nav-links { display: flex; gap: 8px; }
89
+ .nav-link {
90
+ color: var(--text2); text-decoration: none; font-size: 14px; font-weight: 500;
91
+ padding: 6px 14px; border-radius: 8px; transition: all 0.2s;
92
+ }
93
+ .nav-link:hover { color: var(--text); background: var(--surface2); }
94
+ .nav-status { display: flex; align-items: center; gap: 8px; font-size: 13px; color: var(--green); }
95
+ .status-dot {
96
+ width: 8px; height: 8px; border-radius: 50%; background: var(--green);
97
+ animation: pulse 2s ease-in-out infinite;
98
+ }
99
+ @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
100
+
101
+ /* Hero */
102
+ .hero {
103
+ position: relative; z-index: 1;
104
+ padding: 120px 40px 80px;
105
+ max-width: 1400px; margin: 0 auto;
106
+ }
107
+
108
+ .hero-container {
109
+ display: grid;
110
+ grid-template-columns: 1.2fr 1fr;
111
+ gap: 80px;
112
+ align-items: center;
113
+ }
114
+
115
+ .hero-content { text-align: left; }
116
+
117
+ .hero-eyebrow {
118
+ display: inline-flex; align-items: center; gap: 8px;
119
+ padding: 6px 16px; border-radius: 100px;
120
+ background: rgba(192, 193, 255, 0.1);
121
+ color: var(--primary);
122
+ font-size: 12px; font-weight: 700;
123
+ text-transform: uppercase; letter-spacing: 1px;
124
+ margin-bottom: 32px;
125
+ }
126
+
127
+ .hero-title {
128
+ font-size: clamp(48px, 5vw, 72px); font-weight: 800;
129
+ line-height: 1.05; margin-bottom: 24px;
130
+ color: #fff;
131
+ letter-spacing: -0.04em;
132
+ }
133
+
134
+ .gradient-text {
135
+ background: linear-gradient(135deg, var(--primary), var(--secondary), var(--accent));
136
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
137
+ background-clip: text;
138
+ }
139
+
140
+ .hero-subtitle {
141
+ font-size: 18px; color: var(--text-dim); max-width: 600px; margin: 0 0 48px;
142
+ line-height: 1.6;
143
+ }
144
+
145
+ .hero-actions { display: flex; gap: 16px; justify-content: flex-start; margin-bottom: 0; }
146
+
147
+ /* Hero Visual (Bento Preview) */
148
+ .hero-visual {
149
+ position: relative;
150
+ }
151
+
152
+ .pipeline-card {
153
+ border-radius: 24px;
154
+ padding: 24px;
155
+ position: relative;
156
+ z-index: 10;
157
+ box-shadow: 0 40px 80px rgba(0, 0, 0, 0.5);
158
+ }
159
+
160
+ .pipeline-header {
161
+ display: flex; justify-content: space-between; align-items: center;
162
+ margin-bottom: 24px; padding-bottom: 16px;
163
+ border-bottom: 1px solid rgba(255, 255, 255, 0.05);
164
+ }
165
+
166
+ .window-controls { display: flex; gap: 6px; }
167
+ .dot { width: 10px; height: 10px; border-radius: 50%; opacity: 0.5; }
168
+ .dot-red { background: var(--red); }
169
+ .dot-amber { background: var(--yellow); }
170
+ .dot-green { background: var(--green); }
171
+
172
+ .pipeline-status { font-family: var(--mono); font-size: 12px; color: #666; }
173
+
174
+ .pipeline-tiers { display: flex; flex-direction: column; gap: 16px; }
175
+
176
+ .tier-card {
177
+ padding: 16px; border-radius: 16px;
178
+ background: rgba(255, 255, 255, 0.03);
179
+ border: 1px solid rgba(255, 255, 255, 0.05);
180
+ display: flex; justify-content: space-between; align-items: center;
181
+ transition: all 0.3s;
182
+ }
183
+
184
+ .tier-card.active {
185
+ background: rgba(192, 193, 255, 0.08);
186
+ border-color: rgba(192, 193, 255, 0.2);
187
+ }
188
+
189
+ .tier-info { display: flex; align-items: center; gap: 16px; }
190
+ .tier-icon {
191
+ width: 40px; height: 40px; border-radius: 10px;
192
+ background: rgba(192, 193, 255, 0.15);
193
+ display: flex; align-items: center; justify-content: center;
194
+ color: var(--primary);
195
+ }
196
+
197
+ .tier-name { font-size: 14px; font-weight: 600; color: #fff; }
198
+ .tier-meta { font-size: 12px; color: #666; }
199
+ .tier-indicator { font-size: 20px; }
200
+
201
+
202
+ /* Buttons */
203
+ .btn {
204
+ display: inline-flex; align-items: center; gap: 10px;
205
+ padding: 14px 32px; border-radius: 12px; font-size: 15px; font-weight: 700;
206
+ text-decoration: none; cursor: pointer; border: none;
207
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
208
+ font-family: var(--font);
209
+ }
210
+
211
+ .btn-primary {
212
+ background: var(--primary);
213
+ color: var(--on-primary);
214
+ box-shadow: 0 8px 24px rgba(192, 193, 255, 0.2);
215
+ }
216
+
217
+ .btn-primary:hover {
218
+ transform: translateY(-2px);
219
+ box-shadow: 0 12px 32px rgba(192, 193, 255, 0.3);
220
+ filter: brightness(1.1);
221
+ }
222
+
223
+ .btn-ghost {
224
+ background: rgba(255, 255, 255, 0.03);
225
+ color: #fff;
226
+ border: 1px solid rgba(255, 255, 255, 0.1);
227
+ }
228
+
229
+ .btn-ghost:hover {
230
+ background: rgba(255, 255, 255, 0.08);
231
+ border-color: var(--primary);
232
+ }
233
+
234
+ /* Stats Section */
235
+ .hero-stats {
236
+ display: grid; grid-template-columns: repeat(4, 1fr); gap: 24px;
237
+ max-width: 1400px; margin: 0 auto;
238
+ padding: 0 40px 80px;
239
+ }
240
+
241
+ .stat-card {
242
+ padding: 32px;
243
+ border-radius: 20px;
244
+ transition: all 0.3s;
245
+ }
246
+
247
+ .stat-card:hover {
248
+ border-color: rgba(192, 193, 255, 0.2);
249
+ transform: translateY(-4px);
250
+ background: rgba(192, 193, 255, 0.03);
251
+ }
252
+
253
+ .stat-label {
254
+ font-size: 12px; font-weight: 600; color: #666;
255
+ text-transform: uppercase; letter-spacing: 1px;
256
+ margin-bottom: 8px;
257
+ }
258
+
259
+ .stat-value {
260
+ font-size: 40px; font-weight: 800; color: #fff;
261
+ line-height: 1; margin-bottom: 12px;
262
+ }
263
+
264
+ .stat-delta {
265
+ font-size: 13px; font-weight: 600;
266
+ display: flex; align-items: center; gap: 4px;
267
+ }
268
+
269
+ .btn-full { width: 100%; justify-content: center; padding: 14px; font-size: 16px; margin-top: 16px; }
270
+ .btn-icon { font-size: 20px; }
271
+
272
+
273
+ /* Sections */
274
+ .section {
275
+ position: relative; z-index: 1;
276
+ padding: 100px 40px;
277
+ max-width: 1400px; margin: 0 auto;
278
+ }
279
+ .section-dark { max-width: none; background: #0a0a0f; }
280
+ .section-dark > * { max-width: 1400px; margin-left: auto; margin-right: auto; }
281
+ .section-header { text-align: center; margin-bottom: 60px; }
282
+ .section-badge {
283
+ display: inline-block; font-size: 12px; font-weight: 700; text-transform: uppercase;
284
+ letter-spacing: 2px; color: var(--primary); background: rgba(192, 193, 255, 0.1);
285
+ padding: 6px 18px; border-radius: 20px; margin-bottom: 16px;
286
+ }
287
+ .section-title { font-size: 40px; font-weight: 800; margin-bottom: 16px; color: #fff; }
288
+ .section-desc { font-size: 17px; color: var(--text-dim); max-width: 700px; margin: 0 auto; }
289
+
290
+ /* Demo */
291
+ .demo-container {
292
+ display: grid; grid-template-columns: 1fr 1fr; gap: 24px;
293
+ }
294
+ .demo-input-panel, .demo-results-panel {
295
+ background: var(--surface); border: 1px solid var(--border);
296
+ border-radius: var(--radius2); padding: 28px;
297
+ }
298
+ .input-label { font-size: 14px; font-weight: 600; color: var(--text2); margin-bottom: 10px; display: block; }
299
+ .ticket-textarea {
300
+ width: 100%; background: var(--bg); border: 1px solid var(--border);
301
+ border-radius: var(--radius); padding: 14px; color: var(--text);
302
+ font-family: var(--font); font-size: 14px; resize: vertical;
303
+ transition: border-color 0.2s; line-height: 1.6;
304
+ }
305
+ .ticket-textarea:focus { outline: none; border-color: var(--primary); }
306
+ .demo-presets { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; align-items: center; }
307
+ .preset-label { font-size: 13px; color: var(--text3); font-weight: 500; }
308
+ .preset-btn {
309
+ font-size: 12px; padding: 4px 12px; border-radius: 6px;
310
+ background: var(--surface2); border: 1px solid var(--border);
311
+ color: var(--text2); cursor: pointer; font-family: var(--font);
312
+ transition: all 0.2s;
313
+ }
314
+ .preset-btn:hover { border-color: var(--primary); color: var(--primary2); }
315
+
316
+ /* Result placeholder */
317
+ .result-placeholder {
318
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
319
+ height: 100%; min-height: 300px; color: var(--text3); text-align: center;
320
+ }
321
+ .placeholder-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.5; }
322
+
323
+ /* Action badge */
324
+ .action-badge-container { text-align: center; margin-bottom: 20px; }
325
+ .action-badge {
326
+ display: inline-block; padding: 8px 24px; border-radius: 8px;
327
+ font-size: 18px; font-weight: 800; font-family: var(--mono);
328
+ letter-spacing: 2px;
329
+ animation: badgePop 0.4s ease;
330
+ }
331
+ @keyframes badgePop { 0% { transform: scale(0.8); opacity: 0; } 100% { transform: scale(1); opacity: 1; } }
332
+ .action-badge.route { background: rgba(74, 222, 128, 0.1); color: var(--green); border: 1px solid rgba(74, 222, 128, 0.2); }
333
+ .action-badge.clarify { background: rgba(250, 204, 21, 0.1); color: var(--yellow); border: 1px solid rgba(250, 204, 21, 0.2); }
334
+ .action-badge.escalate { background: rgba(248, 113, 113, 0.1); color: var(--red); border: 1px solid rgba(248, 113, 113, 0.2); }
335
+ .action-queue { font-size: 13px; color: var(--text2); margin-top: 8px; }
336
+
337
+ /* Gauges */
338
+ .gauge-row { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; margin-bottom: 20px; }
339
+ .gauge-container { position: relative; }
340
+ .gauge-label { font-size: 12px; font-weight: 600; color: var(--text2); margin-bottom: 6px; display: block; }
341
+ .gauge-track {
342
+ height: 10px; background: var(--bg); border-radius: 5px;
343
+ overflow: hidden; position: relative;
344
+ }
345
+ .gauge-fill {
346
+ height: 100%; border-radius: 5px; transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
347
+ background: linear-gradient(90deg, var(--green), var(--primary));
348
+ position: relative; z-index: 2;
349
+ }
350
+ .entropy-fill { background: linear-gradient(90deg, var(--cyan), var(--yellow), var(--red)); }
351
+ .gauge-zones { position: absolute; inset: 0; display: flex; opacity: 0.15; }
352
+ .zone { height: 100%; }
353
+ .zone-red { background: var(--red); }
354
+ .zone-yellow { background: var(--yellow); }
355
+ .zone-green { background: var(--green); }
356
+ .gauge-value { font-size: 20px; font-weight: 700; font-family: var(--mono); margin-top: 6px; }
357
+
358
+ /* Probability chart */
359
+ .prob-chart { margin-bottom: 20px; }
360
+ .prob-row {
361
+ display: flex; align-items: center; gap: 10px; margin-bottom: 6px;
362
+ font-size: 12px;
363
+ }
364
+ .prob-label { width: 130px; color: var(--text2); text-align: right; font-family: var(--mono); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
365
+ .prob-bar-track { flex: 1; height: 6px; background: var(--bg); border-radius: 3px; overflow: hidden; }
366
+ .prob-bar-fill { height: 100%; border-radius: 3px; transition: width 0.6s ease; background: var(--primary); }
367
+ .prob-val { width: 50px; font-family: var(--mono); color: var(--text3); font-size: 11px; }
368
+
369
+ /* Clarification */
370
+ .clarification-box {
371
+ background: rgba(234,179,8,0.06); border: 1px solid rgba(234,179,8,0.2);
372
+ border-radius: var(--radius); padding: 20px; margin-bottom: 20px;
373
+ }
374
+ .clarify-header { font-weight: 700; font-size: 14px; margin-bottom: 10px; color: var(--yellow); }
375
+ .clarify-question { font-size: 15px; line-height: 1.6; margin-bottom: 12px; }
376
+ .clarify-options { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
377
+ .clarify-options .option-btn {
378
+ padding: 6px 16px; border-radius: 6px; font-size: 13px;
379
+ background: var(--surface2); border: 1px solid var(--border);
380
+ color: var(--text); cursor: pointer; font-family: var(--font);
381
+ }
382
+ .clarify-gain { font-size: 12px; color: var(--text3); }
383
+
384
+ /* Signals */
385
+ .signals-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 16px; }
386
+ .signal-card {
387
+ background: var(--bg); border-radius: 8px; padding: 14px; text-align: center;
388
+ }
389
+ .signal-label { font-size: 11px; color: var(--text3); font-weight: 600; text-transform: uppercase; letter-spacing: 1px; }
390
+ .signal-value { font-size: 18px; font-weight: 700; font-family: var(--mono); margin-top: 4px; }
391
+ .signal-bar { height: 4px; background: var(--surface); border-radius: 2px; margin-top: 8px; overflow: hidden; }
392
+ .signal-fill { height: 100%; border-radius: 2px; background: var(--orange); transition: width 0.6s; }
393
+ .result-reason {
394
+ font-size: 13px; color: var(--text3); text-align: center;
395
+ padding: 12px; background: var(--bg); border-radius: 8px;
396
+ font-style: italic;
397
+ }
398
+
399
+ /* Architecture */
400
+ .arch-pipeline {
401
+ display: flex; align-items: stretch; gap: 0; margin-bottom: 80px;
402
+ justify-content: center;
403
+ }
404
+ .arch-stage {
405
+ padding: 32px; flex: 1; max-width: 340px;
406
+ text-align: center; transition: all 0.3s;
407
+ border-radius: 24px;
408
+ }
409
+ .arch-stage:hover { border-color: var(--primary); transform: translateY(-6px); }
410
+ .stage-highlight { border-color: var(--primary); background: rgba(192, 193, 255, 0.05); }
411
+ .stage-number {
412
+ width: 40px; height: 40px; border-radius: 50%;
413
+ background: var(--primary);
414
+ color: var(--on-primary); font-weight: 800; font-size: 18px;
415
+ display: inline-flex; align-items: center; justify-content: center;
416
+ margin-bottom: 16px;
417
+ }
418
+ .stage-title { font-size: 18px; font-weight: 700; margin-bottom: 12px; color: #fff; }
419
+ .stage-details { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin-bottom: 12px; }
420
+ .tech-tag {
421
+ font-size: 11px; padding: 4px 12px; border-radius: 6px;
422
+ background: rgba(192, 193, 255, 0.1); color: var(--primary); font-weight: 700;
423
+ }
424
+ .stage-desc { font-size: 14px; color: var(--text-dim); line-height: 1.6; }
425
+ .arch-arrow {
426
+ display: flex; align-items: center; font-size: 28px; color: #333;
427
+ padding: 0 16px;
428
+ }
429
+
430
+ /* MC Dropout Viz */
431
+ .mc-dropout-viz {
432
+ text-align: center; margin-bottom: 60px;
433
+ background: var(--surface); border: 1px solid var(--border);
434
+ border-radius: var(--radius2); padding: 36px;
435
+ }
436
+ .viz-title { font-size: 20px; font-weight: 700; margin-bottom: 8px; }
437
+ .viz-desc { font-size: 14px; color: var(--text2); margin-bottom: 24px; max-width: 600px; margin-left: auto; margin-right: auto; }
438
+ .dropout-grid { display: flex; gap: 4px; justify-content: center; flex-wrap: wrap; margin-bottom: 16px; }
439
+ .dropout-col { display: flex; flex-direction: column; gap: 3px; }
440
+ .dropout-cell {
441
+ width: 10px; height: 10px; border-radius: 2px; transition: all 0.3s;
442
+ }
443
+ .dropout-legend { display: flex; gap: 20px; justify-content: center; font-size: 12px; color: var(--text3); }
444
+ .legend-dot { display: inline-block; width: 10px; height: 10px; border-radius: 2px; vertical-align: middle; margin-right: 4px; }
445
+
446
+ /* Competitor Table */
447
+ .competitor-table-wrap { overflow-x: auto; }
448
+ .competitor-table {
449
+ width: 100%; border-collapse: collapse; font-size: 14px;
450
+ }
451
+ .competitor-table th {
452
+ text-align: left; padding: 14px 16px; color: var(--text2);
453
+ font-weight: 600; font-size: 12px; text-transform: uppercase;
454
+ letter-spacing: 1px; border-bottom: 1px solid var(--border);
455
+ }
456
+ .competitor-table td { padding: 14px 16px; border-bottom: 1px solid var(--border); }
457
+ .cell-no { color: var(--red); }
458
+ .cell-yes { color: var(--green); font-weight: 600; }
459
+ .cell-partial { color: var(--yellow); }
460
+ .row-highlight { background: rgba(99,102,241,0.06); }
461
+
462
+ /* Benchmarks */
463
+ .benchmark-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px; margin-bottom: 48px; }
464
+ .bench-card {
465
+ background: var(--surface); border: 1px solid var(--border);
466
+ border-radius: var(--radius2); padding: 24px;
467
+ }
468
+ .bench-metric { font-size: 15px; font-weight: 700; margin-bottom: 16px; }
469
+ .bench-bar-group { margin-bottom: 8px; }
470
+ .bench-label { font-size: 12px; color: var(--text3); margin-bottom: 4px; font-weight: 500; }
471
+ .bench-bar { height: 28px; background: var(--bg); border-radius: 6px; overflow: hidden; }
472
+ .bench-fill {
473
+ height: 100%; border-radius: 6px; display: flex; align-items: center;
474
+ padding-left: 12px; font-size: 13px; font-weight: 700; font-family: var(--mono);
475
+ transition: width 1s ease;
476
+ }
477
+ .bench-fill.baseline { background: rgba(148,163,184,0.2); color: var(--text2); }
478
+ .bench-fill.ours { background: linear-gradient(90deg, var(--primary), var(--accent)); color: white; }
479
+ .bench-fill.bad { background: rgba(239,68,68,0.2); color: var(--red); }
480
+ .bench-delta { font-size: 14px; font-weight: 700; color: var(--green); margin-top: 8px; text-align: right; }
481
+
482
+ /* Ops Grid */
483
+ .ops-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; }
484
+ .ops-card {
485
+ background: var(--surface); border: 1px solid var(--border);
486
+ border-radius: var(--radius); padding: 24px; text-align: center;
487
+ }
488
+ .ops-icon { font-size: 28px; margin-bottom: 10px; }
489
+ .ops-metric { font-size: 13px; color: var(--text2); font-weight: 600; margin-bottom: 12px; }
490
+ .ops-before { font-size: 18px; font-weight: 700; color: var(--text3); font-family: var(--mono); }
491
+ .ops-arrow { color: var(--text3); margin: 6px 0; }
492
+ .ops-after { font-size: 22px; font-weight: 800; color: white; font-family: var(--mono); }
493
+ .ops-delta { font-size: 13px; font-weight: 700; color: var(--green); margin-top: 8px; }
494
+ .ops-delta.positive { color: var(--green); }
495
+
496
+ /* API */
497
+ .api-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
498
+ .api-card {
499
+ background: var(--surface); border: 1px solid var(--border);
500
+ border-radius: var(--radius); padding: 20px;
501
+ }
502
+ .api-method {
503
+ display: inline-block; font-size: 11px; font-weight: 800; padding: 3px 10px;
504
+ border-radius: 4px; font-family: var(--mono); margin-bottom: 8px;
505
+ }
506
+ .api-method.post { background: rgba(99,102,241,0.15); color: var(--primary2); }
507
+ .api-method.get { background: rgba(34,197,94,0.15); color: var(--green); }
508
+ .api-path { font-family: var(--mono); font-size: 16px; font-weight: 600; display: block; margin-bottom: 8px; }
509
+ .api-desc { font-size: 13px; color: var(--text3); }
510
+
511
+ /* Footer */
512
+ .footer {
513
+ position: relative; z-index: 1;
514
+ border-top: 1px solid var(--border); padding: 80px 40px;
515
+ background: #050508;
516
+ }
517
+ .footer-brand { font-size: 24px; font-weight: 800; color: #fff; margin-bottom: 8px; letter-spacing: -1px; }
518
+ .footer-author { font-size: 14px; color: #666; margin-bottom: 8px; }
519
+ .footer-tech { font-size: 14px; color: #666; font-weight: 500; cursor: pointer; }
520
+ .footer-tech span:hover { color: var(--primary); }
521
+
522
+ /* Responsive */
523
+ @media (max-width: 900px) {
524
+ .hero-stats { grid-template-columns: repeat(2, 1fr); }
525
+ .demo-container { grid-template-columns: 1fr; }
526
+ .arch-pipeline { flex-direction: column; align-items: center; }
527
+ .arch-arrow { transform: rotate(90deg); padding: 8px; }
528
+ .benchmark-grid, .ops-grid, .api-grid { grid-template-columns: 1fr; }
529
+ .signals-grid { grid-template-columns: repeat(2, 1fr); }
530
+ .gauge-row { grid-template-columns: 1fr; }
531
+ .navbar { padding: 12px 16px; }
532
+ .nav-links { display: none; }
533
+ }
534
+
535
+ /* Animations */
536
+ .fade-in { opacity: 0; transform: translateY(20px); transition: all 0.6s ease; }
537
+ .fade-in.visible { opacity: 1; transform: translateY(0); }
538
+
539
+ /* Loading spinner */
540
+ .spinner {
541
+ display: inline-block; width: 20px; height: 20px;
542
+ border: 2px solid rgba(255,255,255,0.2); border-top-color: white;
543
+ border-radius: 50%; animation: spin 0.6s linear infinite;
544
+ }
545
+ /* SHAP Interpretability */
546
+ .explanation-box {
547
+ background: rgba(192, 193, 255, 0.03);
548
+ border: 1px solid var(--border);
549
+ border-radius: var(--radius);
550
+ padding: 20px;
551
+ margin-bottom: 20px;
552
+ animation: fadeIn 0.4s ease;
553
+ }
554
+ .explain-header {
555
+ font-weight: 700;
556
+ font-size: 14px;
557
+ margin-bottom: 12px;
558
+ color: var(--primary);
559
+ display: flex;
560
+ align-items: center;
561
+ gap: 8px;
562
+ }
563
+ .explain-text {
564
+ font-family: var(--font);
565
+ font-size: 15px;
566
+ line-height: 2.2;
567
+ color: var(--text);
568
+ margin-bottom: 16px;
569
+ }
570
+ .shap-token {
571
+ padding: 2px 4px;
572
+ margin: 0 1px;
573
+ border-radius: 4px;
574
+ display: inline-block;
575
+ transition: all 0.3s;
576
+ }
577
+ .explain-legend {
578
+ display: flex;
579
+ gap: 16px;
580
+ font-size: 11px;
581
+ color: var(--text3);
582
+ }
583
+ .legend-item { display: flex; align-items: center; gap: 6px; }
584
+ .highlight-box {
585
+ width: 12px;
586
+ height: 12px;
587
+ border-radius: 2px;
588
+ }
589
+ .highlight-box.pos { background: rgba(74, 222, 128, 0.4); border: 1px solid var(--green); }
590
+ .highlight-box.neg { background: rgba(248, 113, 113, 0.4); border: 1px solid var(--red); }
591
+
592
+ @keyframes fadeIn {
593
+ from { opacity: 0; transform: translateY(10px); }
594
+ to { opacity: 1; transform: translateY(0); }
595
+ }
596
+
597
+ @keyframes spin { to { transform: rotate(360deg); } }
598
+