
    {iw                    F   U d Z ddlmZ ddlZddlZddlZddlZddlZddl	Z	ddl
mZ ddlmZ ddlmZ  ee      j!                         j"                  Zej"                  Zedz  dz  Zed	z  Zi d
ddddddddddddddddddddddddddddddddd i d!d d"d d#d$d%d$d&d$d'd(d)d(d*d(d+d(d,d(d-d(d.d(d/d(d0d(d1d(d2d(d3d(d4d4d4d5d5d6Zd7ed8<   d9d:d;d<d=d>d?d@dAdBdCdDZd7edE<   dOdFZdPdGZdQdHZdRdIZdSdJZdKZdTdLZdUdMZ e!dNk(  r e         yy)Vu  
RingAI – Call Analytics Dashboard

Scans dataset/calls/ and generates a single, self-contained HTML report.

Usage
-----
    cd ~/my/fyp/ringai
    python dashboard.py              # writes dashboard.html
    python dashboard.py --serve      # also serves on http://localhost:8080
    )annotationsN)defaultdict)datetime)Pathdatasetcallszdashboard.htmlerp_inquiryERP	erp_clouderp_demozerp_on premiseerp_featurestraffic_erpcrm_inquiryCRMcrm_featurespricingPricingpricing_inquiryschedule_call
Schedulingdate_requestclosingClosinggoodbyefarewell	thank_youintroductionIntroductiongreetinginitialclarificationClarificationclarification_neededneeds_clarificationgeneralGeneralgeneral_requestgeneral_discussiongeneral_concerngeneral_inquiryinfo_requestservice_informationfeature_detailsdata_package_infoimplementation_detailssoftware_namesolution_power
EscalationUnknown)contactcontact_agenthuman_agentnoneunknownzdict[str, str]
INTENT_MAPz#6366f1z#8b5cf6z#f59e0bz#10b981z#ec4899z#3b82f6z#f97316z#64748bz#ef4444z#a3a3a3z#475569)r
   r   r   r   r   r   r"   r&   r2   Otherr3   GROUP_COLORSc                    | r| j                         syt        j                  | j                         j                         d      S )u+   Raw intent string  →  display-group name.r3   r:   )stripr9   getlower)raws    ./home/sas/my/fyp/ringai/analytics/dashboard.pynormrB   R   s0    ciik>>#))+++-w77    c                P    t        j                  | d      j                  dd      S )z0JSON-encode for embedding inside a <script> tag.F)ensure_asciiz</z<\/)jsondumpsreplace)objs    rA   	safe_jsonrJ   Y   s     ::c.66tVDDrC   c                     t         j                         sg S t        t         j                               D  cg c]  } | j                         st	        |         c} S c c} w )N)
CALLS_ROOTis_dirsortediterdir	_load_one)ds    rA   
load_callsrR   _   sA    	"(););)=">MQ!((*IaLMMMs   AAc                   | j                   d dg g d| dz  dz  j                         d}| dz  }|j                         r]t        |d      5 }t        t	        j
                  |            }d d d        t              |d	<   |r|d   j                  d
      nd |d<   | dz  dz  }|j                         rMt        |d      5 }|D cg c](  }|j                         st        j                  |      * c}|d<   d d d        | dz  dz  }|j                         rMt        |d      5 }|D cg c](  }|j                         st        j                  |      * c}|d<   d d d        | dz  dz  }|j                         r#|j                  d      j                         |d<   |S # 1 sw Y   :xY wc c}w # 1 sw Y   xY wc c}w # 1 sw Y   ixY w)Nr    audioz
