Site icon Filestack Blog

Prioritize Support Tickets with Text Sentiment (and a Clean UI)

robot detecting mood

Goal: Use the Filestack CDN text_sentiment transform to score ticket text and convert those scores into a clear priority (P0–P4). The guide builds the UI block-by-block so each part is understandable and testable. A complete, ready-to-run index.html is provided at the end.

What we’ll build

Security note: In production, sign policies server-side and avoid exposing secrets in the browser. This demo keeps everything in one file for clarity and fast iteration.

API URL: exact shape

Keep the structure below—matching the known-good format:

https:<span class="hljs-regexp">//</span>cdn.filestackcontent.com/<APIKEY><span class="hljs-regexp">/security=p:<POLICY>,s:<SIGNATURE>/</span>text_sentiment=text:%22<URL-ENCODED-TEXT>%22

Priority rules (editable)

Use simple defaults and tweak them in UI inputs:

These thresholds balance straightforward triage with enough nuance to catch frustration (Negative) and uncertainty (Mixed).

1) HTML skeleton (structure only)

Define where credentials, text input, thresholds, and results live—no behavior yet.

<!-- 1_skeleton.html (snippet) -->
<body>
  <div class="app">
    <header class="header">
      <div>
        <div class="title">Text Sentiment → Ticket Priority</div>
        <div class="subtitle">Paste credentials, enter ticket text, get scores + priority.</div>
      </div>
      <button id="analyzeBtn" class="btn">Analyze</button>
    </header>

    <section class="panel left">
      <div class="field"><label>API Key</label><input id="apiKey" type="text" placeholder="YOUR_API_KEY" /></div>
      <div class="field"><label>Policy</label><input id="policy" type="text" placeholder="YOUR_BASE64_POLICY" /></div>
      <div class="field"><label>Signature</label><input id="signature" type="password" placeholder="YOUR_SIGNATURE" /></div>

      <div class="field">
        <label>Ticket text</label>
        <textarea id="ticketText" placeholder="Paste the customer/agent message here…"></textarea>
        <div class="chips">
          <span class="chip" data-demo="This product is outstanding. Setup was painless and support was fast.">Demo: very positive</span>
          <span class="chip" data-demo="The upload keeps failing, your docs are wrong, and I'm stuck before a deadline.">Demo: negative</span>
          <span class="chip" data-demo="It works, but the new editor toolbar is confusing and slows my team down.">Demo: mixed</span>
        </div>
      </div>

      <div class="grid2 thresholds">
        <div class="panel compact">
          <div class="field"><label>Negative ≥ X ⇒ P0 (Critical)</label><input id="tNegP0" type="number" step="0.01" min="0" max="1" value="0.70" /></div>
          <div class="field"><label>Negative ≥ X ⇒ P1 (High)</label><input id="tNegP1" type="number" step="0.01" min="0" max="1" value="0.40" /></div>
        </div>
        <div class="panel compact">
          <div class="field"><label>Mixed ≥ X ⇒ P2 (Medium)</label><input id="tMixed" type="number" step="0.01" min="0" max="1" value="0.50" /></div>
          <div class="field"><label>Neutral ≥ X ⇒ P3 (Low)</label><input id="tNeutral" type="number" step="0.01" min="0" max="1" value="0.60" /></div>
        </div>
      </div>

      <div class="row">
        <button class="btn ghost" id="copyUrlBtn">Copy request URL</button>
        <button class="btn ghost" id="clearBtn">Clear</button>
      </div>
    </section>

    <section class="panel right">
      <div class="field">
        <label>Computed priority</label>
        <div class="kpi">
          <div class="pill p4" id="priorityPill">P4 • Very Low</div>
          <div id="dominantLabel" class="small">Dominant: —</div>
        </div>
      </div>

      <div class="bars" id="bars"></div>

      <div class="field">
        <label>Raw JSON</label>
        <pre id="rawJson">{ }</pre>
      </div>
    </section>

    <footer class="footer small">
      Tip: In production, sign on the server and proxy requests—don’t leak secrets in the browser.
    </footer>
  </div>
