Skip to content

Instantly share code, notes, and snippets.

@mkwatson
Last active August 16, 2025 21:16
Show Gist options
  • Save mkwatson/242f7a39bc137f4ce45066aad56c148f to your computer and use it in GitHub Desktop.
Save mkwatson/242f7a39bc137f4ce45066aad56c148f to your computer and use it in GitHub Desktop.

Employee Engagement Pulse, minimal build plan

Takeaway: Ship a single SSR page that fetches last‑7‑days Slack messages for selected channels on demand, scores text and emoji (including reactions), aggregates to daily mood with simple burnout rules, and prints three actionable bullets, nothing more.


Project definition

  Project 3: Employee Engagement Pulse

  Description: Provide managers with a weekly sentiment dashboard built from all messages in configurable Slack channels.

  Requirements:
  • Monitor a user-defined list of Slack channels (include threads + reactions)
  • Run text & emoji sentiment analysis on every message
  • Aggregate daily mood into weekly trends with burnout warnings
  • Generate actionable, team-level insights for managers

Context and strategy

  • AI Fund Buildathon goal: most value for least code, finish fast for the tiebreaker.
  • Scope to essentials only: one page, one API, no database, no background jobs, no webhooks.
  • Configurable channels via query string or a tiny form.
  • Insights are rule‑based to avoid LLM integration overhead.

Minimal architecture

  • Next.js + TypeScript on Vercel, Node serverless, SSR on page load.
  • Slack Web API polling using a single token from env. Prefer a user token in the demo workspace, bot token as fallback.
  • Zero persistence. Compute in memory on each request.
  • Sentiment with sentiment for text plus node-emoji for shortcode to Unicode.
  • Reactions weighting using a small curated map with a hard cap per message.
  • UI: one page, one line chart, one insights panel, one input for channels, one refresh button.

Dependencies

  • next, react, typescript
  • @slack/web-api
  • sentiment
  • node-emoji
  • dayjs
  • react-chartjs-2, chart.js

Slack scopes and setup

  • Token in env: SLACK_TOKEN.
  • Scopes for a user token: channels:read, groups:read, channels:history, groups:history, reactions:read.
  • If using a bot token, invite the bot to each monitored channel.

Data flow, end to end

  1. User opens / with ?channels=C123,C234 or submits a simple form that reloads the page with this query.
  2. SSR calls /api/pulse on the server with the list of channel IDs.
  3. For each channel: fetch conversations.history for last 7 days, paginate if has_more.
  4. For each parent with reply_count > 0: fetch conversations.replies.
  5. For every message and reply: clean text, convert :shortcode: to emoji, score text with sentiment, add reaction weights, cap reaction impact.
  6. Bucket by UTC day, compute daily averages and week summary.
  7. Apply simple burnout rules, produce three actionable bullets.
  8. Return JSON to the page.
  9. Page renders a 7‑point line chart, a burnout badge if triggered, and the bullets.

Minimal data contracts

export interface SlackReaction { name: string; count: number }
export interface SlackMsg { ts: string; text?: string; reactions?: SlackReaction[]; reply_count?: number; thread_ts?: string; subtype?: string }

export interface MsgScore { ts: number; score: number; burnoutFlag: boolean }

export interface DayAgg {
  dateISO: string; avgScore: number; msgCount: number; negCount: number; burnoutCount: number
}

export interface WeekAgg {
  days: DayAgg[]; weeklyAvg: number; trendDelta: number; anyBurnout: boolean; reasons: string[]; insights: string[]
}

Minimal scoring rules

  • Text score: sentiment(cleanedText).score.

  • Emoji in text: convert top shortcodes with node-emoji. Unknown stay neutral.

  • Reaction weights:

    • Positive: thumbsup, +1, heart, smile, tada → +1, joy → +2.
    • Negative: thumbsdown, -1 → −1, cry, sob → −1, angry, rage → −2.
    • Cap absolute reaction impact at 3 per message.
  • Burnout keywords: ['burnout','burned out','overwhelmed','exhausted','drained','stressed','quitting','resign'].

  • Final message score = text score + capped reaction score.

  • Flags: if any burnout keyword in text, set burnoutFlag.


Minimal burnout warnings

Trigger if any of the following holds:

  • At least 2 days with avgScore < -0.30.
  • Week trendDelta ≤ −0.40 (Mon to latest day).
  • Total burnoutCount across the week ≥ 3.

Store brief reasons, for example: ["2 very low days","downward trend","3 burnout mentions"].


Minimal insights (no LLM)

Return exactly three short bullets, team‑level, data‑driven:

  • Lowest day with suggested action.
  • Trend note with a countermeasure.
  • Burnout warning with a manager check‑in suggestion.

