← 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 →