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 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
- 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.
- 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
sentimentfor text plusnode-emojifor 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.
next,react,typescript@slack/web-apisentimentnode-emojidayjsreact-chartjs-2,chart.js
- 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.
- User opens
/with?channels=C123,C234or submits a simple form that reloads the page with this query. - SSR calls
/api/pulseon the server with the list of channel IDs. - For each channel: fetch
conversations.historyfor last 7 days, paginate ifhas_more. - For each parent with
reply_count > 0: fetchconversations.replies. - For every message and reply: clean text, convert
:shortcode:to emoji, score text withsentiment, add reaction weights, cap reaction impact. - Bucket by UTC day, compute daily averages and week summary.
- Apply simple burnout rules, produce three actionable bullets.
- Return JSON to the page.
- Page renders a 7‑point line chart, a burnout badge if triggered, and the bullets.
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[]
}-
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.
- Positive:
-
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.
Trigger if any of the following holds:
- At least 2 days with
avgScore < -0.30. - Week
trendDelta≤ −0.40 (Mon to latest day). - Total
burnoutCountacross the week ≥ 3.
Store brief reasons, for example: ["2 very low days","downward trend","3 burnout mentions"].
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.”
- 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.
/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
M0, scaffold
- Create Next app with TS. Install deps.
- Check: dev server runs, typecheck clean.
M1, one channel, no threads
/api/pulse: fetchconversations.historyfor 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, fetchconversations.replies. - Combine parents and replies.
- Check: totals increase when threads present.
M3, sentiment for text
- Add
scoreText()usingsentiment. - 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/pulseserver‑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.
// 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 }
}- 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.
- OAuth install flow, multi‑tenant, real‑time events, databases, Claude integration, advanced NLP, topic mining, user‑level analytics.
- 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.