Example template:

  • “Lowest mood on Wed (−0.42, 96 msgs). Ask for blockers in standup and trim scope.”
  • “Trend down Mon → Fri (−0.35). Rebalance workload and set clearer daily goals.”
  • “Burnout signals mentioned 4 times. Schedule 1:1s and enforce no‑meeting focus time.”

Minimal UI

  • Header with week dates and channel IDs.
  • Text input for channel IDs, comma‑separated, and a Submit that reloads the page.
  • Line chart of 7 points with zero baseline and tooltip for avg and count.
  • Burnout badge if anyBurnout.
  • Three insight bullets.
  • Loading spinner, one‑line error.

Minimal file map

/pages/index.tsx          // SSR page: reads channels from query, calls API, renders chart + insights
/pages/api/pulse.ts       // Server-only: Slack fetch, score, aggregate, insights, returns WeekAgg JSON

/lib/slack.ts             // list messages last 7 days, fetch replies
/lib/score.ts             // clean text, emojify, sentiment score, reactions weighting, burnout flags
/lib/aggregate.ts         // bucket by day, compute WeekAgg and reasons, build insight bullets

/lib/types.ts             // interfaces above

Milestones and checks

M0, scaffold

  • Create Next app with TS. Install deps.
  • Check: dev server runs, typecheck clean.

M1, one channel, no threads

  • /api/pulse: fetch conversations.history for last 7 days for a single channel from query.
  • Return raw count.
  • Check: JSON shows message count.

M2, add threads

  • For each parent with reply_count > 0, fetch conversations.replies.
  • Combine parents and replies.
  • Check: totals increase when threads present.

M3, sentiment for text

  • Add scoreText() using sentiment.
  • Clean Slack markup: strip mentions <@...>, channels <#...>, URLs <...>.
  • Check: unit test “love this” > 0, “terrible” < 0.

M4, emoji in text

  • Convert shortcodes using node-emoji.
  • Check: “great 😄” scores higher than plain “great”.

M5, reactions weighting

  • Sum curated reactions with cap 3.
  • Check: add 3 thumbs up moves score by +3 max.

M6, aggregate per day

  • Bucket by UTC day, compute averages and counts.
  • Check: exactly 7 day entries, zeros for days with no messages.

M7, burnout rules

  • Implement three triggers and reasons.
  • Check: synthetic week triggers as expected.

M8, insights bullets

  • Generate three bullets from WeekAgg.
  • Check: bullets reference actual day names and numbers.

M9, SSR page and chart

  • Read ?channels= from query.
  • Call /api/pulse server‑side.
  • Render line chart and bullets.
  • Add a minimal form to set channels.
  • Check: full flow works for 1 to 3 channels.

M10, errors and polish

  • Handle missing token, missing scopes, 429 with a short message.
  • Show “no data this week” state.
  • Check: demo stable on Vercel.

Minimal helpers

// lib/score.ts
import Sentiment from 'sentiment'
import emoji from 'node-emoji'
const s = new Sentiment()
const REACTION: Record<string, number> = { thumbsup:1, '+1':1, heart:1, smile:1, tada:1, joy:2, thumbsdown:-1, '-1':-1, cry:-1, sob:-1, angry:-2, rage:-2 }
const BURNOUT = ['burnout','burned out','overwhelmed','exhausted','drained','stressed','quitting','resign']
export function scoreMessage(text: string, reactions?: {name:string,count:number}[]) {
  const t = emoji.emojify(text || '')
  const textScore = s.analyze(t).score
  let rx = 0
  for (const r of reactions || []) rx += (REACTION[r.name] ?? 0) * r.count
  const rxCap = Math.max(-3, Math.min(3, rx))
  const burnoutFlag = BURNOUT.some(k => t.toLowerCase().includes(k))
  return { score: textScore + rxCap, burnoutFlag }
}

Acceptance checklist

  • Channels are user‑defined via query or form, and persisted in the URL.
  • Threads are included by fetching replies.
  • Reactions influence score using the curated map.
  • Every message is scored for text and emoji.
  • Daily mood is aggregated and plotted for the last 7 days.
  • Burnout warnings appear when rules trigger, with reasons.
  • Insights show three concrete, team‑level actions.

Out of scope for minimal delivery

  • OAuth install flow, multi‑tenant, real‑time events, databases, Claude integration, advanced NLP, topic mining, user‑level analytics.

Notes for the demo

  • Use a small set of active channels to keep latency low.
  • Show the URL with channels in query to prove configurability.
  • Click refresh after a message is posted to show live effect.
  • Keep a printed one‑pager of scopes and setup in case of token issues.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment