← back to Zendesk Chat Analyzer
public/index.html
173 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>DW Chat History Analyzer</title>
<!-- href-drill:exempt reason="one-shot chat-history analyzer — analysis output, no filterable data grid" (Steve 2026-08-23, exclusion-marker verdict) -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/wordcloud@1.2.2/src/wordcloud2.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vis-network@9.1.9/standalone/umd/vis-network.min.js"></script>
<style>
:root{--bg:#0f1115;--card:#181b22;--line:#262b36;--ink:#e8eaf0;--dim:#9aa3b2;--acc:#6ea8fe;--good:#4ade80;--warn:#fbbf24;--bad:#f87171}
*{box-sizing:border-box}
body{margin:0;background:var(--bg);color:var(--ink);font:14px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif}
header{padding:18px 24px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:16px;position:sticky;top:0;background:var(--bg);z-index:5}
header h1{font-size:18px;margin:0;font-weight:700}
header .meta{color:var(--dim);font-size:12px}
header .sp{flex:1}
button{background:var(--card);color:var(--ink);border:1px solid var(--line);border-radius:8px;padding:7px 12px;cursor:pointer;font-size:13px}
button:hover{border-color:var(--acc)}
.golive{background:var(--good);color:#08130b;border:1px solid var(--good);border-radius:8px;padding:7px 13px;font-size:13px;font-weight:700;text-decoration:none;display:inline-flex;align-items:center;gap:5px}
.golive:hover{filter:brightness(1.1);text-decoration:none}
.wrap{padding:20px 24px;max-width:1400px;margin:0 auto}
.kpis{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:12px;margin-bottom:20px}
.kpi{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px 16px}
.kpi .n{font-size:26px;font-weight:800}
.kpi .l{color:var(--dim);font-size:12px;text-transform:uppercase;letter-spacing:.04em}
.kpi .s{font-size:11px;color:var(--dim);margin-top:2px}
.kpi.clk{cursor:pointer;transition:border-color .15s}
.kpi.clk:hover{border-color:var(--warn)}
.kpi.clk:hover .s{color:var(--warn)}
.clr{color:var(--acc);cursor:pointer}
.grid{display:grid;grid-template-columns:repeat(12,1fr);gap:16px}
.card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:16px;min-width:0}
.card h3{margin:0 0 12px;font-size:13px;color:var(--dim);text-transform:uppercase;letter-spacing:.04em;font-weight:700}
.c6{grid-column:span 6}.c4{grid-column:span 4}.c8{grid-column:span 8}.c12{grid-column:span 12}
@media(max-width:900px){.c6,.c4,.c8{grid-column:span 12}}
canvas{max-width:100%}
.wc{width:100%;height:300px;display:block}
.tabs{display:flex;gap:6px;margin-bottom:10px;flex-wrap:wrap}
.tab{padding:5px 11px;border-radius:20px;background:#11141a;border:1px solid var(--line);cursor:pointer;font-size:12px;color:var(--dim)}
.tab.on{background:var(--acc);color:#0b0d12;border-color:var(--acc);font-weight:700}
.filterbar{margin:0 0 14px;color:var(--dim);font-size:13px;min-height:20px}
.filterbar b{color:var(--ink)}
.filterbar .clr{color:var(--acc);cursor:pointer;margin-left:8px}
table{width:100%;border-collapse:collapse;font-size:12.5px}
th,td{text-align:left;padding:7px 9px;border-bottom:1px solid var(--line);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:280px}
th{color:var(--dim);cursor:pointer;user-select:none;position:sticky;top:0;background:var(--card)}
th:hover{color:var(--ink)}
tr:hover td{background:#1d222c}
.tablewrap{max-height:460px;overflow:auto}
.chip{display:inline-block;background:#11141a;border:1px solid var(--line);border-radius:10px;padding:1px 7px;font-size:11px;color:var(--dim);margin:1px}
.pill{cursor:pointer}
.pill:hover{color:var(--acc)}
.controls{display:flex;gap:10px;align-items:center;margin-bottom:10px;flex-wrap:wrap}
.controls input[type=range]{width:130px}
.controls select,.controls input[type=search]{background:#11141a;border:1px solid var(--line);color:var(--ink);border-radius:8px;padding:6px 9px;font-size:13px}
.mut{color:var(--dim)}
.livedot{width:10px;height:10px;border-radius:50%;background:var(--bad);box-shadow:0 0 0 0 rgba(248,113,113,.7);animation:pulse 1.8s infinite}
@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(248,113,113,.6)}70%{box-shadow:0 0 0 9px rgba(248,113,113,0)}100%{box-shadow:0 0 0 0 rgba(248,113,113,0)}}
.feed{display:flex;flex-direction:column;gap:8px;max-height:340px;overflow:auto}
.fitem{display:flex;gap:12px;align-items:flex-start;padding:9px 11px;background:#11141a;border:1px solid var(--line);border-radius:9px}
.fitem .ago{font-size:11px;color:var(--dim);white-space:nowrap;min-width:74px;padding-top:2px}
.fitem .who{font-weight:600}
.fitem .loc{color:var(--dim);font-size:12px}
.fitem .msg{font-size:12.5px;color:var(--ink);margin-top:3px}
.fitem .brows{font-size:11px;color:var(--dim);margin-top:2px}
.fitem.miss{border-color:#5a3a1a;background:#1a1408}
.badge{font-size:10px;padding:1px 6px;border-radius:9px;border:1px solid var(--line);color:var(--dim)}
.badge.m{background:#3a2a08;border-color:#7a5a1a;color:#fbbf24}
.fresh{border-color:#1a5a3a!important;background:#0c1a12!important}
.hm{display:grid;grid-template-columns:34px repeat(24,1fr);gap:2px;align-items:center}
.hm .lbl{font-size:9px;color:var(--dim);text-align:right;padding-right:4px}
.hm .hh{font-size:8px;color:var(--dim);text-align:center}
.hm .cell{aspect-ratio:1;border-radius:2px;background:#11141a;min-height:12px}
.hm .cell[title]{cursor:default}
.colmenu{position:relative;display:inline-block}
.colpop{position:absolute;z-index:20;top:110%;left:0;background:#11141a;border:1px solid var(--line);border-radius:8px;padding:8px;min-width:190px;max-height:300px;overflow:auto;display:none;box-shadow:0 8px 24px rgba(0,0,0,.5)}
.colpop.on{display:block}
.colpop label{display:flex;align-items:center;gap:7px;padding:4px 6px;font-size:12.5px;cursor:pointer;border-radius:6px}
.colpop label:hover{background:#1d222c}
th.drag{cursor:grab}
th.dragging{opacity:.4}
th.dragover{border-left:2px solid var(--acc)}
th .grip{color:var(--dim);margin-right:3px;font-size:10px}
a{color:var(--acc);text-decoration:none}a:hover{text-decoration:underline}
</style>
</head>
<body>
<header>
<h1>💬 DW Chat History Analyzer</h1>
<span class="meta" id="meta">loading…</span>
<span class="sp"></span>
<a class="golive" href="https://designerwallcoverings.zendesk.com/chat/agent" target="_blank" rel="noopener" title="Open the Zendesk agent console — live visitor monitor + real-time chat">💬 Go Live in Zendesk</a>
<button id="refresh">↻ Refresh from Zendesk</button>
</header>
<div class="wrap">
<div class="card c12" id="liveCard" style="margin-bottom:20px">
<h3 style="display:flex;align-items:center;gap:10px">
<span class="livedot"></span> Live — recent chat activity
<select id="liveWindow" style="background:#11141a;border:1px solid var(--line);color:var(--ink);border-radius:6px;padding:3px 7px;font-size:12px">
<option value="6">last 6h</option><option value="24" selected>last 24h</option><option value="72">last 3d</option>
</select>
<span class="sp" style="flex:1"></span>
<span class="mut" id="liveMeta" style="font-size:11px;font-weight:400"></span>
</h3>
<div id="liveFeed"></div>
</div>
<div class="filterbar" id="filterbar"></div>
<div class="kpis" id="kpis"></div>
<div class="grid">
<div class="card c8">
<h3>Chats by country — click a bar to filter</h3>
<canvas id="ctryChart" height="120"></canvas>
</div>
<div class="card c4">
<h3>Chat vs offline message</h3>
<canvas id="typeChart" height="150"></canvas>
</div>
<div class="card c12">
<h3>Word clouds</h3>
<div class="tabs" id="wcTabs"></div>
<canvas id="wc" class="wc"></canvas>
<div class="mut" id="wcNote" style="font-size:12px"></div>
</div>
<div class="card c6">
<h3>Themes — what customers chat about (click to filter)</h3>
<canvas id="themeChart" height="150"></canvas>
</div>
<div class="card c6">
<h3>Top brands / patterns asked about (click to filter)</h3>
<canvas id="brandChart" height="150"></canvas>
</div>
<div class="card c6">
<h3>When chats happen — hour × weekday</h3>
<div id="heatmap"></div>
<div class="mut" id="heatNote" style="font-size:11px;margin-top:6px"></div>
</div>
<div class="card c6">
<h3>Topic mind-map — themes ↔ brands</h3>
<div id="mindmap" style="width:100%;height:320px;background:#11141a;border-radius:8px"></div>
</div>
<div class="card c4"><h3>Platform</h3><canvas id="platChart" height="180"></canvas></div>
<div class="card c4"><h3>Top agents</h3><canvas id="agentChart" height="180"></canvas></div>
<div class="card c4"><h3>Chats over time</h3><canvas id="timeChart" height="180"></canvas></div>
<div class="card c12">
<h3>Chats</h3>
<div class="controls">
<input type="search" id="search" placeholder="search all fields (space = AND)…">
<label class="mut">density <input type="range" id="density" min="4" max="14" value="8"></label>
<div class="colmenu">
<button id="colBtn">▦ Columns</button>
<div class="colpop" id="colPop"></div>
</div>
<button id="resetCols">↺ Reset</button>
<button id="exportCsv">⭳ Export CSV</button>
<span class="mut" id="tableCount"></span>
</div>
<div class="mut" style="font-size:11px;margin:-4px 0 8px">Tip: click a header to sort · <b>drag a header to reorder</b> · use ▦ Columns to show/hide · choices are saved.</div>
<div class="tablewrap"><table id="tbl"><thead></thead><tbody></tbody></table></div>
</div>
</div>
</div>
<script src="app.js"></script>
</body>
</html>