merged.wav)call_idfirst_timestampduration_sec
utterances
llm_eventsfinal_transcript
has_mergedzmetadata.csvutf-8encodingrX   	timestamprW   
transcriptzutterances.jsonlrY   llmzevents.jsonlrZ   z	final.txtr[   )nameis_fileopenlistcsv
DictReaderlenr>   r=   rF   loads	read_text)	cdrecmpfrowsuplnlpfps	            rA   rP   rP   e   s   GG '\L8AACC 
n	B	zz|"w' 	+1q)*D	+!$TN=Aa[!9t 
l	/	/B	zz|"w' 	K1:; JBrxxzB JC	K 
en	$B	zz|"w' 	K1:; JBrxxzB JC	K 
l	[	(B	zz|"$,,,"@"F"F"HJ-	+ 	+ !K	K 	K !K	K 	KsT   F" F4F/F/2F4%G*G  G G"F,/F44F= GGc                   t        |       }t        d | D              }t        t              }| D ]B  }|d   D ]8  }|t	        |j                  di       j                  dd            xx   dz  cc<   : D dddddd}|D ][  }|d	k  r|d
xx   dz  cc<   |dk  r|dxx   dz  cc<   )|dk  r|dxx   dz  cc<   <|dk  r|dxx   dz  cc<   O|dxx   dz  cc<   ] t        d | D              }t        d | D              }	g }
| D ]  }|d   D cg c]1  }d|vr+t	        |j                  di       j                  dd            3 }}|r t        t        |      |j                        nd}|d   D cg c]  }d|v r	d|vr|d    }}|
j                  |d   |d   |d   t        |d         t        |d         ||r t        t        |      t        |      z        ndt        d |d   D              rdnt        d |d   D              rdnd d!        t        d" | D              }|t        d# | D              t        d$ | D              ||rt        ||z  d      nd|r t        t        |      t        |      z        nd|r|t        |      d%z     nd||	|rt        |t        |      z  d&z  d      nd't        |      ||
d(S c c}w c c}w ))Nc              3  J   K   | ]  }|d    D ]  }d|v rd|vr|d      yw)rZ   llm_mserrorN .0cevs      rA   	<genexpr>zaggregate.<locals>.<genexpr>   sC      L/ r>gR/ 	8s   !#rZ   rb   intentrT      r   )   0–1s   1–3s   3–5s   5–10s10s+i  r   i  r   i  r   i'  r   r   c              3  X   K   | ]"  }|d    D ]  }|j                  d      sd  $ yw)rZ   fallbackr   Nr>   rz   s      rA   r~   zaggregate.<locals>.<genexpr>   s+     N1,N"266*;MQNQNs   *	*c              3  >   K   | ]  }|d    D ]  }d|v sd   yw)rZ   rx   r   Nry   rz   s      rA   r~   zaggregate.<locals>.<genexpr>   s&     I1,I"7b=QIQIs   	rx   )keyzN/Arw   rV   rW   rX   rY   c              3  $   K   | ]  }d |v  
 yw)rx   Nry   r{   r}   s     rA   r~   zaggregate.<locals>.<genexpr>   s     !P"'R-!Ps   Errorc              3  >   K   | ]  }|j                  d         yw)r   Nr   r   s     rA   r~   zaggregate.<locals>.<genexpr>   s     &Tbrvvj'9&Ts   FallbackNormal)rV   tsdurationutts	llm_countprimary_intentavg_latstatusc              3  &   K   | ]	  }|d      yw)rX   Nry   r{   r|   s     rA   r~   zaggregate.<locals>.<genexpr>   s     .1Q~.s   c              3  ,   K   | ]  }|d    s	d  yw)rZ   r   Nry   r   s     rA   r~   zaggregate.<locals>.<genexpr>   s     DaAlODs   
c              3  8   K   | ]  }t        |d            yw)rY   N)ri   r   s     rA   r~   zaggregate.<locals>.<genexpr>   s     DAlO 4Ds      d   g        )total_callscalls_with_llmtotal_utterancestotal_durationavg_durationavg_latencymedian_latencyfallback_counterror_countfallback_rateintent_countslatency_buckets	summaries)ri   rN   r   intrB   r>   summaxsetcountappendroundanydict)r   nlatsicr|   r}   bkltfbersumsnormsprimarycltds                  rA   	aggregater      s   E
A   D %S)B ?L/ 	?BtBFF5"%))(B789Q>9	??
 aAq	IB *4ZH!+4ZH!+4ZH!+%ZI!+F)* 
NN	NB	II	IB D  o
b  r"&&x45
 

 7<#c%jekk2 o
2~'"3 xL
 

 		l 12/!!L/2!!L/2%:<eCGc"g$56!!!P,!PP#&&TAlO&T#TZ
 	6 
..	.BD5DDDeDD01E"q&!,q<@E#d)c$i"78a48DTa0a>BE"s4y.3"6: H  7

s   6K2K	u{K  <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>RingAI – Call Analytics</title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4/dist/chart.umd.min.js"></script>
<style>
/* ── reset & tokens ── */
*{margin:0;padding:0;box-sizing:border-box}
:root{
  --bg:#0f172a; --card:#1e293b; --card2:#243447;
  --border:#334155; --txt:#e2e8f0; --txt2:#94a3b8;
  --acc:#6366f1; --ok:#10b981; --warn:#f59e0b;
  --err:#ef4444; --r:10px;
}
body{
  font-family:'Inter',system-ui,-apple-system,sans-serif;
  background:var(--bg); color:var(--txt);
  min-height:100vh; padding:28px 24px;
}
.pg{max-width:1380px;margin:0 auto}

/* ── header ── */
.hdr{display:flex;justify-content:space-between;align-items:center;margin-bottom:24px;flex-wrap:wrap;gap:8px}
.hdr h1{font-size:1.5rem;font-weight:700;letter-spacing:-.025em}
.hdr h1 .hi{color:var(--acc)}
.hdr .gen{color:var(--txt2);font-size:.79rem;margin-top:2px}
.btn{background:var(--acc);color:#fff;border:none;border-radius:6px;padding:7px 15px;cursor:pointer;font-size:.82rem;font-weight:600;transition:background .2s}
.btn:hover{background:#4f46e5}

/* ── summary cards ── */
.cards{display:grid;grid-template-columns:repeat(6,1fr);gap:14px;margin-bottom:24px}
@media(max-width:1100px){.cards{grid-template-columns:repeat(3,1fr)}}
@media(max-width:580px){.cards{grid-template-columns:repeat(2,1fr)}}
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:18px 16px}
.card .v{font-size:1.7rem;font-weight:700;line-height:1.1;margin-bottom:3px}
.card .l{color:var(--txt2);font-size:.75rem;text-transform:uppercase;letter-spacing:.06em;font-weight:600}
.card .s{color:var(--txt2);font-size:.72rem;margin-top:5px}
.c-acc .v{color:var(--acc)}
.c-pur .v{color:#a78bfa}
.c-ok  .v{color:var(--ok)}
.c-sky .v{color:#38bdf8}
.c-wrn .v{color:var(--warn)}
.c-err .v{color:var(--err)}

/* ── charts row ── */
.charts{display:grid;grid-template-columns:1fr 1fr;gap:18px;margin-bottom:24px}
@media(max-width:700px){.charts{grid-template-columns:1fr}}
.cbox{background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:22px}
.cbox h3{font-size:.82rem;font-weight:600;color:var(--txt2);text-transform:uppercase;letter-spacing:.05em;margin-bottom:16px}

/* donut wrapper */
.dwrap{display:flex;align-items:center;gap:24px}
.dwrap canvas{width:190px!important;height:190px!important;flex-shrink:0}
.legend{display:flex;flex-direction:column;gap:5px}
.li{display:flex;align-items:center;gap:7px;font-size:.79rem}
.ld{width:11px;height:11px;border-radius:3px;flex-shrink:0}
.li .cnt{color:var(--txt2);margin-left:auto;font-size:.75rem}
@media(max-width:480px){.dwrap{flex-direction:column}.dwrap canvas{width:160px!important;height:160px!important}}

/* ── call-log table ── */
.tsec{background:var(--card);border:1px solid var(--border);border-radius:var(--r);padding:20px;overflow-x:auto;margin-bottom:24px}
.ttop{display:flex;justify-content:space-between;align-items:center;margin-bottom:14px;flex-wrap:wrap;gap:10px}
.ttop h3{font-size:.82rem;font-weight:600;color:var(--txt2);text-transform:uppercase;letter-spacing:.05em}
.srch{background:#0f172a;border:1px solid var(--border);border-radius:6px;color:var(--txt);padding:7px 12px;font-size:.83rem;width:230px;outline:none;transition:border-color .2s}
.srch:focus{border-color:var(--acc)}
table{width:100%;border-collapse:collapse;font-size:.82rem}
thead th{text-align:left;padding:9px 10px;color:var(--txt2);font-weight:600;border-bottom:1px solid var(--border);white-space:nowrap;cursor:pointer;user-select:none;-webkit-user-select:none;transition:color .15s}
thead th:hover{color:var(--txt)}
thead th .si{opacity:.35;margin-left:4px;font-size:.7rem}
thead th.active{color:var(--acc)}
thead th.active .si{opacity:1}
tbody tr{border-bottom:1px solid rgba(51,65,85,.4);cursor:pointer;transition:background .15s}
tbody tr:hover{background:var(--card2)}
tbody td{padding:9px 10px}
.mono{font-family:'SF Mono','Fira Code',monospace;font-size:.76rem;color:var(--txt2)}

/* badges */
.badge{display:inline-block;border-radius:4px;padding:2px 8px;font-size:.73rem;font-weight:600}
.b-n{background:rgba(16,185,129,.12);color:var(--ok)}
.b-f{background:rgba(245,158,11,.12);color:var(--warn)}
.b-e{background:rgba(239,68,68,.12);color:var(--err)}
.lat-ok{color:var(--ok)}
.lat-md{color:var(--warn)}
.lat-bd{color:var(--err)}

/* ── modal ── */
.ovl{position:fixed;inset:0;background:rgba(0,0,0,.6);z-index:200;display:none;align-items:center;justify-content:center;backdrop-filter:blur(2px)}
.ovl.open{display:flex}
.mdl{background:var(--card);border:1px solid var(--border);border-radius:12px;width:90%;max-width:740px;max-height:84vh;overflow-y:auto;padding:28px;position:relative}
.mdl-x{position:absolute;top:14px;right:18px;background:none;border:none;color:var(--txt2);font-size:1.45rem;cursor:pointer;line-height:1;transition:color .15s}
.mdl-x:hover{color:var(--txt)}
.mdl-hd{display:flex;align-items:center;gap:10px;margin-bottom:20px;flex-wrap:wrap}
.mdl-hd h2{font-size:1rem;font-weight:700;font-family:monospace;word-break:break-all}
.msec{margin-bottom:18px}
.msec h4{font-size:.77rem;color:var(--txt2);text-transform:uppercase;letter-spacing:.06em;font-weight:600;margin-bottom:8px}

/* transcript box */
.tbox{background:#0f172a;border-radius:6px;padding:12px 14px;font-size:.83rem;line-height:1.6;word-break:break-word;white-space:pre-wrap}

/* conversation thread */
.thread{display:flex;flex-direction:column;gap:9px}
.msg{display:flex;gap:10px;align-items:flex-start}
.av{width:28px;height:28px;border-radius:50%;flex-shrink:0;display:flex;align-items:center;justify-content:center;font-size:.68rem;font-weight:700;color:#fff}
.av-u{background:#3b82f6}
.av-a{background:var(--acc)}
.bub{background:#0f172a;border-radius:7px;padding:9px 12px;flex:1;font-size:.82rem;line-height:1.5}
.bub .who{font-weight:700;font-size:.71rem;color:var(--txt2);margin-bottom:2px}
.bub .wat{color:var(--txt);word-break:break-word}
.bub .meta{margin-top:5px;font-size:.71rem;color:var(--txt2);display:flex;align-items:center;gap:6px;flex-wrap:wrap}
.bub .lat{font-weight:600}
.tag{padding:1px 5px;border-radius:3px;font-size:.7rem;font-weight:600}
.tag-fb{background:rgba(245,158,11,.12);color:var(--warn)}
.tag-cl{background:rgba(251,191,36,.1);color:#fbbf24}
.err-bub{margin-left:38px;background:rgba(239,68,68,.08);border:1px solid rgba(239,68,68,.2);border-radius:6px;padding:7px 10px;font-size:.78rem;color:var(--err)}

/* empty-state row */
.empty{text-align:center;color:var(--txt2);padding:28px;font-size:.84rem}

/* footer */
.footer{text-align:center;color:var(--txt2);font-size:.75rem;padding-top:16px;border-top:1px solid var(--border)}
</style>
</head>
<body>
<div class="pg">

<!-- ── header ── -->
<div class="hdr">
  <div>
    <h1><span class="hi">Ring</span>AI – Call Analytics</h1>
    <div class="gen">Generated __GENERATED_AT__</div>
  </div>
  <button class="btn" onclick="location.reload()">&#8635; Refresh</button>
</div>

<!-- ── summary cards ── -->
<div class="cards">
  <div class="card c-acc"><div class="v">__TOTAL_CALLS__</div><div class="l">Total Calls</div></div>
  <div class="card c-pur"><div class="v">__CALLS_WITH_LLM__</div><div class="l">LLM Active</div><div class="s">__LLM_PCT__% of total</div></div>
  <div class="card c-ok" ><div class="v">__AVG_DUR__s</div><div class="l">Avg Duration</div><div class="s">__TOT_DUR__s total</div></div>
  <div class="card c-sky"><div class="v">__TOT_UTTS__</div><div class="l">Utterances</div></div>
  <div class="card c-wrn"><div class="v">__AVG_LAT__ms</div><div class="l">Avg LLM Latency</div><div class="s">median __MED_LAT__ms</div></div>
  <div class="card c-err"><div class="v">__FB_RATE__%</div><div class="l">Fallback Rate</div><div class="s">__FB_CNT__ fallbacks &middot; __ERR_CNT__ errors</div></div>
</div>

<!-- ── charts ── -->
<div class="charts">
  <!-- intent donut -->
  <div class="cbox">
    <h3>Intent Distribution</h3>
    <div class="dwrap">
      <canvas id="cIntent"></canvas>
      <div class="legend" id="legend"></div>
    </div>
  </div>

  <!-- latency histogram -->
  <div class="cbox">
    <h3>LLM Response Latency</h3>
    <canvas id="cLat"></canvas>
  </div>
</div>

<!-- ── call log ── -->
<div class="tsec">
  <div class="ttop">
    <h3>Call Log (__TOTAL_CALLS__ calls)</h3>
    <input class="srch" type="text" placeholder="Search call ID, intent…" id="srch" oninput="doFilter()"/>
  </div>
  <table>
    <thead><tr>
      <th data-col="ts"             onclick="doSort(this)">Date            <span class="si">&#9660;</span></th>
      <th data-col="call_id"        onclick="doSort(this)">Call ID         <span class="si">&#9660;</span></th>
      <th data-col="duration"       onclick="doSort(this)">Duration        <span class="si">&#9660;</span></th>
      <th data-col="utts"           onclick="doSort(this)">Utts            <span class="si">&#9660;</span></th>
      <th data-col="llm_count"      onclick="doSort(this)">LLM Events      <span class="si">&#9660;</span></th>
      <th data-col="primary_intent" onclick="doSort(this)">Intent          <span class="si">&#9660;</span></th>
      <th data-col="avg_lat"        onclick="doSort(this)">Avg Lat         <span class="si">&#9660;</span></th>
      <th data-col="status"         onclick="doSort(this)">Status          <span class="si">&#9660;</span></th>
    </tr></thead>
    <tbody id="tbody"></tbody>
  </table>
</div>

<!-- ── call-detail modal ── -->
<div class="ovl" id="ovl">
  <div class="mdl">
    <button class="mdl-x" onclick="closeModal()">&times;</button>
    <div id="mdlBody"></div>
  </div>
</div>

<div class="footer">RingAI Call Analytics &middot; generated from <code>dataset/calls/</code></div>
</div><!-- /pg -->

<!-- ─────────────────── embedded data ─────────────────── -->
<script>
const CALLS   = __CALLS_JSON__;
const DATA    = __DATA_JSON__;
const COLORS  = __COLORS_JSON__;
const IMAP    = __IMAP_JSON__;
const BUCKETS = __BUCKETS_JSON__;
</script>

<!-- ─────────────────── app logic ─────────────────── -->
<script>
// ── utility functions ──────────────────────────────────────────
function esc(s){ return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;') }

function fts(t){
  if(!t) return 'N/A';
  try{
    const M=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
    return +(t.slice(6,8))+' '+M[+(t.slice(4,6))-1]+' '+t.slice(0,4)+'  '+t.slice(9,11)+':'+t.slice(12,14);
  }catch(e){ return t; }
}

function normI(raw){
  if(!raw||!raw.trim()) return 'Unknown';
  return IMAP[raw.trim().toLowerCase()] || 'Other';
}

function latCls(ms){ return !ms ? '' : ms<3000 ? 'lat-ok' : ms<7000 ? 'lat-md' : 'lat-bd'; }

// ── intent donut chart ─────────────────────────────────────────
(function(){
  const labels = Object.keys(DATA.intent_counts);
  const vals   = labels.map(k => DATA.intent_counts[k]);
  const cols   = labels.map(k => COLORS[k] || '#a3a3a3');
  const total  = vals.reduce((a,b) => a+b, 0);

  new Chart(document.getElementById('cIntent'), {
    type: 'doughnut',
    data: {
      labels,
      datasets: [{ data: vals, backgroundColor: cols, borderColor: '#1e293b', borderWidth: 2 }]
    },
    options: {
      responsive: true,
      maintainAspectRatio: true,
      cutout: '62%',
      plugins: {
        legend: { display: false },
        tooltip: {
          callbacks: {
            label: function(c){ return ' '+c.label+': '+c.parsed+' ('+(c.parsed/total*100).toFixed(0)+'%)'; }
          }
        }
      }
    }
  });

  // manual legend
  const leg = document.getElementById('legend');
  labels.forEach(function(l, i){
    leg.innerHTML += '<div class="li"><span class="ld" style="background:'+cols[i]+'"></span>'+l+'<span class="cnt">'+vals[i]+'</span></div>';
  });
})();

// ── latency bar chart ──────────────────────────────────────────
(function(){
  const labels = Object.keys(BUCKETS);
  const vals   = labels.map(function(k){ return BUCKETS[k]; });

  new Chart(document.getElementById('cLat'), {
    type: 'bar',
    data: {
      labels,
      datasets: [{
        data: vals,
        backgroundColor: ['#10b981','#10b981','#f59e0b','#f59e0b','#ef4444'],
        borderRadius: 4,
        borderSkipped: false
      }]
    },
    options: {
      responsive: true,
      maintainAspectRatio: true,
      plugins: { legend: { display: false } },
      scales: {
        x: { ticks: { color: '#94a3b8' }, grid: { color: 'rgba(51,65,85,.25)' } },
        y: { beginAtZero: true, ticks: { color: '#94a3b8' }, grid: { color: 'rgba(51,65,85,.25)' },
             title: { display: true, text: 'Events', color: '#94a3b8' } }
      }
    }
  });
})();

// ── table ──────────────────────────────────────────────────────
var _sort = { col: 'ts', asc: false };

function getSorted(){
  var d = DATA.summaries.slice();   // shallow copy
  var col = _sort.col, asc = _sort.asc;
  d.sort(function(a,b){
    var va = a[col], vb = b[col];
    if(typeof va === 'string' && typeof vb === 'string')
      return asc ? va.localeCompare(vb) : vb.localeCompare(va);
    return asc ? (va||0)-(vb||0) : (vb||0)-(va||0);
  });
  return d;
}

function render(arr){
  var tb = document.getElementById('tbody');
  if(!arr.length){
    tb.innerHTML = '<tr><td colspan="8" class="empty">No matching calls</td></tr>';
    return;
  }
  var out = '';
  arr.forEach(function(c){
    var sc = c.status==='Normal' ? 'b-n' : c.status==='Fallback' ? 'b-f' : 'b-e';
    var ic = COLORS[c.primary_intent] || '#a3a3a3';
    out +=
      '<tr onclick="openModal(\''+c.call_id+'\')">'+
      '<td>'+fts(c.ts)+'</td>'+
      '<td class="mono">'+c.call_id.slice(0,28)+'…</td>'+
      '<td>'+c.duration+'s</td>'+
      '<td>'+c.utts+'</td>'+
      '<td>'+c.llm_count+'</td>'+
      '<td><span class="badge" style="background:'+ic+'1a;color:'+ic+'">'+c.primary_intent+'</span></td>'+
      '<td class="'+latCls(c.avg_lat)+'">'+(c.avg_lat ? c.avg_lat+'ms' : '—')+'</td>'+
      '<td><span class="badge '+sc+'">'+c.status+'</span></td>'+
      '</tr>';
  });
  tb.innerHTML = out;
}

function doSort(th){
  var col = th.dataset.col;
  if(_sort.col === col) _sort.asc = !_sort.asc;
  else _sort = { col: col, asc: true };
  document.querySelectorAll('thead th').forEach(function(h){ h.classList.remove('active'); });
  th.classList.add('active');
  render(getSorted());
}

function doFilter(){
  var q = document.getElementById('srch').value.toLowerCase();
  render(getSorted().filter(function(c){
    return c.call_id.toLowerCase().indexOf(q) !== -1 ||
           c.primary_intent.toLowerCase().indexOf(q) !== -1 ||
           c.status.toLowerCase().indexOf(q) !== -1;
  }));
}

// initial paint  +  mark Date column as active
render(getSorted());
document.querySelectorAll('thead th')[0].classList.add('active');

// ── modal ──────────────────────────────────────────────────────
function openModal(cid){
  var call = CALLS.find(function(c){ return c.call_id === cid; });
  if(!call) return;

  var hasErr  = call.llm_events.some(function(e){ return 'error' in e; });
  var hasFb   = call.llm_events.some(function(e){ return !!e.fallback; });
  var sCls    = hasErr ? 'b-e' : hasFb ? 'b-f' : 'b-n';
  var sTxt    = hasErr ? 'Error' : hasFb ? 'Fallback' : 'Normal';

  var h = '<div class="mdl-hd">'+
    '<h2>'+cid+'</h2>'+
    '<span class="badge '+sCls+'">'+sTxt+'</span>'+
    '<span class="badge" style="background:rgba(99,102,241,.12);color:#a78bfa">'+call.duration_sec+'s</span>'+
    '<span class="badge" style="background:rgba(148,163,184,.1);color:var(--txt2)">'+fts(call.first_timestamp)+'</span>'+
    '</div>';

  // ── transcript ──
  if(call.final_transcript){
    h += '<div class="msec"><h4>Transcript</h4>'+
         '<div class="tbox">'+esc(call.final_transcript)+'</div></div>';
  }

  // ── conversation thread  (LLM events)  ──
  if(call.llm_events.length > 0){
    h += '<div class="msec"><h4>Conversation</h4><div class="thread">';

    call.llm_events.forEach(function(ev){
      // caller bubble
      h += '<div class="msg">'+
           '<div class="av av-u">U</div>'+
           '<div class="bub"><div class="who">Caller</div>'+
           '<div class="wat">'+esc(ev.utterance||'')+'</div></div></div>';

      // error bubble (no AI response)
      if('error' in ev){
        h += '<div class="err-bub">&#9888; LLM Error: '+esc(ev.error)+'</div>';
        return;                                              // skip AI bubble
      }

      // AI bubble
      var llm    = ev.llm || {};
      var reply  = llm.assistant_reply || llm.question || '';
      var intent = llm.intent || '';
      var grp    = normI(intent);
      var ic     = COLORS[grp] || '#a3a3a3';

      var meta = '';
      if(ev.llm_ms)                  meta += '<span class="lat '+latCls(ev.llm_ms)+'">'+ev.llm_ms+'ms</span>';
      if(intent)                     meta += '<span class="badge" style="background:'+ic+'1a;color:'+ic+';padding:1px 5px">'+grp+'</span>';
      if(ev.fallback)                meta += '<span class="tag tag-fb">fallback</span>';
      if(llm.needs_clarification)    meta += '<span class="tag tag-cl">clarify</span>';

      h += '<div class="msg">'+
           '<div class="av av-a">AI</div>'+
           '<div class="bub"><div class="who">RingAI</div>'+
           '<div class="wat">'+esc(reply)+'</div>'+
           '<div class="meta">'+meta+'</div></div></div>';
    });

    h += '</div></div>';   // close thread + msec

  // ── utterances only  (pre-LLM calls) ──
  } else if(call.utterances.length > 0){
    h += '<div class="msec"><h4>Utterances</h4><div class="thread">';

    call.utterances.forEach(function(u, i){
      var dur = (u.utter_ms / 1000).toFixed(1);
      var wc  = u.wallclock ? ' (wall-clock flush)' : '';
      h += '<div class="msg">'+
           '<div class="av av-u">U</div>'+
           '<div class="bub"><div class="who">Utterance '+(i+1)+' &#8226; '+dur+'s'+wc+'</div>'+
           '<div class="wat">'+esc(u.text||'')+'</div></div></div>';
    });

    h += '</div></div>';
  }

  document.getElementById('mdlBody').innerHTML = h;
  document.getElementById('ovl').classList.add('open');
}

function closeModal(){ document.getElementById('ovl').classList.remove('open'); }
document.getElementById('ovl').addEventListener('click', function(e){ if(e.target===e.currentTarget) closeModal(); });
document.addEventListener('keydown', function(e){ if(e.key==='Escape') closeModal(); });
</script>
</body>
</html>
c                    |d   rt        |d   |d   z  dz  d      nd}t        j                  dt        j                         j                  d            j                  dt        |d               j                  d	t        |d               j                  d
t        |            j                  dt        |d               j                  dt        |d               j                  dt        |d               j                  dt        |d               j                  dt        |d               j                  dt        |d               j                  dt        |d               j                  dt        |d               j                  dt        |             j                  dt        |            j                  dt        t                    j                  dt        t                    j                  dt        |d                S )!Nr   r   r   r   r   __GENERATED_AT__z%d %b %Y  %H:%M__TOTAL_CALLS____CALLS_WITH_LLM____LLM_PCT____AVG_DUR__r   __TOT_DUR__r   __TOT_UTTS__r   __AVG_LAT__r   __MED_LAT__r   __FB_RATE__r   
__FB_CNT__r   __ERR_CNT__r   __CALLS_JSON____DATA_JSON____COLORS_JSON____IMAP_JSON____BUCKETS_JSON__r   )
r   HTMLrH   r   nowstrftimestrrJ   r;   r9   )r   datallm_pcts      rA   generate_htmlr     s   NRS`NaeD)*T--@@3FJghG 		#hlln&=&=>O&P	Q	"c$}*=&>	?	%c$/?*@&A	B	c'l	3	c$~*>&?	@	c$/?*@&A	B	c$/A*B&C	D	c$}*=&>	?	c$/?*@&A	B	c$*?&@	A	c$/?*@&A	B	c$}*=&>	?	!i&6	7	io	6	"i&=	>	i
&;	<	#i5F0G&H	I'rC   c                    t        j                  d      } | j                  ddd       | j                  ddd	
       | j                  dt        dd       | j	                         }t               }|s't        dt                t        j                  d       t        |      }t        ||      }t        j                  |d       t                t        dt                t        d|d           t        d|d           t        d|d           t        d|d    d       t                |j                  rt        j                   t#        t$                     t&        j(                  j*                  }t&        j(                  j-                  |j.                  |j0                  f|      5 }t        d|j.                   d|j0                   d       |j3                          d d d        y t        d        y # 1 sw Y   y xY w)!NzRingAI Call Analytics Dashboard)descriptionz--serve
store_truezStart HTTP server)actionhelpz--hostz0.0.0.0z"Host to bind to (default: 0.0.0.0))defaultr   z--porti  z Port to serve on (default: 8081))typer   r   zNo call directories found in r   r]   r^   u     Dashboard  →  z  Total calls       r   z  LLM-active        r   z  Utterances        r   z  Avg LLM latency   r   z msz  Serving on http://:z/dashboard.htmlz:  Open dashboard.html in a browser, or re-run with --serve)argparseArgumentParseradd_argumentr   
parse_argsrR   printrL   sysexitr   r   OUTPUT
write_textserveoschdirr   HEREhttpserverSimpleHTTPRequestHandler
HTTPServerhostportserve_forever)parserargsr   r   htmlhandlersrvs          rA   mainr     s   $$1RSF
	,=PQ
):^_
sD?abDLE-j\:;UD%D
dW-	G	vh
'(	 m!4 5
67	 &6!7 8
9:	 &8!9 :
;<	 m!4 5S
9:	Gzz
T++66[[##TYY		$:GD 	 (1TYYKOP	  	  	JK		  	 s   27G>>H__main__)r@   r   returnr   )r   r   )r   
list[dict])rl   r   r   r   )r   r   r   r   )r   r   r   r   r   r   )r   None)"__doc__
__future__r   r   rg   http.serverr   rF   r   r   collectionsr   r   pathlibr   __file__resolveparentr   PROJECT_ROOTrL   r   r9   __annotations__r;   rB   rJ   rR   rP   r   r   r   r   __name__ry   rC   rA   <module>r     s  
 #  
   	 
 #   (^##%,,{{I%/
,,5%u.8% e ,U 5B5
 5
 )% y ,Y \ $2< y $Y 	 '	 N %/ ~" _#" '=o#$ ?%( y)( ,Y)* )+* &7	+, y-, #1)-. 9/. '8/0 10 %=i12 Y32 !1)36 l)=
N D  n  8EN$PJbwv6LD zF rC   