BlackTesla commited on
Commit
0ef754d
·
verified ·
1 Parent(s): ebb4fbf

Add NextAuth or MetaMask Login: OAuth with Google/GitHub/email or Web3 wallet signature. With the ability to continue without signing in, bringing you back to the home page - Follow Up Deployment

Browse files
Files changed (1) hide show
  1. dashboard.html +56 -66
dashboard.html CHANGED
@@ -19,6 +19,10 @@
19
  .stat-card:hover { transform: translateY(-3px); box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05); }
20
  .active-tab { border-bottom: 2px solid #4F46E5; color: #4F46E5; }
21
  .badge { font-size: 0.75rem; padding: 0.25rem 0.5rem; border-radius: 9999px; }
 
 
 
 
22
  @media (max-width: 768px) {
23
  .sidebar { position: fixed; z-index: 50; left: -280px; }
24
  .sidebar.open { left: 0; }
@@ -71,37 +75,11 @@
71
  </li>
72
  </ul>
73
  </div>
74
- <div class="mb-8">
75
- <h3 class="text-xs font-semibold text-gray-500 uppercase tracking-wider mb-4 sidebar-section-title">Tools</h3>
76
- <ul class="space-y-1">
77
- <li>
78
- <a href="#" class="flex items-center space-x-3 p-2 rounded-lg text-gray-600 hover:bg-gray-100 transition">
79
- <i data-feather="database" class="w-5 h-5"></i>
80
- <span class="sidebar-item-text">Datasets</span>
81
- </a>
82
- </li>
83
- <li>
84
- <a href="#" class="flex items-center space-x-3 p-2 rounded-lg text-gray-600 hover:bg-gray-100 transition">
85
- <i data-feather="sliders" class="w-5 h-5"></i>
86
- <span class="sidebar-item-text">Settings</span>
87
- </a>
88
- </li>
89
- </ul>
90
- </div>
91
  </div>
92
  </aside>
93
 
94
  <!-- Main Content -->
95
  <main class="main-content flex-1 ml-0 md:ml-[280px]">
96
- <!-- Top bar (mobile) -->
97
- <div class="md:hidden bg-white border-b border-gray-200 sticky top-0 z-40 flex items-center justify-between px-4 py-3">
98
- <button id="mobileMenu" class="text-gray-600">
99
- <i data-feather="menu" class="w-6 h-6"></i>
100
- </button>
101
- <div class="font-semibold">Dashboard</div>
102
- <div></div>
103
- </div>
104
-
105
  <section class="p-6 md:p-8" data-aos="fade-up">
106
  <div class="flex items-center justify-between mb-6">
107
  <div>
@@ -119,17 +97,17 @@
119
  <div class="stat-card bg-white rounded-2xl p-4 border border-gray-200">
120
  <div class="flex items-center justify-between">
121
  <div>
122
- <p class="text-sm text-gray-500">Total Feedback</p>
123
  <p id="kpiTotal" class="text-2xl font-bold">—</p>
124
  </div>
125
  <i data-feather="message-circle" class="w-8 h-8 text-indigo-600"></i>
126
  </div>
127
- <p id="kpiTotalDelta" class="text-xs text-gray-500 mt-2">—</p>
128
  </div>
129
  <div class="stat-card bg-white rounded-2xl p-4 border border-gray-200">
130
  <div class="flex items-center justify-between">
131
  <div>
132
- <p class="text-sm text-gray-500">Avg. Rating</p>
133
  <p id="kpiAvg" class="text-2xl font-bold">—</p>
134
  </div>
135
  <i data-feather="star" class="w-8 h-8 text-yellow-500"></i>
@@ -139,7 +117,7 @@
139
  <div class="stat-card bg-white rounded-2xl p-4 border border-gray-200">
140
  <div class="flex items-center justify-between">
141
  <div>
142
- <p class="text-sm text-gray-500">Positive Sentiment</p>
143
  <p id="kpiSentPos" class="text-2xl font-bold">—</p>
144
  </div>
145
  <i data-feather="smile" class="w-8 h-8 text-green-500"></i>
@@ -149,7 +127,7 @@
149
  <div class="stat-card bg-white rounded-2xl p-4 border border-gray-200">
150
  <div class="flex items-center justify-between">
151
  <div>
152
- <p class="text-sm text-gray-500">Top Agent</p>
153
  <p id="kpiTopAgent" class="text-2xl font-bold">—</p>
154
  </div>
155
  <i data-feather="award" class="w-8 h-8 text-amber-600"></i>
@@ -165,18 +143,22 @@
165
  <h3 class="font-semibold">Feedback Volume (14 days)</h3>
166
  <span class="text-xs text-gray-500">live</span>
167
  </div>
168
- <canvas id="chartVolume" height="120"></canvas>
 
 
169
  </div>
170
  <div class="bg-white rounded-2xl p-4 border border-gray-200">
171
  <div class="flex items-center justify-between mb-2">
172
  <h3 class="font-semibold">Sentiment Mix</h3>
173
  <span class="text-xs text-gray-500">auto</span>
174
  </div>
175
- <canvas id="chartSentiment" height="120"></canvas>
 
 
176
  </div>
177
  </div>
178
 
179
- <!-- Top agents -->
180
  <div class="bg-white rounded-2xl p-4 border border-gray-200 mb-8">
181
  <div class="flex items-center justify-between">
182
  <h3 class="font-semibold">Agents Leaderboard</h3>
@@ -216,13 +198,7 @@
216
  // --- CONFIG ---
217
  const HF_OWNER = 'BlackTesla';
218
  const HF_SPACE = 'archindus';
219
- /**
220
- * How this works
221
- * 1) We try to read Space discussions via a HUB endpoint (public read). If CORS blocks in your env,
222
- * point PROXY_URL to a tiny server you control that calls the Hugging Face Hub API using an access token.
223
- * Example proxy response shape: { items: [{ id, title, createdAt, url, author, text, rating, agent }], total, windowDelta }
224
- */
225
- const PROXY_URL = ''; // e.g. '/api/agents-feedback' (leave blank to use experimental direct read)
226
 
227
  // --- UTILITIES ---
228
  const el = (id) => document.getElementById(id);
@@ -235,7 +211,7 @@
235
  return `${Math.floor(diff/86400)}d ago`;
236
  };
237
 
238
- // Lightweight sentiment heuristic
239
  const POS = ['great','good','love','fast','helpful','amazing','excellent','awesome','smooth','wow'];
240
  const NEG = ['bad','slow','hate','bug','issue','error','crash','broken','lag','confusing','terrible'];
241
  function sentimentScore(text=''){
@@ -246,21 +222,16 @@
246
 
247
  // --- DATA ACCESS ---
248
  async function fetchFeedback(){
249
- // 1) Preferred: your proxy (handles auth + shaping)
250
  if(PROXY_URL){
251
  const r = await fetch(PROXY_URL);
252
  if(!r.ok) throw new Error('Proxy fetch failed');
253
  return await r.json();
254
  }
255
- // 2) Experimental: attempt to read public discussions from the Hub
256
- // NOTE: Depending on CORS and API policy, this may require a proxy.
257
- // Fallback to mock if blocked.
258
  const url = `https://huggingface.co/api/spaces/${HF_OWNER}/${HF_SPACE}/discussions?limit=50`;
259
  try{
260
  const r = await fetch(url, { headers: { 'Accept': 'application/json' } });
261
  if(!r.ok) throw new Error('HF discussions not accessible from browser');
262
  const data = await r.json();
263
- // Map HF discussions -> generic items
264
  const items = (data.discussions || data.items || []).map(d => ({
265
  id: d.id || d.num,
266
  title: d.title || 'Feedback',
@@ -268,7 +239,6 @@
268
  url: d.url || `https://huggingface.co/spaces/${HF_OWNER}/${HF_SPACE}/discussions/${d.num}`,
269
  author: d.author || (d.user && d.user.name) || 'user',
270
  text: (d.first ? d.first.content : d.body) || d.summary || '',
271
- // Try parse rating/agent from title like "[Agent: Visioneer] Rating: 4"
272
  rating: (()=>{ const m=(d.title||'').match(/rating\s*[:=]\s*(\d+(?:\.\d+)?)/i); return m?Number(m[1]):null; })(),
273
  agent: (()=>{ const m=(d.title||'').match(/agent\s*[:=]\s*([^\]\)\-:]+)/i); return m?m[1].trim():null; })()
274
  }));
@@ -324,7 +294,6 @@
324
  return { ...a, avg: Number(avg.toFixed(2)), score: Number(score.toFixed(2)) };
325
  }).sort((a,b)=> b.score - a.score);
326
 
327
- // last 14 days labels
328
  const days = Array.from({length:14}).map((_,i)=>{
329
  const d = new Date(); d.setDate(d.getDate()-(13-i));
330
  return d.toISOString().slice(0,10);
@@ -349,7 +318,7 @@
349
  chartVolume = new Chart(ctx1, {
350
  type: 'line',
351
  data: { labels: agg.volumeLabels, datasets: [{ label: 'Feedback', data: agg.volumeValues, tension: 0.35, fill: false }]},
352
- options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } } }
353
  });
354
 
355
  const ctx2 = document.getElementById('chartSentiment');
@@ -363,6 +332,7 @@
363
 
364
  function renderLeaderboard(rows){
365
  const tbody = el('leaderboard');
 
366
  tbody.innerHTML = rows.map(r => `
367
  <tr class="border-b last:border-b-0">
368
  <td class="py-2 pr-4">
@@ -380,6 +350,7 @@
380
 
381
  function renderRecent(items){
382
  const ul = el('recentFeedback');
 
383
  ul.innerHTML = items.slice(0,10).map(it=> `
384
  <li class="py-3">
385
  <a class="block" href="${it.url||'#'}" target="_blank">
@@ -405,40 +376,59 @@
405
  const delta = prev7 ? ((last7 - prev7)/prev7) : 0;
406
  el('kpiTotalDelta').textContent = `7d vs prev: ${(delta*100).toFixed(0)}%`;
407
 
408
- const avg = agg.leaderboard.reduce((acc,r)=> acc + r.avg*r.count, 0) / (agg.leaderboard.reduce((a,r)=>a+r.count,0) || 1);
 
 
409
  el('kpiAvg').textContent = avg.toFixed(2);
410
- const posRate = (agg.pos / Math.max(1, agg.total));
411
- el('kpiSentPos').textContent = fmtPct(posRate);
412
  const top = agg.leaderboard[0];
413
  el('kpiTopAgent').textContent = top ? top.name : '—';
414
  el('kpiTopAgentScore').textContent = top ? `score ${top.score.toFixed(2)} · avg ${top.avg.toFixed(2)}` : '—';
415
  }
416
 
 
 
417
  async function refresh(){
418
- const { items } = await fetchFeedback();
419
- const agg = aggregate(items);
420
- renderCharts(agg);
421
- renderLeaderboard(agg.leaderboard);
422
- renderRecent(items.sort((a,b)=> new Date(b.createdAt) - new Date(a.createdAt)));
423
- renderKpis(agg);
424
- el('lastUpdated').textContent = `Last update: ${new Date().toLocaleTimeString()}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
  }
426
 
427
  // --- WIRE-UP ---
428
- document.getElementById('mobileMenu').addEventListener('click', ()=>{
429
- document.querySelector('.sidebar').classList.toggle('open');
430
- });
431
- document.getElementById('sidebarToggle').addEventListener('click', ()=>{
432
  const s = document.querySelector('.sidebar');
433
  const m = document.querySelector('.main-content');
434
  s.classList.toggle('collapsed');
435
  m.classList.toggle('expanded');
436
  });
437
- document.getElementById('manualRefresh').addEventListener('click', refresh);
438
 
439
  // Kickoff + Polling
440
  refresh();
441
- setInterval(refresh, 15000); // 15s
442
  </script>
443
  </body>
444
  </html>
 
 
19
  .stat-card:hover { transform: translateY(-3px); box-shadow: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05); }
20
  .active-tab { border-bottom: 2px solid #4F46E5; color: #4F46E5; }
21
  .badge { font-size: 0.75rem; padding: 0.25rem 0.5rem; border-radius: 9999px; }
22
+ /* Fix the “descending page” by constraining chart height and forcing canvas to fill */
23
+ .chart-box { position: relative; }
24
+ .chart-box canvas { display:block; width:100% !important; height:100% !important; }
25
+
26
  @media (max-width: 768px) {
27
  .sidebar { position: fixed; z-index: 50; left: -280px; }
28
  .sidebar.open { left: 0; }
 
75
  </li>
76
  </ul>
77
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  </div>
79
  </aside>
80
 
81
  <!-- Main Content -->
82
  <main class="main-content flex-1 ml-0 md:ml-[280px]">
 
 
 
 
 
 
 
 
 
83
  <section class="p-6 md:p-8" data-aos="fade-up">
84
  <div class="flex items-center justify-between mb-6">
85
  <div>
 
97
  <div class="stat-card bg-white rounded-2xl p-4 border border-gray-200">
98
  <div class="flex items-center justify-between">
99
  <div>
100
+ <p class="text-sm text-gray-700">Total Feedback</p>
101
  <p id="kpiTotal" class="text-2xl font-bold">—</p>
102
  </div>
103
  <i data-feather="message-circle" class="w-8 h-8 text-indigo-600"></i>
104
  </div>
105
+ <p id="kpiTotalDelta" class="text-xs text-gray-500 mt-2">7d vs prev: —</p>
106
  </div>
107
  <div class="stat-card bg-white rounded-2xl p-4 border border-gray-200">
108
  <div class="flex items-center justify-between">
109
  <div>
110
+ <p class="text-sm text-gray-700">Avg. Rating</p>
111
  <p id="kpiAvg" class="text-2xl font-bold">—</p>
112
  </div>
113
  <i data-feather="star" class="w-8 h-8 text-yellow-500"></i>
 
117
  <div class="stat-card bg-white rounded-2xl p-4 border border-gray-200">
118
  <div class="flex items-center justify-between">
119
  <div>
120
+ <p class="text-sm text-gray-700">Positive Sentiment</p>
121
  <p id="kpiSentPos" class="text-2xl font-bold">—</p>
122
  </div>
123
  <i data-feather="smile" class="w-8 h-8 text-green-500"></i>
 
127
  <div class="stat-card bg-white rounded-2xl p-4 border border-gray-200">
128
  <div class="flex items-center justify-between">
129
  <div>
130
+ <p class="text-sm text-gray-700">Top Agent</p>
131
  <p id="kpiTopAgent" class="text-2xl font-bold">—</p>
132
  </div>
133
  <i data-feather="award" class="w-8 h-8 text-amber-600"></i>
 
143
  <h3 class="font-semibold">Feedback Volume (14 days)</h3>
144
  <span class="text-xs text-gray-500">live</span>
145
  </div>
146
+ <div class="chart-box h-72 md:h-72 lg:h-80">
147
+ <canvas id="chartVolume"></canvas>
148
+ </div>
149
  </div>
150
  <div class="bg-white rounded-2xl p-4 border border-gray-200">
151
  <div class="flex items-center justify-between mb-2">
152
  <h3 class="font-semibold">Sentiment Mix</h3>
153
  <span class="text-xs text-gray-500">auto</span>
154
  </div>
155
+ <div class="chart-box h-72 md:h-72 lg:h-80">
156
+ <canvas id="chartSentiment"></canvas>
157
+ </div>
158
  </div>
159
  </div>
160
 
161
+ <!-- Leaderboard -->
162
  <div class="bg-white rounded-2xl p-4 border border-gray-200 mb-8">
163
  <div class="flex items-center justify-between">
164
  <h3 class="font-semibold">Agents Leaderboard</h3>
 
198
  // --- CONFIG ---
199
  const HF_OWNER = 'BlackTesla';
200
  const HF_SPACE = 'archindus';
201
+ const PROXY_URL = ''; // set to your proxy endpoint to avoid CORS
 
 
 
 
 
 
202
 
203
  // --- UTILITIES ---
204
  const el = (id) => document.getElementById(id);
 
211
  return `${Math.floor(diff/86400)}d ago`;
212
  };
213
 
214
+ // Sentiment heuristic
215
  const POS = ['great','good','love','fast','helpful','amazing','excellent','awesome','smooth','wow'];
216
  const NEG = ['bad','slow','hate','bug','issue','error','crash','broken','lag','confusing','terrible'];
217
  function sentimentScore(text=''){
 
222
 
223
  // --- DATA ACCESS ---
224
  async function fetchFeedback(){
 
225
  if(PROXY_URL){
226
  const r = await fetch(PROXY_URL);
227
  if(!r.ok) throw new Error('Proxy fetch failed');
228
  return await r.json();
229
  }
 
 
 
230
  const url = `https://huggingface.co/api/spaces/${HF_OWNER}/${HF_SPACE}/discussions?limit=50`;
231
  try{
232
  const r = await fetch(url, { headers: { 'Accept': 'application/json' } });
233
  if(!r.ok) throw new Error('HF discussions not accessible from browser');
234
  const data = await r.json();
 
235
  const items = (data.discussions || data.items || []).map(d => ({
236
  id: d.id || d.num,
237
  title: d.title || 'Feedback',
 
239
  url: d.url || `https://huggingface.co/spaces/${HF_OWNER}/${HF_SPACE}/discussions/${d.num}`,
240
  author: d.author || (d.user && d.user.name) || 'user',
241
  text: (d.first ? d.first.content : d.body) || d.summary || '',
 
242
  rating: (()=>{ const m=(d.title||'').match(/rating\s*[:=]\s*(\d+(?:\.\d+)?)/i); return m?Number(m[1]):null; })(),
243
  agent: (()=>{ const m=(d.title||'').match(/agent\s*[:=]\s*([^\]\)\-:]+)/i); return m?m[1].trim():null; })()
244
  }));
 
294
  return { ...a, avg: Number(avg.toFixed(2)), score: Number(score.toFixed(2)) };
295
  }).sort((a,b)=> b.score - a.score);
