[object Object]

← back to Zendesk Chat Analyzer

Add rule-based sentiment + urgency scoring (/bin/zsh local): grid columns + one-click 'unhappy'/'urgent' filters for priority follow-up

d73376a4051fd314c4087146b3953f93371784b9 · 2026-08-11 10:07:41 -0700 · Steve

Files touched

Diff

commit d73376a4051fd314c4087146b3953f93371784b9
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue Aug 11 10:07:41 2026 -0700

    Add rule-based sentiment + urgency scoring (/bin/zsh local): grid columns + one-click 'unhappy'/'urgent' filters for priority follow-up
---
 public/app.js | 15 +++++++++++----
 pull.py       | 23 +++++++++++++++++++++++
 2 files changed, 34 insertions(+), 4 deletions(-)

diff --git a/public/app.js b/public/app.js
index c6b8b70..5138d93 100644
--- a/public/app.js
+++ b/public/app.js
@@ -1,5 +1,5 @@
 /* DW Chat History Analyzer — client. Loads aggregated data.json (no secrets) and renders. */
-let DATA = {chats: []}, FILTER = {country: null, missed: false, theme: null, brand: null}, WCMODE = 'titles', charts = {}, sortKey = 'ts', sortDir = -1, network = null;
+let DATA = {chats: []}, FILTER = {country: null, missed: false, theme: null, brand: null, sentiment: null, urgency: null}, WCMODE = 'titles', charts = {}, sortKey = 'ts', sortDir = -1, network = null;
 
 const STOP = new Set(("the a an and or of to in for on with is are be it this that you your we our i "+
 "at as by from he she they them his her hi hello hey thanks thank please great day designer wallcoverings "+
@@ -12,7 +12,9 @@ const rows = () => DATA.chats.filter(c =>
   (!FILTER.country || c.country===FILTER.country) &&
   (!FILTER.missed || c.missed) &&
   (!FILTER.theme || (c.themes||[]).includes(FILTER.theme)) &&
-  (!FILTER.brand || (c.brands||[]).includes(FILTER.brand)));
+  (!FILTER.brand || (c.brands||[]).includes(FILTER.brand)) &&
+  (!FILTER.sentiment || c.sentiment===FILTER.sentiment) &&
+  (!FILTER.urgency || c.urgency===FILTER.urgency));
 
 function load(){
   fetch('data.json?_='+Date.now()).then(r=>r.json()).then(d=>{ DATA=d; boot(); })
@@ -33,11 +35,14 @@ function renderFilter(){
   if(FILTER.missed) parts.push(`<b>missed / unanswered</b> chats only 📞`);
   if(FILTER.theme) parts.push(`theme = <b>${FILTER.theme}</b>`);
   if(FILTER.brand) parts.push(`brand = <b>${FILTER.brand}</b>`);
+  if(FILTER.sentiment) parts.push(`sentiment = <b>${FILTER.sentiment}</b>`);
+  if(FILTER.urgency) parts.push(`urgency = <b>${FILTER.urgency}</b>`);
+  const neg=DATA.chats.filter(c=>c.sentiment==='negative').length, urg=DATA.chats.filter(c=>c.urgency==='high').length, mis=DATA.chats.filter(c=>c.missed).length;
   fb.innerHTML = parts.length
     ? `Filtered to ${parts.join(' + ')} — <b>${rows().length}</b> chats <span class="clr" onclick="clearF()">✕ clear</span>`
-    : `Showing all <b>${DATA.chats.length}</b> chats · <span class="clr" onclick="showMissed()">📞 show ${DATA.chats.filter(c=>c.missed).length} missed chats</span>`;
+    : `Showing all <b>${DATA.chats.length}</b> chats · <span class="clr" onclick="showMissed()">📞 ${mis} missed</span> · <span class="clr" onclick="setF('sentiment','negative')">😞 ${neg} unhappy</span> · <span class="clr" onclick="setF('urgency','high')">⏰ ${urg} urgent</span>`;
 }
-window.clearF=()=>{ FILTER={country:null,missed:false,theme:null,brand:null}; renderAll(); };
+window.clearF=()=>{ FILTER={country:null,missed:false,theme:null,brand:null,sentiment:null,urgency:null}; renderAll(); };
 window.showMissed=()=>{ FILTER.missed=true; sortKey='ts'; sortDir=-1; renderAll(); document.querySelector('#tbl').scrollIntoView({behavior:'smooth'}); };
 
 function kpis(){
@@ -180,6 +185,8 @@ const COLDEFS=[
   {k:'duration',l:'Duration',g:c=>dur(c.duration),p:c=>+c.duration||0},
   {k:'agents',l:'Agent',g:c=>(c.agents||[]).join(', '),p:c=>(c.agents||[]).join(' ')},
   {k:'rating',l:'Rating',g:c=>c.rating==='good'?'👍':c.rating==='bad'?'👎':'',p:c=>c.rating},
+  {k:'sentiment',l:'Sentiment',g:c=>c.sentiment==='negative'?'😞 neg':c.sentiment==='positive'?'😊 pos':c.sentiment?'😐':'',p:c=>c.sentiment||''},
+  {k:'urgency',l:'Urgency',g:c=>c.urgency==='high'?'⏰ HIGH':'',p:c=>c.urgency||''},
   {k:'themes',l:'Themes',g:c=>chips(c.themes),p:c=>(c.themes||[]).join(' ')},
   {k:'brands',l:'Brands',g:c=>chips(c.brands),p:c=>(c.brands||[]).join(' ')},
   {k:'titles',l:'Browsed',g:c=>(c.titles||[])[0]||'',p:c=>(c.titles||[]).join(' ')},
diff --git a/pull.py b/pull.py
index 5be7703..058eb4e 100644
--- a/pull.py
+++ b/pull.py
@@ -63,6 +63,27 @@ def find_brands(text):
     return sorted({b for b in BRANDS if b.lower() in t})
 
 
+NEG = r"wrong|damag|defect|broken|torn|missing|refund|return|cancel|terrible|awful|horrible|disappoint|frustrat|angry|upset|unhappy|not happy|problem|issue|never (received|got|arrived)|still waiting|ridiculous|unacceptable|complain|poor|bad (lot|quality|experience)|worst|rude|ignored|no (one|response|reply|answer)"
+POS = r"love|great|beautiful|perfect|thank you|thanks|excellent|wonderful|happy|gorgeous|amazing|fantastic|pleased|appreciate|awesome|excited|can.?t wait|lovely|stunning"
+URG = r"\basap\b|urgent|immediately|right away|deadline|need (it |them )?by|running out|time.?sensitive|today|tomorrow|rush|as soon as|quickly|expedite|in a hurry|last minute"
+
+
+def sentiment(text):
+    t = (text or "").lower()
+    if not t.strip():
+        return ""
+    neg = len(re.findall(NEG, t)); pos = len(re.findall(POS, t))
+    if neg > pos:
+        return "negative"
+    if pos > neg:
+        return "positive"
+    return "neutral"
+
+
+def urgency(text):
+    return "high" if re.search(URG, (text or "").lower()) else "normal"
+
+
 def main():
     tok = token()
     url = BASE + "?" + urllib.parse.urlencode({"fields": "chats(*)", "start_time": START})
@@ -87,6 +108,8 @@ def main():
             rows.append({
                 "themes": themes,
                 "brands": brands,
+                "sentiment": sentiment(vtext),
+                "urgency": urgency(vtext),
                 "id": c.get("id"),
                 "ts": c.get("timestamp"),
                 "type": c.get("type"),

← 590979c Add CSV export of current filtered/arranged grid view (clien  ·  back to Zendesk Chat Analyzer  ·  gitignore recoverable-leads.md (customer PII) 9fc32c4 →