</body>

2) Light-mode styles (accessible, framework-free)

Readable defaults, subtle shadows, and components that don’t fight your content.

<!-- 2_styles.css (snippet in <style>) -->
<style>
  :root{
    --bg:#f7f9fc; --text:#0f172a; --muted:#475569; --border:#e2e8f0; --card:#ffffff;
    --accent:#2563eb;
    --p0:#ef4444; --p1:#f59e0b; --p2:#2563eb; --p3:#22c55e; --p4:#7c3aed;
  }
  *{box-sizing:border-box}
  body{
    margin:0; font-family: ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Inter,Arial;
    background: radial-gradient(1200px 600px at 10% -10%, #e8eefc 0%, transparent 60%),
                radial-gradient(1000px 500px at 100% 0%, #eaf2ff 0%, transparent 60%), var(--bg);
    color:var(--text); min-height:100vh; display:flex; align-items:stretch; justify-content:center; padding:32px;
  }
  .app{max-width:1200px; width:100%; display:grid; grid-template-columns:360px 1fr; gap:24px}
  .header{grid-column:1/-1; display:flex; align-items:center; justify-content:space-between; padding:6px 0}
  .title{font-size:20px; font-weight:700}
  .subtitle{color:var(--muted); font-size:13px}
  .panel{background:var(--card); border:1px solid var(--border); border-radius:14px; padding:16px; box-shadow:0 8px 24px rgba(15,23,42,.05)}
  .panel.compact{padding:12px}
  .field{margin:12px 0}
  label{display:block; font-size:12px; color:var(--muted); margin-bottom:6px}
  input[type="text"], input[type="password"], textarea, input[type="number"]{
    width:100%; background:#fff; color:var(--text); border:1px solid var(--border); border-radius:10px; padding:10px 12px; outline:none;
  }
  input:focus, textarea:focus{border-color:#c7d2fe; box-shadow:0 0 0 3px #e0e7ff}
  textarea{min-height:140px; resize:vertical}
  .btn{
    display:inline-flex; align-items:center; gap:8px; border:1px solid #c7d2fe; color:#fff;
    background: linear-gradient(180deg,#60a5fa,#2563eb); padding:10px 14px; border-radius:10px; cursor:pointer; font-weight:600;
    box-shadow:0 6px 18px rgba(37,99,235,.25)
  }
  .btn.ghost{background:#fff; color:var(--muted); border:1px solid var(--border); box-shadow:none}
  .chips{display:flex; gap:8px; flex-wrap:wrap; margin-top:8px}
  .chip{background:#f8fafc; border:1px dashed var(--border); color:var(--muted); padding:6px 10px; border-radius:30px; font-size:12px; cursor:pointer}
  .grid2{display:grid; grid-template-columns:1fr 1fr; gap:12px}
  .row{display:flex; gap:10px; flex-wrap:wrap}
  .kpi{display:flex; align-items:center; gap:10px; background:#f8fafc; border:1px solid var(--border); border-radius:12px; padding:10px 12px}
  .pill{padding:4px 10px; border-radius:999px; font-weight:700; font-size:12px; border:1px solid transparent}
  .p0{background:#fee2e2; color:#991b1b; border-color:#fecaca}
  .p1{background:#fef3c7; color:#92400e; border-color:#fde68a}
  .p2{background:#dbeafe; color:#1e3a8a; border-color:#bfdbfe}
  .p3{background:#dcfce7; color:#166534; border-color:#bbf7d0}
  .p4{background:#ede9fe; color:#5b21b6; border-color:#ddd6fe}
  .bars{display:grid; gap:10px; margin-top:8px}
  .bar{display:grid; grid-template-columns: 120px 1fr 60px; align-items:center; gap:10px}
  .track{height:10px; background:#f1f5f9; border:1px solid var(--border); border-radius:999px; overflow:hidden}
  .fill{height:100%; background:linear-gradient(90deg,#60a5fa,#2563eb)}
  pre{white-space:pre-wrap; word-break:break-word; background:#f8fafc; border:1px solid var(--border); border-radius:12px; padding:12px; font-size:12px; color:#0f172a}
  .small{font-size:12px; color:var(--muted)}
  .footer{grid-column:1/-1; color:var(--muted); font-size:12px; margin-top:6px}
  @media (max-width:980px){ .app{grid-template-columns:1fr} }
</style>

3) URL builder (one job: construct the request)

This matches the proven, working shape—including %22 around the text.

<script>
  function buildSentimentUrl({ apiKey, policy, signature, text }) {
    if (!apiKey || !policy || !signature) throw new Error('Missing apikey/policy/signature.');
    const enc = encodeURIComponent(text || '');
    // Exact shape you confirmed works:
    return `https://cdn.filestackcontent.com/${apiKey}/security=p:${policy},s:${signature}/text_sentiment=text:%22${enc}%22`;
  }
</script>

4) Priority engine (pure functions)

Decoupled from the DOM so it’s easy to test.

<script>
  function computePriority(emotions, thresholds) {
    const neg = Number(emotions?.Negative || 0);
    const mix = Number(emotions?.Mixed   || 0);
    const neu = Number(emotions?.Neutral || 0);

    if (neg >= thresholds.negP0) return ['P0','Critical'];
    if (neg >= thresholds.negP1) return ['P1','High'];
    if (mix >= thresholds.mixed) return ['P2','Medium'];
    if (neu >= thresholds.neutral) return ['P3','Low'];
    return ['P4','Very Low'];
  }

  function dominantEmotion(emotions) {
    let maxK = '—', maxV = 0;
    for (const [k,v] of Object.entries(emotions || {})) {
      const n = Number(v||0);
      if (n > maxV) { maxV = n; maxK = k; }
    }
    return { key:maxK, val:maxV };
  }
</script>

5) UI bindings (wire up inputs, fetch, and render)

Keep DOM work separate from logic for clarity.

<script>
  const els = {
    apiKey: document.getElementById('apiKey'),
    policy: document.getElementById('policy'),
    signature: document.getElementById('signature'),
    ticketText: document.getElementById('ticketText'),
    tNegP0: document.getElementById('tNegP0'),
    tNegP1: document.getElementById('tNegP1'),
    tMixed: document.getElementById('tMixed'),
    tNeutral: document.getElementById('tNeutral'),
    analyzeBtn: document.getElementById('analyzeBtn'),
    copyUrlBtn: document.getElementById('copyUrlBtn'),
    clearBtn: document.getElementById('clearBtn'),
    priorityPill: document.getElementById('priorityPill'),
    dominantLabel: document.getElementById('dominantLabel'),
    bars: document.getElementById('bars'),
    rawJson: document.getElementById('rawJson'),
  };

  document.querySelectorAll('.chip').forEach(ch => ch.addEventListener('click', () => {
    els.ticketText.value = ch.dataset.demo || '';
  }));

  const pct = n => (Number(n||0)*100).toFixed(2) + '%';
  function setPriorityUI(code,label){
    els.priorityPill.className = `pill ${code.toLowerCase()}`;
    els.priorityPill.textContent = `${code} • ${label}`;
  }
  function renderBars(emotions={}){
    const names = ['Negative','Mixed','Neutral','Positive'];
    els.bars.innerHTML = names.map(name=>{
      const v = Number(emotions[name] || 0);
      return `
        <div class="bar">
          <label>${name}</label>
          <div class="track"><div class="fill" style="width:${Math.max(0,Math.min(100,v*100))}%"></div></div>
          <div class="small">${pct(v)}</div>
        </div>
      `;
    }).join('');
  }

  let lastBuiltUrl = '';

  async function analyze(){
    const text = (els.ticketText.value || '').trim();
    if (!text) { alert('Enter ticket text first.'); return; }

    els.analyzeBtn.disabled = true;
    const original = els.analyzeBtn.textContent;
    els.analyzeBtn.textContent = 'Analyzing…';

    try{
      const url = buildSentimentUrl({
        apiKey: (els.apiKey.value||'').trim(),
        policy: (els.policy.value||'').trim(),
        signature: (els.signature.value||'').trim(),
        text
      });
      lastBuiltUrl = url;

      const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
      if (!res.ok) {
        const errText = await res.text().catch(()=> '');
        throw new Error(`HTTP ${res.status} – ${res.statusText}\n${errText}`);
      }
      const data = await res.json();
      const emotions = data?.emotions || {};
      const [code,label] = computePriority(emotions, {
        negP0: Number(els.tNegP0.value||0.70),
        negP1: Number(els.tNegP1.value||0.40),
        mixed: Number(els.tMixed.value ||0.50),
        neutral:Number(els.tNeutral.value||0.60),
      });
      const dom = dominantEmotion(emotions);

      renderBars(emotions);
      setPriorityUI(code,label);
      els.dominantLabel.textContent = `Dominant: ${dom.key} (${pct(dom.val)})`;
      els.rawJson.textContent = JSON.stringify(data, null, 2);
    } catch (err){
      els.rawJson.textContent = String(err?.message || err);
      renderBars({});
      setPriorityUI('P4','Very Low');
      els.dominantLabel.textContent = 'Dominant: —';
    } finally {
      els.analyzeBtn.disabled = false;
      els.analyzeBtn.textContent = original;
    }
  }

  els.analyzeBtn.addEventListener('click', analyze);
  els.clearBtn.addEventListener('click', ()=>{
    els.ticketText.value = '';
    els.rawJson.textContent = '{ }';
    renderBars({});
    setPriorityUI('P4','Very Low');
    els.dominantLabel.textContent = 'Dominant: —';
  });
  els.copyUrlBtn.addEventListener('click', ()=>{
    if (!lastBuiltUrl) {
      try { lastBuiltUrl = buildSentimentUrl({
        apiKey: (els.apiKey.value||'').trim(),
        policy: (els.policy.value||'').trim(),
        signature: (els.signature.value||'').trim(),
        text: els.ticketText.value || 'Hello world'
      }); } catch { /* ignore */ }
    }
    if (!lastBuiltUrl) return alert('Run once to build the URL, or fill credentials.');
    navigator.clipboard.writeText(lastBuiltUrl).then(()=>{
      els.copyUrlBtn.textContent = 'Copied ✓';
      setTimeout(()=> els.copyUrlBtn.textContent = 'Copy request URL', 900);
    });
  });
</script>

Full demo (single-file index.html)

Paste your API key / policy / signature, enter ticket text, and click Analyze. The UI shows the JSON and the computed priority. You can also copy the exact CDN request URL the page used.

This is identical to the blocks above, consolidated for quick use.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Text Sentiment → Ticket Priority (Light Mode)</title>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <style>
    :root{
      --bg:#f7f9fc; --text:#0f172a; --muted:#475569; --border:#e2e8f0; --card:#ffffff;
      --accent:#2563eb;
      --p0:#ef4444; --p1:#f59e0b; --p2:#2563eb; --p3:#22c55e; --p4:#7c3aed;
    }
    *{box-sizing:border-box}
    body{
      margin:0; font-family: ui-sans-serif,system-ui,-apple-system,Segoe UI,Roboto,Inter,Arial;
      background: radial-gradient(1200px 600px at 10% -10%, #e8eefc 0%, transparent 60%),
                  radial-gradient(1000px 500px at 100% 0%, #eaf2ff 0%, transparent 60%), var(--bg);
      color:var(--text); min-height:100vh; display:flex; align-items:stretch; justify-content:center; padding:32px;
    }
    .app{max-width:1200px; width:100%; display:grid; grid-template-columns:360px 1fr; gap:24px}
    .header{grid-column:1/-1; display:flex; align-items:center; justify-content:space-between; padding:6px 0}
    .title{font-size:20px; font-weight:700}
    .subtitle{color:var(--muted); font-size:13px}
    .panel{background:var(--card); border:1px solid var(--border); border-radius:14px; padding:16px; box-shadow:0 8px 24px rgba(15,23,42,.05)}
    .panel.compact{padding:12px}
    .field{margin:12px 0}
    label{display:block; font-size:12px; color:var(--muted); margin-bottom:6px}
    input[type="text"], input[type="password"], textarea, input[type="number"]{
      width:100%; background:#fff; color:var(--text); border:1px solid var(--border); border-radius:10px; padding:10px 12px; outline:none;
    }
    input:focus, textarea:focus{border-color:#c7d2fe; box-shadow:0 0 0 3px #e0e7ff}
    textarea{min-height:140px; resize:vertical}
    .btn{
      display:inline-flex; align-items:center; gap:8px; border:1px solid #c7d2fe; color:#fff;
      background: linear-gradient(180deg,#60a5fa,#2563eb); padding:10px 14px; border-radius:10px; cursor:pointer; font-weight:600;
      box-shadow:0 6px 18px rgba(37,99,235,.25)
    }
    .btn.ghost{background:#fff; color:var(--muted); border:1px solid var(--border); box-shadow:none}
    .chips{display:flex; gap:8px; flex-wrap:wrap; margin-top:8px}
    .chip{background:#f8fafc; border:1px dashed var(--border); color:var(--muted); padding:6px 10px; border-radius:30px; font-size:12px; cursor:pointer}
    .grid2{display:grid; grid-template-columns:1fr 1fr; gap:12px}
    .row{display:flex; gap:10px; flex-wrap:wrap}
    .kpi{display:flex; align-items:center; gap:10px; background:#f8fafc; border:1px solid var(--border); border-radius:12px; padding:10px 12px}
    .pill{padding:4px 10px; border-radius:999px; font-weight:700; font-size:12px; border:1px solid transparent}
    .p0{background:#fee2e2; color:#991b1b; border-color:#fecaca}
    .p1{background:#fef3c7; color:#92400e; border-color:#fde68a}
    .p2{background:#dbeafe; color:#1e3a8a; border-color:#bfdbfe}
    .p3{background:#dcfce7; color:#166534; border-color:#bbf7d0}
    .p4{background:#ede9fe; color:#5b21b6; border-color:#ddd6fe}
    .bars{display:grid; gap:10px; margin-top:8px}
    .bar{display:grid; grid-template-columns: 120px 1fr 60px; align-items:center; gap:10px}
    .track{height:10px; background:#f1f5f9; border:1px solid var(--border); border-radius:999px; overflow:hidden}
    .fill{height:100%; background:linear-gradient(90deg,#60a5fa,#2563eb)}
    pre{white-space:pre-wrap; word-break:break-word; background:#f8fafc; border:1px solid var(--border); border-radius:12px; padding:12px; font-size:12px; color:#0f172a}
    .small{font-size:12px; color:var(--muted)}
    .footer{grid-column:1/-1; color:var(--muted); font-size:12px; margin-top:6px}
    @media (max-width:980px){ .app{grid-template-columns:1fr} }
  </style>
</head>
<body>
  <div class="app">
    <header class="header">
      <div>
        <div class="title">Text Sentiment → Ticket Priority</div>
        <div class="subtitle">Paste credentials, enter ticket text, get scores + priority.</div>
      </div>
      <button id="analyzeBtn" class="btn">Analyze</button>
    </header>

    <section class="panel left">
      <div class="field"><label>API Key</label><input id="apiKey" type="text" placeholder="YOUR_API_KEY" /></div>
      <div class="field"><label>Policy</label><input id="policy" type="text" placeholder="YOUR_BASE64_POLICY" /></div>
      <div class="field"><label>Signature</label><input id="signature" type="password" placeholder="YOUR_SIGNATURE" /></div>

      <div class="field">
        <label>Ticket text</label>
        <textarea id="ticketText" placeholder="Paste the customer/agent message here…"></textarea>
        <div class="chips">
          <span class="chip" data-demo="This product is outstanding. Setup was painless and support was fast.">Demo: very positive</span>
          <span class="chip" data-demo="The upload keeps failing, your docs are wrong, and I'm stuck before a deadline.">Demo: negative</span>
          <span class="chip" data-demo="It works, but the new editor toolbar is confusing and slows my team down.">Demo: mixed</span>
        </div>
      </div>

      <div class="grid2 thresholds">
        <div class="panel compact">
          <div class="field"><label>Negative ≥ X ⇒ P0 (Critical)</label><input id="tNegP0" type="number" step="0.01" min="0" max="1" value="0.70" /></div>
          <div class="field"><label>Negative ≥ X ⇒ P1 (High)</label><input id="tNegP1" type="number" step="0.01" min="0" max="1" value="0.40" /></div>
        </div>
        <div class="panel compact">
          <div class="field"><label>Mixed ≥ X ⇒ P2 (Medium)</label><input id="tMixed" type="number" step="0.01" min="0" max="1" value="0.50" /></div>
          <div class="field"><label>Neutral ≥ X ⇒ P3 (Low)</label><input id="tNeutral" type="number" step="0.01" min="0" max="1" value="0.60" /></div>
        </div>
      </div>

      <div class="row">
        <button class="btn ghost" id="copyUrlBtn">Copy request URL</button>
        <button class="btn ghost" id="clearBtn">Clear</button>
      </div>
    </section>

    <section class="panel right">
      <div class="field">
        <label>Computed priority</label>
        <div class="kpi">
          <div class="pill p4" id="priorityPill">P4 • Very Low</div>
          <div id="dominantLabel" class="small">Dominant: —</div>
        </div>
      </div>

      <div class="bars" id="bars"></div>

      <div class="field">
        <label>Raw JSON</label>
        <pre id="rawJson">{ }</pre>
      </div>
    </section>

    <footer class="footer small">
      Tip: In production, sign on the server and proxy requests—don’t leak secrets in the browser.
    </footer>
  </div>

  <script>
    function buildSentimentUrl({ apiKey, policy, signature, text }) {
      if (!apiKey || !policy || !signature) throw new Error('Missing apikey/policy/signature.');
      const enc = encodeURIComponent(text || '');
      return `https://cdn.filestackcontent.com/${apiKey}/security=p:${policy},s:${signature}/text_sentiment=text:%22${enc}%22`;
    }
    function computePriority(emotions, thresholds) {
      const neg = Number(emotions?.Negative || 0);
      const mix = Number(emotions?.Mixed   || 0);
      const neu = Number(emotions?.Neutral || 0);
      if (neg >= thresholds.negP0) return ['P0','Critical'];
      if (neg >= thresholds.negP1) return ['P1','High'];
      if (mix >= thresholds.mixed) return ['P2','Medium'];
      if (neu >= thresholds.neutral) return ['P3','Low'];
      return ['P4','Very Low'];
    }
    function dominantEmotion(emotions) {
      let maxK = '—', maxV = 0;
      for (const [k,v] of Object.entries(emotions || {})) {
        const n = Number(v||0);
        if (n > maxV) { maxV = n; maxK = k; }
      }
      return { key:maxK, val:maxV };
    }

    const els = {
      apiKey: document.getElementById('apiKey'),
      policy: document.getElementById('policy'),
      signature: document.getElementById('signature'),
      ticketText: document.getElementById('ticketText'),
      tNegP0: document.getElementById('tNegP0'),
      tNegP1: document.getElementById('tNegP1'),
      tMixed: document.getElementById('tMixed'),
      tNeutral: document.getElementById('tNeutral'),
      analyzeBtn: document.getElementById('analyzeBtn'),
      copyUrlBtn: document.getElementById('copyUrlBtn'),
      clearBtn: document.getElementById('clearBtn'),
      priorityPill: document.getElementById('priorityPill'),
      dominantLabel: document.getElementById('dominantLabel'),
      bars: document.getElementById('bars'),
      rawJson: document.getElementById('rawJson'),
    };

    document.querySelectorAll('.chip').forEach(ch => ch.addEventListener('click', () => {
      els.ticketText.value = ch.dataset.demo || '';
    }));

    const pct = n => (Number(n||0)*100).toFixed(2) + '%';
    function setPriorityUI(code,label){
      els.priorityPill.className = `pill ${code.toLowerCase()}`;
      els.priorityPill.textContent = `${code} • ${label}`;
    }
    function renderBars(emotions={}){
      const names = ['Negative','Mixed','Neutral','Positive'];
      els.bars.innerHTML = names.map(name=>{
        const v = Number(emotions[name] || 0);
        return `
          <div class="bar">
            <label>${name}</label>
            <div class="track"><div class="fill" style="width:${Math.max(0,Math.min(100,v*100))}%"></div></div>
            <div class="small">${pct(v)}</div>
          </div>
        `;
      }).join('');
    }

    let lastBuiltUrl = '';

    async function analyze(){
      const text = (els.ticketText.value || '').trim();
      if (!text) { alert('Enter ticket text first.'); return; }

      els.analyzeBtn.disabled = true;
      const original = els.analyzeBtn.textContent;
      els.analyzeBtn.textContent = 'Analyzing…';

      try{
        const url = buildSentimentUrl({
          apiKey: (els.apiKey.value||'').trim(),
          policy: (els.policy.value||'').trim(),
          signature: (els.signature.value||'').trim(),
          text
        });
        lastBuiltUrl = url;

        const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
        if (!res.ok) {
          const errText = await res.text().catch(()=> '');
          throw new Error(`HTTP ${res.status} – ${res.statusText}\n${errText}`);
        }
        const data = await res.json();
        const emotions = data?.emotions || {};
        const [code,label] = computePriority(emotions, {
          negP0: Number(els.tNegP0.value||0.70),
          negP1: Number(els.tNegP1.value||0.40),
          mixed: Number(els.tMixed.value ||0.50),
          neutral:Number(els.tNeutral.value||0.60),
        });
        const dom = dominantEmotion(emotions);

        renderBars(emotions);
        setPriorityUI(code,label);
        els.dominantLabel.textContent = `Dominant: ${dom.key} (${pct(dom.val)})`;
        els.rawJson.textContent = JSON.stringify(data, null, 2);
      } catch (err){
        els.rawJson.textContent = String(err?.message || err);
        renderBars({});
        setPriorityUI('P4','Very Low');
        els.dominantLabel.textContent = 'Dominant: —';
      } finally {
        els.analyzeBtn.disabled = false;
        els.analyzeBtn.textContent = original;
      }
    }

    els.analyzeBtn.addEventListener('click', analyze);
    els.clearBtn.addEventListener('click', ()=>{
      els.ticketText.value = '';
      els.rawJson.textContent = '{ }';
      renderBars({});
      setPriorityUI('P4','Very Low');
      els.dominantLabel.textContent = 'Dominant: —';
    });
    els.copyUrlBtn.addEventListener('click', ()=>{
      if (!lastBuiltUrl) {
        try { lastBuiltUrl = buildSentimentUrl({
          apiKey: (els.apiKey.value||'').trim(),
          policy: (els.policy.value||'').trim(),
          signature: (els.signature.value||'').trim(),
          text: els.ticketText.value || 'Hello world'
        }); } catch { /* ignore */ }
      }
      if (!lastBuiltUrl) return alert('Run once to build the URL, or fill credentials.');
      navigator.clipboard.writeText(lastBuiltUrl).then(()=>{
        els.copyUrlBtn.textContent = 'Copied ✓';
        setTimeout(()=> els.copyUrlBtn.textContent = 'Copy request URL', 900);
      });
    });
  </script>
</body>
</html>

Production notes

Extensions (for later)

Exit mobile version