296
 
 
297
  const days = Array.from({length:14}).map((_,i)=>{
298
  const d = new Date(); d.setDate(d.getDate()-(13-i));
299
  return d.toISOString().slice(0,10);
 
318
  chartVolume = new Chart(ctx1, {
319
  type: 'line',
320
  data: { labels: agg.volumeLabels, datasets: [{ label: 'Feedback', data: agg.volumeValues, tension: 0.35, fill: false }]},
321
+ options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } } }
322
  });
323
 
324
  const ctx2 = document.getElementById('chartSentiment');
 
332
 
333
  function renderLeaderboard(rows){
334
  const tbody = el('leaderboard');
335
+ if(!tbody) return;
336
  tbody.innerHTML = rows.map(r => `
337
  <tr class="border-b last:border-b-0">
338
  <td class="py-2 pr-4">
 
350
 
351
  function renderRecent(items){
352
  const ul = el('recentFeedback');
353
+ if(!ul) return;
354
  ul.innerHTML = items.slice(0,10).map(it=> `
355
  <li class="py-3">
356
  <a class="block" href="${it.url||'#'}" target="_blank">
 
376
  const delta = prev7 ? ((last7 - prev7)/prev7) : 0;
377
  el('kpiTotalDelta').textContent = `7d vs prev: ${(delta*100).toFixed(0)}%`;
378
 
379
+ const weightedSum = agg.leaderboard.reduce((acc,r)=> acc + r.avg*r.count, 0);
380
+ const totalCount = agg.leaderboard.reduce((a,r)=>a+r.count,0) || 1;
381
+ const avg = weightedSum / totalCount;
382
  el('kpiAvg').textContent = avg.toFixed(2);
383
+ el('kpiSentPos').textContent = fmtPct(agg.pos / Math.max(1, agg.total));
 
384
  const top = agg.leaderboard[0];
385
  el('kpiTopAgent').textContent = top ? top.name : '—';
386
  el('kpiTopAgentScore').textContent = top ? `score ${top.score.toFixed(2)} · avg ${top.avg.toFixed(2)}` : '—';
387
  }
388
 
389
+ // Cache last non-empty payload to avoid zeroed KPIs on transient empty responses
390
+ let lastItems = [];
391
  async function refresh(){
392
+ try{
393
+ const resp = await fetchFeedback();
394
+ let items = Array.isArray(resp?.items) ? resp.items : [];
395
+ if(!items.length){
396
+ items = lastItems.length ? lastItems : mockFeedback().items;
397
+ } else {
398
+ lastItems = items;
399
+ }
400
+ const agg = aggregate(items);
401
+ renderCharts(agg);
402
+ renderLeaderboard(agg.leaderboard);
403
+ renderRecent(items.sort((a,b)=> new Date(b.createdAt) - new Date(a.createdAt)));
404
+ renderKpis(agg);
405
+ const lu = document.getElementById('lastUpdated');
406
+ if(lu){ lu.textContent = `Last update: ${new Date().toLocaleTimeString()}`; lu.classList.remove('text-red-600'); }
407
+ } catch(e){
408
+ const { items } = mockFeedback();
409
+ const agg = aggregate(items);
410
+ renderCharts(agg);
411
+ renderLeaderboard(agg.leaderboard);
412
+ renderRecent(items);
413
+ renderKpis(agg);
414
+ const lu = document.getElementById('lastUpdated');
415
+ if(lu){ lu.textContent = `Last update: fallback ${new Date().toLocaleTimeString()}`; lu.classList.add('text-red-600'); }
416
+ }
417
  }
418
 
419
  // --- WIRE-UP ---
420
+ document.getElementById('sidebarToggle')?.addEventListener('click', ()=>{
 
 
 
421
  const s = document.querySelector('.sidebar');
422
  const m = document.querySelector('.main-content');
423
  s.classList.toggle('collapsed');
424
  m.classList.toggle('expanded');
425
  });
426
+ document.getElementById('manualRefresh')?.addEventListener('click', refresh);
427
 
428
  // Kickoff + Polling
429
  refresh();
430
+ setInterval(refresh, 15000);
431
  </script>
432
  </body>
433
  </html>
434
+