From 48ed30009473258552227b0341a1f57dde8bfadd Mon Sep 17 00:00:00 2001 From: saridsa2 Date: Thu, 26 Mar 2026 09:51:49 +0530 Subject: [PATCH] feat: unified context panel, remove tabs, context-aware layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Merged AI Assistant + Lead 360 tabs into single context-aware panel - Context section shows: lead profile, AI insight (live from event bus), appointments, activity - "On call with" banner only shows during active calls (isInCall prop) - AI Chat always available at bottom of panel - Phase 1 only — AI Chat panel needs full redesign with Vercel AI SDK tool calling (Phase 2) Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/generate-pptx.cjs | 680 ++++++++++++++ docs/generate-pptx.js | 680 ++++++++++++++ docs/weekly-update-mar18-25.html | 886 ++++++++++++++++++ docs/weekly-update-mar18-25.pptx | Bin 0 -> 218927 bytes .../avatar/base-components/avatar-count.tsx | 14 + src/components/base/select/select-shared.tsx | 49 + src/components/call-desk/context-panel.tsx | 310 +++--- 7 files changed, 2424 insertions(+), 195 deletions(-) create mode 100644 docs/generate-pptx.cjs create mode 100644 docs/generate-pptx.js create mode 100644 docs/weekly-update-mar18-25.html create mode 100644 docs/weekly-update-mar18-25.pptx create mode 100644 src/components/base/avatar/base-components/avatar-count.tsx create mode 100644 src/components/base/select/select-shared.tsx diff --git a/docs/generate-pptx.cjs b/docs/generate-pptx.cjs new file mode 100644 index 0000000..3977483 --- /dev/null +++ b/docs/generate-pptx.cjs @@ -0,0 +1,680 @@ +/** + * Helix Engage — Weekly Update (Mar 18–25, 2026) + * Light Mode PowerPoint Generator via PptxGenJS + */ +const PptxGenJS = require("pptxgenjs"); + +// ── Design Tokens (Light Mode) ───────────────────────────────────────── +const C = { + bg: "FFFFFF", + bgSubtle: "F8FAFC", + bgCard: "F1F5F9", + bgCardAlt: "E2E8F0", + text: "1E293B", + textSec: "475569", + textMuted: "94A3B8", + accent1: "0EA5E9", // Sky blue (telephony) + accent2: "8B5CF6", // Violet (server/backend) + accent3: "10B981", // Emerald (UX) + accent4: "F59E0B", // Amber (features) + accent5: "EF4444", // Rose (ops) + accent6: "6366F1", // Indigo (timeline) + white: "FFFFFF", + border: "CBD5E1", +}; + +const FONT = { + heading: "Arial", + body: "Arial", +}; + +// ── Helpers ────────────────────────────────────────────────────────────── +function addSlideNumber(slide, num, total) { + slide.addText(`${num} / ${total}`, { + x: 8.8, y: 5.2, w: 1.2, h: 0.3, + fontSize: 8, color: C.textMuted, + fontFace: FONT.body, + align: "right", + }); +} + +function addAccentBar(slide, color) { + slide.addShape("rect", { + x: 0, y: 0, w: 10, h: 0.06, + fill: { color }, + }); +} + +function addLabel(slide, text, color, x, y) { + slide.addShape("roundRect", { + x, y, w: text.length * 0.09 + 0.4, h: 0.3, + fill: { color, transparency: 88 }, + rectRadius: 0.15, + }); + slide.addText(text.toUpperCase(), { + x, y, w: text.length * 0.09 + 0.4, h: 0.3, + fontSize: 7, fontFace: FONT.heading, bold: true, + color, align: "center", valign: "middle", + letterSpacing: 2, + }); +} + +function addCard(slide, opts) { + const { x, y, w, h, title, titleColor, items, badge } = opts; + // Card background + slide.addShape("roundRect", { + x, y, w, h, + fill: { color: C.bgCard }, + line: { color: C.border, width: 0.5 }, + rectRadius: 0.1, + }); + // Title + const titleText = badge + ? [{ text: title + " ", options: { bold: true, color: titleColor, fontSize: 11 } }, + { text: badge, options: { bold: true, color: C.white, fontSize: 7, highlight: titleColor } }] + : title; + slide.addText(titleText, { + x: x + 0.2, y: y + 0.08, w: w - 0.4, h: 0.35, + fontSize: 11, fontFace: FONT.heading, bold: true, + color: titleColor, + }); + // Items as bullet list + if (items && items.length > 0) { + slide.addText( + items.map(item => ({ + text: item, + options: { + fontSize: 8.5, fontFace: FONT.body, color: C.textSec, + bullet: { type: "bullet", style: "arabicPeriod" }, + paraSpaceAfter: 2, + breakLine: true, + }, + })), + { + x: x + 0.2, y: y + 0.4, w: w - 0.4, h: h - 0.5, + valign: "top", + bullet: { type: "bullet" }, + lineSpacingMultiple: 1.1, + } + ); + } +} + +// ── Build Presentation ────────────────────────────────────────────────── +async function build() { + const pptx = new PptxGenJS(); + pptx.layout = "LAYOUT_16x9"; + pptx.author = "Satya Suman Sari"; + pptx.company = "FortyTwo Platform"; + pptx.title = "Helix Engage — Weekly Update (Mar 18–25, 2026)"; + pptx.subject = "Engineering Progress Report"; + + const TOTAL = 9; + + // ═══════════════════════════════════════════════════════════════════════ + // SLIDE 1 — Title + // ═══════════════════════════════════════════════════════════════════════ + { + const slide = pptx.addSlide(); + slide.background = { color: C.bg }; + + // Accent bar top + addAccentBar(slide, C.accent1); + + // Decorative side stripe + slide.addShape("rect", { + x: 0, y: 0, w: 0.12, h: 5.63, + fill: { color: C.accent1 }, + }); + + // Label + addLabel(slide, "Weekly Engineering Update", C.accent1, 3.0, 1.2); + + // Title + slide.addText("Helix Engage", { + x: 1.0, y: 1.8, w: 8, h: 1.2, + fontSize: 44, fontFace: FONT.heading, bold: true, + color: C.accent1, align: "center", + }); + + // Subtitle + slide.addText("Contact Center CRM · Real-time Telephony · AI Copilot", { + x: 1.5, y: 2.9, w: 7, h: 0.5, + fontSize: 14, fontFace: FONT.body, + color: C.textSec, align: "center", + }); + + // Date + slide.addText("March 18 – 25, 2026", { + x: 3, y: 3.6, w: 4, h: 0.4, + fontSize: 12, fontFace: FONT.heading, bold: true, + color: C.textMuted, align: "center", + letterSpacing: 3, + }); + + // Bottom decoration + slide.addShape("rect", { + x: 3.5, y: 4.2, w: 3, h: 0.04, + fill: { color: C.accent2 }, + }); + + // Author + slide.addText("Satya Suman Sari · FortyTwo Platform", { + x: 2, y: 4.5, w: 6, h: 0.35, + fontSize: 9, fontFace: FONT.body, + color: C.textMuted, align: "center", + }); + + addSlideNumber(slide, 1, TOTAL); + } + + // ═══════════════════════════════════════════════════════════════════════ + // SLIDE 2 — At a Glance + // ═══════════════════════════════════════════════════════════════════════ + { + const slide = pptx.addSlide(); + slide.background = { color: C.bg }; + addAccentBar(slide, C.accent2); + + addLabel(slide, "At a Glance", C.accent2, 0.5, 0.3); + + slide.addText("Week in Numbers", { + x: 0.5, y: 0.65, w: 5, h: 0.5, + fontSize: 24, fontFace: FONT.heading, bold: true, + color: C.text, + }); + + // Stat cards + const stats = [ + { value: "78", label: "Total Commits", color: C.accent1 }, + { value: "3", label: "Repositories", color: C.accent2 }, + { value: "8", label: "Days Active", color: C.accent3 }, + { value: "50", label: "Frontend Commits", color: C.accent4 }, + ]; + + stats.forEach((s, i) => { + const x = 0.5 + i * 2.35; + // Card bg + slide.addShape("roundRect", { + x, y: 1.3, w: 2.1, h: 1.7, + fill: { color: C.bgCard }, + line: { color: C.border, width: 0.5 }, + rectRadius: 0.12, + }); + // Accent top line + slide.addShape("rect", { + x: x + 0.2, y: 1.35, w: 1.7, h: 0.035, + fill: { color: s.color }, + }); + // Number + slide.addText(s.value, { + x, y: 1.5, w: 2.1, h: 0.9, + fontSize: 36, fontFace: FONT.heading, bold: true, + color: s.color, align: "center", valign: "middle", + }); + // Label + slide.addText(s.label, { + x, y: 2.4, w: 2.1, h: 0.4, + fontSize: 9, fontFace: FONT.body, + color: C.textSec, align: "center", + }); + }); + + // Repo breakdown pills + const repos = [ + { name: "helix-engage", count: "50", clr: C.accent1 }, + { name: "helix-engage-server", count: "27", clr: C.accent2 }, + { name: "FortyTwoApps/SDK", count: "1", clr: C.accent3 }, + ]; + repos.forEach((r, i) => { + const x = 1.5 + i * 2.8; + slide.addShape("roundRect", { + x, y: 3.4, w: 2.5, h: 0.4, + fill: { color: C.bgCard }, + line: { color: r.clr, width: 1 }, + rectRadius: 0.2, + }); + slide.addText(`${r.name} ${r.count}`, { + x, y: 3.4, w: 2.5, h: 0.4, + fontSize: 9, fontFace: FONT.heading, bold: true, + color: r.clr, align: "center", valign: "middle", + }); + }); + + // Summary text + slide.addText("3 repos · 7 working days · 78 commits shipped to production", { + x: 1, y: 4.2, w: 8, h: 0.35, + fontSize: 10, fontFace: FONT.body, italic: true, + color: C.textMuted, align: "center", + }); + + addSlideNumber(slide, 2, TOTAL); + } + + // ═══════════════════════════════════════════════════════════════════════ + // SLIDE 3 — Telephony & SIP + // ═══════════════════════════════════════════════════════════════════════ + { + const slide = pptx.addSlide(); + slide.background = { color: C.bg }; + addAccentBar(slide, C.accent1); + + addLabel(slide, "Core Infrastructure", C.accent1, 0.5, 0.3); + + slide.addText([ + { text: "☎ ", options: { fontSize: 22 } }, + { text: "Telephony & SIP Overhaul", options: { fontSize: 22, bold: true, color: C.text } }, + ], { x: 0.5, y: 0.65, w: 9, h: 0.5, fontFace: FONT.heading }); + + addCard(slide, { + x: 0.3, y: 1.35, w: 4.5, h: 2.0, + title: "Outbound Calling", titleColor: C.accent1, badge: "Frontend", + items: [ + "Direct SIP call from browser — no Kookoo bridge", + "Immediate call card UI with auto-answer SIP bridge", + "End Call label fix, force active state after auto-answer", + "Reset outboundPending on call end", + ], + }); + + addCard(slide, { + x: 5.2, y: 1.35, w: 4.5, h: 2.0, + title: "Ozonetel Integration", titleColor: C.accent2, badge: "Server", + items: [ + "Ozonetel V3 dial endpoint + webhook handler", + "Set Disposition API for ACW release", + "Force Ready endpoint for agent state mgmt", + "Token: 10-min cache, 401 invalidation, refresh on login", + ], + }); + + addCard(slide, { + x: 0.3, y: 3.55, w: 4.5, h: 1.8, + title: "SIP & Agent State", titleColor: C.accent1, badge: "Frontend", + items: [ + "SIP driven by Agent entity with token refresh", + "Centralised outbound dial into useSip().dialOutbound()", + "UCID tracking from SIP headers for disposition", + "Network indicator for connection health", + ], + }); + + addCard(slide, { + x: 5.2, y: 3.55, w: 4.5, h: 1.8, + title: "Multi-Agent & Sessions", titleColor: C.accent2, badge: "Server", + items: [ + "Multi-agent SIP with Redis session lockout", + "Strict duplicate login — one device per agent", + "Session lock stores IP + timestamp for debugging", + "SSE agent state broadcast for supervisor view", + ], + }); + + addSlideNumber(slide, 3, TOTAL); + } + + // ═══════════════════════════════════════════════════════════════════════ + // SLIDE 4 — Call Desk & Agent UX + // ═══════════════════════════════════════════════════════════════════════ + { + const slide = pptx.addSlide(); + slide.background = { color: C.bg }; + addAccentBar(slide, C.accent3); + + addLabel(slide, "User Experience", C.accent3, 0.5, 0.3); + + slide.addText([ + { text: "🖥 ", options: { fontSize: 22 } }, + { text: "Call Desk & Agent UX", options: { fontSize: 22, bold: true, color: C.text } }, + ], { x: 0.5, y: 0.65, w: 9, h: 0.5, fontFace: FONT.heading }); + + addCard(slide, { + x: 0.3, y: 1.35, w: 3.05, h: 2.6, + title: "Call Desk Redesign", titleColor: C.accent3, + items: [ + "2-panel layout with collapsible sidebar & inline AI", + "Collapsible context panel, worklist/calls tabs", + "Pinned header & chat input, numpad dialler", + "Ringtone support for incoming calls", + ], + }); + + addCard(slide, { + x: 3.55, y: 1.35, w: 3.05, h: 2.6, + title: "Post-Call Workflow", titleColor: C.accent3, + items: [ + "Disposition → appointment booking → follow-up", + "Disposition returns straight to worklist", + "Send disposition to sidecar with UCID for ACW", + "Enquiry in post-call, appointment skip button", + ], + }); + + addCard(slide, { + x: 6.8, y: 1.35, w: 2.9, h: 2.6, + title: "UI Polish", titleColor: C.accent3, + items: [ + "FontAwesome Pro Duotone icon migration", + "Tooltips, sticky headers, roles, search", + "Fix React error #520 in prod tables", + "AI scroll containment, brand tokens refresh", + ], + }); + + addSlideNumber(slide, 4, TOTAL); + } + + // ═══════════════════════════════════════════════════════════════════════ + // SLIDE 5 — Features Shipped + // ═══════════════════════════════════════════════════════════════════════ + { + const slide = pptx.addSlide(); + slide.background = { color: C.bg }; + addAccentBar(slide, C.accent4); + + addLabel(slide, "Features Shipped", C.accent4, 0.5, 0.3); + + slide.addText([ + { text: "🚀 ", options: { fontSize: 22 } }, + { text: "Major Features", options: { fontSize: 22, bold: true, color: C.text } }, + ], { x: 0.5, y: 0.65, w: 9, h: 0.5, fontFace: FONT.heading }); + + addCard(slide, { + x: 0.3, y: 1.35, w: 4.5, h: 2.0, + title: "Supervisor Module", titleColor: C.accent4, + items: [ + "Team performance analytics page", + "Live monitor with active calls visibility", + "Master data management pages", + "Server: team perf + active calls endpoints", + ], + }); + + addCard(slide, { + x: 5.2, y: 1.35, w: 4.5, h: 2.0, + title: "Missed Call Queue (Phase 2)", titleColor: C.accent4, + items: [ + "Missed call queue ingestion & worklist", + "Auto-assignment engine for agents", + "Login redesign with role-based routing", + "Lead lookup for missed callers", + ], + }); + + addCard(slide, { + x: 0.3, y: 3.55, w: 4.5, h: 1.8, + title: "Agent Features (Phase 1)", titleColor: C.accent4, + items: [ + "Agent status toggle (Ready / Not Ready / Break)", + "Global search across patients, leads, calls", + "Enquiry form for new patient intake", + "My Performance page + logout modal", + ], + }); + + addCard(slide, { + x: 5.2, y: 3.55, w: 4.5, h: 1.8, + title: "Recording Analysis", titleColor: C.accent4, + items: [ + "Deepgram diarization + AI insights", + "Redis caching layer for analysis results", + "Full-stack: frontend player + server module", + ], + }); + + addSlideNumber(slide, 5, TOTAL); + } + + // ═══════════════════════════════════════════════════════════════════════ + // SLIDE 6 — Backend & Data + // ═══════════════════════════════════════════════════════════════════════ + { + const slide = pptx.addSlide(); + slide.background = { color: C.bg }; + addAccentBar(slide, C.accent2); + + addLabel(slide, "Backend & Data", C.accent2, 0.5, 0.3); + + slide.addText([ + { text: "⚙ ", options: { fontSize: 22 } }, + { text: "Backend & Data Layer", options: { fontSize: 22, bold: true, color: C.text } }, + ], { x: 0.5, y: 0.65, w: 9, h: 0.5, fontFace: FONT.heading }); + + addCard(slide, { + x: 0.3, y: 1.35, w: 4.5, h: 2.0, + title: "Platform Data Wiring", titleColor: C.accent2, + items: [ + "Migrated frontend to Jotai + Vercel AI SDK", + "Corrected all 7 GraphQL queries (fields, LINKS/PHONES)", + "Webhook handler for Ozonetel call records", + "Complete seeder: 5 doctors, appointments linked", + ], + }); + + addCard(slide, { + x: 5.2, y: 1.35, w: 4.5, h: 2.0, + title: "Server Endpoints", titleColor: C.accent2, + items: [ + "Call control, recording, CDR, missed calls, live assist", + "Agent summary, AHT, performance aggregation", + "Token refresh endpoint for auto-renewal", + "Search module with full-text capabilities", + ], + }); + + addCard(slide, { + x: 0.3, y: 3.55, w: 4.5, h: 1.8, + title: "Data Pages Built", titleColor: C.accent2, + items: [ + "Worklist table, call history, patients, dashboard", + "Reports, team dashboard, campaigns, settings", + "Agent detail page, campaign edit slideout", + "Appointments page with data refresh on login", + ], + }); + + addCard(slide, { + x: 5.2, y: 3.55, w: 4.5, h: 1.8, + title: "SDK App", titleColor: C.accent3, badge: "FortyTwoApps", + items: [ + "Helix Engage SDK app entity definitions", + "Call center CRM object model for platform", + "Foundation for platform-native data integration", + ], + }); + + addSlideNumber(slide, 6, TOTAL); + } + + // ═══════════════════════════════════════════════════════════════════════ + // SLIDE 7 — Deployment & Ops + // ═══════════════════════════════════════════════════════════════════════ + { + const slide = pptx.addSlide(); + slide.background = { color: C.bg }; + addAccentBar(slide, C.accent5); + + addLabel(slide, "Operations", C.accent5, 0.5, 0.3); + + slide.addText([ + { text: "🛠 ", options: { fontSize: 22 } }, + { text: "Deployment & DevOps", options: { fontSize: 22, bold: true, color: C.text } }, + ], { x: 0.5, y: 0.65, w: 9, h: 0.5, fontFace: FONT.heading }); + + addCard(slide, { + x: 0.3, y: 1.35, w: 3.05, h: 2.2, + title: "Deployment", titleColor: C.accent5, + items: [ + "Deployed to Hostinger VPS with Docker", + "Switched to global_healthx Ozonetel account", + "Dockerfile for server-side containerization", + ], + }); + + addCard(slide, { + x: 3.55, y: 1.35, w: 3.05, h: 2.2, + title: "AI & Testing", titleColor: C.accent5, + items: [ + "Migrated AI to Vercel AI SDK + OpenAI provider", + "AI flow test script — validates full pipeline", + "Live call assist integration", + ], + }); + + addCard(slide, { + x: 6.8, y: 1.35, w: 2.9, h: 2.2, + title: "Documentation", titleColor: C.accent5, + items: [ + "Team onboarding README with arch guide", + "Supervisor module spec + plan", + "Multi-agent spec + plan", + "Next session plans in commits", + ], + }); + + addSlideNumber(slide, 7, TOTAL); + } + + // ═══════════════════════════════════════════════════════════════════════ + // SLIDE 8 — Timeline + // ═══════════════════════════════════════════════════════════════════════ + { + const slide = pptx.addSlide(); + slide.background = { color: C.bg }; + addAccentBar(slide, C.accent6); + + addLabel(slide, "Day by Day", C.accent6, 0.5, 0.3); + + slide.addText([ + { text: "📅 ", options: { fontSize: 22 } }, + { text: "Development Timeline", options: { fontSize: 22, bold: true, color: C.text } }, + ], { x: 0.5, y: 0.65, w: 9, h: 0.5, fontFace: FONT.heading }); + + const timeline = [ + { date: "MAR 18 (Tue)", title: "Foundation Day", desc: "Call desk redesign, Jotai + AI SDK migration, seeder, AI flow test, VPS deploy" }, + { date: "MAR 19 (Wed)", title: "Data Layer Sprint", desc: "All data pages, post-call workflow, GraphQL fixes, Kookoo IVR, outbound UI" }, + { date: "MAR 20 (Thu)", title: "Telephony Breakthrough", desc: "Direct SIP replacing Kookoo, UCID tracking, Force Ready, Set Disposition" }, + { date: "MAR 21 (Fri)", title: "Agent Experience", desc: "Phase 1: status toggle, search, enquiry form, My Performance, FA icons, AHT" }, + { date: "MAR 23 (Sun)", title: "Scale & Reliability", desc: "Phase 2: missed call queue, auto-assign, Redis lockout, Patient 360, SDK defs" }, + { date: "MAR 24 (Mon)", title: "Supervisor Module", desc: "Team perf, live monitor, master data, SSE, UUID fix, maintenance, QA sweep" }, + { date: "MAR 25 (Tue)", title: "Intelligence Layer", desc: "Deepgram diarization, AI insights, SIP via Agent entity, token refresh, network" }, + ]; + + // Vertical line + slide.addShape("rect", { + x: 1.4, y: 1.3, w: 0.025, h: 4.0, + fill: { color: C.accent6, transparency: 60 }, + }); + + timeline.forEach((entry, i) => { + const y = 1.3 + i * 0.56; + + // Dot + slide.addShape("ellipse", { + x: 1.32, y: y + 0.08, w: 0.18, h: 0.18, + fill: { color: C.accent6 }, + line: { color: C.bg, width: 2 }, + }); + + // Date + slide.addText(entry.date, { + x: 1.7, y: y, w: 1.6, h: 0.22, + fontSize: 7, fontFace: FONT.heading, bold: true, + color: C.accent6, + }); + + // Title + slide.addText(entry.title, { + x: 3.3, y: y, w: 2.0, h: 0.22, + fontSize: 9, fontFace: FONT.heading, bold: true, + color: C.text, + }); + + // Description + slide.addText(entry.desc, { + x: 5.3, y: y, w: 4.2, h: 0.45, + fontSize: 8, fontFace: FONT.body, + color: C.textSec, + valign: "top", + }); + }); + + addSlideNumber(slide, 8, TOTAL); + } + + // ═══════════════════════════════════════════════════════════════════════ + // SLIDE 9 — Closing + // ═══════════════════════════════════════════════════════════════════════ + { + const slide = pptx.addSlide(); + slide.background = { color: C.bg }; + addAccentBar(slide, C.accent3); + + // Big headline + slide.addText("78 commits. 8 days. Ship mode.", { + x: 0.5, y: 1.4, w: 9, h: 0.8, + fontSize: 32, fontFace: FONT.heading, bold: true, + color: C.accent3, align: "center", + }); + + // Ship emoji + slide.addText("🚢", { + x: 4.2, y: 2.3, w: 1.6, h: 0.6, + fontSize: 28, align: "center", + }); + + // Description + slide.addText( + "From browser-native SIP calling to AI-powered recording analysis — Helix Engage is becoming a production contact center platform.", + { + x: 1.5, y: 3.0, w: 7, h: 0.6, + fontSize: 11, fontFace: FONT.body, + color: C.textSec, align: "center", + lineSpacingMultiple: 1.3, + } + ); + + // Achievement pills + const achievements = [ + { text: "SIP Calling ✓", color: C.accent1 }, + { text: "Multi-Agent ✓", color: C.accent2 }, + { text: "Supervisor ✓", color: C.accent3 }, + { text: "AI Copilot ✓", color: C.accent4 }, + { text: "Recording Analysis ✓", color: C.accent5 }, + ]; + + achievements.forEach((a, i) => { + const x = 0.8 + i * 1.8; + slide.addShape("roundRect", { + x, y: 3.9, w: 1.6, h: 0.35, + fill: { color: C.bgCard }, + line: { color: a.color, width: 1 }, + rectRadius: 0.17, + }); + slide.addText(a.text, { + x, y: 3.9, w: 1.6, h: 0.35, + fontSize: 8, fontFace: FONT.heading, bold: true, + color: a.color, align: "center", valign: "middle", + }); + }); + + // Author + slide.addText("Satya Suman Sari · FortyTwo Platform", { + x: 2, y: 4.7, w: 6, h: 0.3, + fontSize: 9, fontFace: FONT.body, + color: C.textMuted, align: "center", + }); + + addSlideNumber(slide, 9, TOTAL); + } + + // ── Save ────────────────────────────────────────────────────────────── + const outPath = "weekly-update-mar18-25.pptx"; + await pptx.writeFile({ fileName: outPath }); + console.log(`✅ Presentation saved: ${outPath}`); +} + +build().catch(err => { + console.error("❌ Failed:", err.message); + process.exit(1); +}); diff --git a/docs/generate-pptx.js b/docs/generate-pptx.js new file mode 100644 index 0000000..3977483 --- /dev/null +++ b/docs/generate-pptx.js @@ -0,0 +1,680 @@ +/** + * Helix Engage — Weekly Update (Mar 18–25, 2026) + * Light Mode PowerPoint Generator via PptxGenJS + */ +const PptxGenJS = require("pptxgenjs"); + +// ── Design Tokens (Light Mode) ───────────────────────────────────────── +const C = { + bg: "FFFFFF", + bgSubtle: "F8FAFC", + bgCard: "F1F5F9", + bgCardAlt: "E2E8F0", + text: "1E293B", + textSec: "475569", + textMuted: "94A3B8", + accent1: "0EA5E9", // Sky blue (telephony) + accent2: "8B5CF6", // Violet (server/backend) + accent3: "10B981", // Emerald (UX) + accent4: "F59E0B", // Amber (features) + accent5: "EF4444", // Rose (ops) + accent6: "6366F1", // Indigo (timeline) + white: "FFFFFF", + border: "CBD5E1", +}; + +const FONT = { + heading: "Arial", + body: "Arial", +}; + +// ── Helpers ────────────────────────────────────────────────────────────── +function addSlideNumber(slide, num, total) { + slide.addText(`${num} / ${total}`, { + x: 8.8, y: 5.2, w: 1.2, h: 0.3, + fontSize: 8, color: C.textMuted, + fontFace: FONT.body, + align: "right", + }); +} + +function addAccentBar(slide, color) { + slide.addShape("rect", { + x: 0, y: 0, w: 10, h: 0.06, + fill: { color }, + }); +} + +function addLabel(slide, text, color, x, y) { + slide.addShape("roundRect", { + x, y, w: text.length * 0.09 + 0.4, h: 0.3, + fill: { color, transparency: 88 }, + rectRadius: 0.15, + }); + slide.addText(text.toUpperCase(), { + x, y, w: text.length * 0.09 + 0.4, h: 0.3, + fontSize: 7, fontFace: FONT.heading, bold: true, + color, align: "center", valign: "middle", + letterSpacing: 2, + }); +} + +function addCard(slide, opts) { + const { x, y, w, h, title, titleColor, items, badge } = opts; + // Card background + slide.addShape("roundRect", { + x, y, w, h, + fill: { color: C.bgCard }, + line: { color: C.border, width: 0.5 }, + rectRadius: 0.1, + }); + // Title + const titleText = badge + ? [{ text: title + " ", options: { bold: true, color: titleColor, fontSize: 11 } }, + { text: badge, options: { bold: true, color: C.white, fontSize: 7, highlight: titleColor } }] + : title; + slide.addText(titleText, { + x: x + 0.2, y: y + 0.08, w: w - 0.4, h: 0.35, + fontSize: 11, fontFace: FONT.heading, bold: true, + color: titleColor, + }); + // Items as bullet list + if (items && items.length > 0) { + slide.addText( + items.map(item => ({ + text: item, + options: { + fontSize: 8.5, fontFace: FONT.body, color: C.textSec, + bullet: { type: "bullet", style: "arabicPeriod" }, + paraSpaceAfter: 2, + breakLine: true, + }, + })), + { + x: x + 0.2, y: y + 0.4, w: w - 0.4, h: h - 0.5, + valign: "top", + bullet: { type: "bullet" }, + lineSpacingMultiple: 1.1, + } + ); + } +} + +// ── Build Presentation ────────────────────────────────────────────────── +async function build() { + const pptx = new PptxGenJS(); + pptx.layout = "LAYOUT_16x9"; + pptx.author = "Satya Suman Sari"; + pptx.company = "FortyTwo Platform"; + pptx.title = "Helix Engage — Weekly Update (Mar 18–25, 2026)"; + pptx.subject = "Engineering Progress Report"; + + const TOTAL = 9; + + // ═══════════════════════════════════════════════════════════════════════ + // SLIDE 1 — Title + // ═══════════════════════════════════════════════════════════════════════ + { + const slide = pptx.addSlide(); + slide.background = { color: C.bg }; + + // Accent bar top + addAccentBar(slide, C.accent1); + + // Decorative side stripe + slide.addShape("rect", { + x: 0, y: 0, w: 0.12, h: 5.63, + fill: { color: C.accent1 }, + }); + + // Label + addLabel(slide, "Weekly Engineering Update", C.accent1, 3.0, 1.2); + + // Title + slide.addText("Helix Engage", { + x: 1.0, y: 1.8, w: 8, h: 1.2, + fontSize: 44, fontFace: FONT.heading, bold: true, + color: C.accent1, align: "center", + }); + + // Subtitle + slide.addText("Contact Center CRM · Real-time Telephony · AI Copilot", { + x: 1.5, y: 2.9, w: 7, h: 0.5, + fontSize: 14, fontFace: FONT.body, + color: C.textSec, align: "center", + }); + + // Date + slide.addText("March 18 – 25, 2026", { + x: 3, y: 3.6, w: 4, h: 0.4, + fontSize: 12, fontFace: FONT.heading, bold: true, + color: C.textMuted, align: "center", + letterSpacing: 3, + }); + + // Bottom decoration + slide.addShape("rect", { + x: 3.5, y: 4.2, w: 3, h: 0.04, + fill: { color: C.accent2 }, + }); + + // Author + slide.addText("Satya Suman Sari · FortyTwo Platform", { + x: 2, y: 4.5, w: 6, h: 0.35, + fontSize: 9, fontFace: FONT.body, + color: C.textMuted, align: "center", + }); + + addSlideNumber(slide, 1, TOTAL); + } + + // ═══════════════════════════════════════════════════════════════════════ + // SLIDE 2 — At a Glance + // ═══════════════════════════════════════════════════════════════════════ + { + const slide = pptx.addSlide(); + slide.background = { color: C.bg }; + addAccentBar(slide, C.accent2); + + addLabel(slide, "At a Glance", C.accent2, 0.5, 0.3); + + slide.addText("Week in Numbers", { + x: 0.5, y: 0.65, w: 5, h: 0.5, + fontSize: 24, fontFace: FONT.heading, bold: true, + color: C.text, + }); + + // Stat cards + const stats = [ + { value: "78", label: "Total Commits", color: C.accent1 }, + { value: "3", label: "Repositories", color: C.accent2 }, + { value: "8", label: "Days Active", color: C.accent3 }, + { value: "50", label: "Frontend Commits", color: C.accent4 }, + ]; + + stats.forEach((s, i) => { + const x = 0.5 + i * 2.35; + // Card bg + slide.addShape("roundRect", { + x, y: 1.3, w: 2.1, h: 1.7, + fill: { color: C.bgCard }, + line: { color: C.border, width: 0.5 }, + rectRadius: 0.12, + }); + // Accent top line + slide.addShape("rect", { + x: x + 0.2, y: 1.35, w: 1.7, h: 0.035, + fill: { color: s.color }, + }); + // Number + slide.addText(s.value, { + x, y: 1.5, w: 2.1, h: 0.9, + fontSize: 36, fontFace: FONT.heading, bold: true, + color: s.color, align: "center", valign: "middle", + }); + // Label + slide.addText(s.label, { + x, y: 2.4, w: 2.1, h: 0.4, + fontSize: 9, fontFace: FONT.body, + color: C.textSec, align: "center", + }); + }); + + // Repo breakdown pills + const repos = [ + { name: "helix-engage", count: "50", clr: C.accent1 }, + { name: "helix-engage-server", count: "27", clr: C.accent2 }, + { name: "FortyTwoApps/SDK", count: "1", clr: C.accent3 }, + ]; + repos.forEach((r, i) => { + const x = 1.5 + i * 2.8; + slide.addShape("roundRect", { + x, y: 3.4, w: 2.5, h: 0.4, + fill: { color: C.bgCard }, + line: { color: r.clr, width: 1 }, + rectRadius: 0.2, + }); + slide.addText(`${r.name} ${r.count}`, { + x, y: 3.4, w: 2.5, h: 0.4, + fontSize: 9, fontFace: FONT.heading, bold: true, + color: r.clr, align: "center", valign: "middle", + }); + }); + + // Summary text + slide.addText("3 repos · 7 working days · 78 commits shipped to production", { + x: 1, y: 4.2, w: 8, h: 0.35, + fontSize: 10, fontFace: FONT.body, italic: true, + color: C.textMuted, align: "center", + }); + + addSlideNumber(slide, 2, TOTAL); + } + + // ═══════════════════════════════════════════════════════════════════════ + // SLIDE 3 — Telephony & SIP + // ═══════════════════════════════════════════════════════════════════════ + { + const slide = pptx.addSlide(); + slide.background = { color: C.bg }; + addAccentBar(slide, C.accent1); + + addLabel(slide, "Core Infrastructure", C.accent1, 0.5, 0.3); + + slide.addText([ + { text: "☎ ", options: { fontSize: 22 } }, + { text: "Telephony & SIP Overhaul", options: { fontSize: 22, bold: true, color: C.text } }, + ], { x: 0.5, y: 0.65, w: 9, h: 0.5, fontFace: FONT.heading }); + + addCard(slide, { + x: 0.3, y: 1.35, w: 4.5, h: 2.0, + title: "Outbound Calling", titleColor: C.accent1, badge: "Frontend", + items: [ + "Direct SIP call from browser — no Kookoo bridge", + "Immediate call card UI with auto-answer SIP bridge", + "End Call label fix, force active state after auto-answer", + "Reset outboundPending on call end", + ], + }); + + addCard(slide, { + x: 5.2, y: 1.35, w: 4.5, h: 2.0, + title: "Ozonetel Integration", titleColor: C.accent2, badge: "Server", + items: [ + "Ozonetel V3 dial endpoint + webhook handler", + "Set Disposition API for ACW release", + "Force Ready endpoint for agent state mgmt", + "Token: 10-min cache, 401 invalidation, refresh on login", + ], + }); + + addCard(slide, { + x: 0.3, y: 3.55, w: 4.5, h: 1.8, + title: "SIP & Agent State", titleColor: C.accent1, badge: "Frontend", + items: [ + "SIP driven by Agent entity with token refresh", + "Centralised outbound dial into useSip().dialOutbound()", + "UCID tracking from SIP headers for disposition", + "Network indicator for connection health", + ], + }); + + addCard(slide, { + x: 5.2, y: 3.55, w: 4.5, h: 1.8, + title: "Multi-Agent & Sessions", titleColor: C.accent2, badge: "Server", + items: [ + "Multi-agent SIP with Redis session lockout", + "Strict duplicate login — one device per agent", + "Session lock stores IP + timestamp for debugging", + "SSE agent state broadcast for supervisor view", + ], + }); + + addSlideNumber(slide, 3, TOTAL); + } + + // ═══════════════════════════════════════════════════════════════════════ + // SLIDE 4 — Call Desk & Agent UX + // ═══════════════════════════════════════════════════════════════════════ + { + const slide = pptx.addSlide(); + slide.background = { color: C.bg }; + addAccentBar(slide, C.accent3); + + addLabel(slide, "User Experience", C.accent3, 0.5, 0.3); + + slide.addText([ + { text: "🖥 ", options: { fontSize: 22 } }, + { text: "Call Desk & Agent UX", options: { fontSize: 22, bold: true, color: C.text } }, + ], { x: 0.5, y: 0.65, w: 9, h: 0.5, fontFace: FONT.heading }); + + addCard(slide, { + x: 0.3, y: 1.35, w: 3.05, h: 2.6, + title: "Call Desk Redesign", titleColor: C.accent3, + items: [ + "2-panel layout with collapsible sidebar & inline AI", + "Collapsible context panel, worklist/calls tabs", + "Pinned header & chat input, numpad dialler", + "Ringtone support for incoming calls", + ], + }); + + addCard(slide, { + x: 3.55, y: 1.35, w: 3.05, h: 2.6, + title: "Post-Call Workflow", titleColor: C.accent3, + items: [ + "Disposition → appointment booking → follow-up", + "Disposition returns straight to worklist", + "Send disposition to sidecar with UCID for ACW", + "Enquiry in post-call, appointment skip button", + ], + }); + + addCard(slide, { + x: 6.8, y: 1.35, w: 2.9, h: 2.6, + title: "UI Polish", titleColor: C.accent3, + items: [ + "FontAwesome Pro Duotone icon migration", + "Tooltips, sticky headers, roles, search", + "Fix React error #520 in prod tables", + "AI scroll containment, brand tokens refresh", + ], + }); + + addSlideNumber(slide, 4, TOTAL); + } + + // ═══════════════════════════════════════════════════════════════════════ + // SLIDE 5 — Features Shipped + // ═══════════════════════════════════════════════════════════════════════ + { + const slide = pptx.addSlide(); + slide.background = { color: C.bg }; + addAccentBar(slide, C.accent4); + + addLabel(slide, "Features Shipped", C.accent4, 0.5, 0.3); + + slide.addText([ + { text: "🚀 ", options: { fontSize: 22 } }, + { text: "Major Features", options: { fontSize: 22, bold: true, color: C.text } }, + ], { x: 0.5, y: 0.65, w: 9, h: 0.5, fontFace: FONT.heading }); + + addCard(slide, { + x: 0.3, y: 1.35, w: 4.5, h: 2.0, + title: "Supervisor Module", titleColor: C.accent4, + items: [ + "Team performance analytics page", + "Live monitor with active calls visibility", + "Master data management pages", + "Server: team perf + active calls endpoints", + ], + }); + + addCard(slide, { + x: 5.2, y: 1.35, w: 4.5, h: 2.0, + title: "Missed Call Queue (Phase 2)", titleColor: C.accent4, + items: [ + "Missed call queue ingestion & worklist", + "Auto-assignment engine for agents", + "Login redesign with role-based routing", + "Lead lookup for missed callers", + ], + }); + + addCard(slide, { + x: 0.3, y: 3.55, w: 4.5, h: 1.8, + title: "Agent Features (Phase 1)", titleColor: C.accent4, + items: [ + "Agent status toggle (Ready / Not Ready / Break)", + "Global search across patients, leads, calls", + "Enquiry form for new patient intake", + "My Performance page + logout modal", + ], + }); + + addCard(slide, { + x: 5.2, y: 3.55, w: 4.5, h: 1.8, + title: "Recording Analysis", titleColor: C.accent4, + items: [ + "Deepgram diarization + AI insights", + "Redis caching layer for analysis results", + "Full-stack: frontend player + server module", + ], + }); + + addSlideNumber(slide, 5, TOTAL); + } + + // ═══════════════════════════════════════════════════════════════════════ + // SLIDE 6 — Backend & Data + // ═══════════════════════════════════════════════════════════════════════ + { + const slide = pptx.addSlide(); + slide.background = { color: C.bg }; + addAccentBar(slide, C.accent2); + + addLabel(slide, "Backend & Data", C.accent2, 0.5, 0.3); + + slide.addText([ + { text: "⚙ ", options: { fontSize: 22 } }, + { text: "Backend & Data Layer", options: { fontSize: 22, bold: true, color: C.text } }, + ], { x: 0.5, y: 0.65, w: 9, h: 0.5, fontFace: FONT.heading }); + + addCard(slide, { + x: 0.3, y: 1.35, w: 4.5, h: 2.0, + title: "Platform Data Wiring", titleColor: C.accent2, + items: [ + "Migrated frontend to Jotai + Vercel AI SDK", + "Corrected all 7 GraphQL queries (fields, LINKS/PHONES)", + "Webhook handler for Ozonetel call records", + "Complete seeder: 5 doctors, appointments linked", + ], + }); + + addCard(slide, { + x: 5.2, y: 1.35, w: 4.5, h: 2.0, + title: "Server Endpoints", titleColor: C.accent2, + items: [ + "Call control, recording, CDR, missed calls, live assist", + "Agent summary, AHT, performance aggregation", + "Token refresh endpoint for auto-renewal", + "Search module with full-text capabilities", + ], + }); + + addCard(slide, { + x: 0.3, y: 3.55, w: 4.5, h: 1.8, + title: "Data Pages Built", titleColor: C.accent2, + items: [ + "Worklist table, call history, patients, dashboard", + "Reports, team dashboard, campaigns, settings", + "Agent detail page, campaign edit slideout", + "Appointments page with data refresh on login", + ], + }); + + addCard(slide, { + x: 5.2, y: 3.55, w: 4.5, h: 1.8, + title: "SDK App", titleColor: C.accent3, badge: "FortyTwoApps", + items: [ + "Helix Engage SDK app entity definitions", + "Call center CRM object model for platform", + "Foundation for platform-native data integration", + ], + }); + + addSlideNumber(slide, 6, TOTAL); + } + + // ═══════════════════════════════════════════════════════════════════════ + // SLIDE 7 — Deployment & Ops + // ═══════════════════════════════════════════════════════════════════════ + { + const slide = pptx.addSlide(); + slide.background = { color: C.bg }; + addAccentBar(slide, C.accent5); + + addLabel(slide, "Operations", C.accent5, 0.5, 0.3); + + slide.addText([ + { text: "🛠 ", options: { fontSize: 22 } }, + { text: "Deployment & DevOps", options: { fontSize: 22, bold: true, color: C.text } }, + ], { x: 0.5, y: 0.65, w: 9, h: 0.5, fontFace: FONT.heading }); + + addCard(slide, { + x: 0.3, y: 1.35, w: 3.05, h: 2.2, + title: "Deployment", titleColor: C.accent5, + items: [ + "Deployed to Hostinger VPS with Docker", + "Switched to global_healthx Ozonetel account", + "Dockerfile for server-side containerization", + ], + }); + + addCard(slide, { + x: 3.55, y: 1.35, w: 3.05, h: 2.2, + title: "AI & Testing", titleColor: C.accent5, + items: [ + "Migrated AI to Vercel AI SDK + OpenAI provider", + "AI flow test script — validates full pipeline", + "Live call assist integration", + ], + }); + + addCard(slide, { + x: 6.8, y: 1.35, w: 2.9, h: 2.2, + title: "Documentation", titleColor: C.accent5, + items: [ + "Team onboarding README with arch guide", + "Supervisor module spec + plan", + "Multi-agent spec + plan", + "Next session plans in commits", + ], + }); + + addSlideNumber(slide, 7, TOTAL); + } + + // ═══════════════════════════════════════════════════════════════════════ + // SLIDE 8 — Timeline + // ═══════════════════════════════════════════════════════════════════════ + { + const slide = pptx.addSlide(); + slide.background = { color: C.bg }; + addAccentBar(slide, C.accent6); + + addLabel(slide, "Day by Day", C.accent6, 0.5, 0.3); + + slide.addText([ + { text: "📅 ", options: { fontSize: 22 } }, + { text: "Development Timeline", options: { fontSize: 22, bold: true, color: C.text } }, + ], { x: 0.5, y: 0.65, w: 9, h: 0.5, fontFace: FONT.heading }); + + const timeline = [ + { date: "MAR 18 (Tue)", title: "Foundation Day", desc: "Call desk redesign, Jotai + AI SDK migration, seeder, AI flow test, VPS deploy" }, + { date: "MAR 19 (Wed)", title: "Data Layer Sprint", desc: "All data pages, post-call workflow, GraphQL fixes, Kookoo IVR, outbound UI" }, + { date: "MAR 20 (Thu)", title: "Telephony Breakthrough", desc: "Direct SIP replacing Kookoo, UCID tracking, Force Ready, Set Disposition" }, + { date: "MAR 21 (Fri)", title: "Agent Experience", desc: "Phase 1: status toggle, search, enquiry form, My Performance, FA icons, AHT" }, + { date: "MAR 23 (Sun)", title: "Scale & Reliability", desc: "Phase 2: missed call queue, auto-assign, Redis lockout, Patient 360, SDK defs" }, + { date: "MAR 24 (Mon)", title: "Supervisor Module", desc: "Team perf, live monitor, master data, SSE, UUID fix, maintenance, QA sweep" }, + { date: "MAR 25 (Tue)", title: "Intelligence Layer", desc: "Deepgram diarization, AI insights, SIP via Agent entity, token refresh, network" }, + ]; + + // Vertical line + slide.addShape("rect", { + x: 1.4, y: 1.3, w: 0.025, h: 4.0, + fill: { color: C.accent6, transparency: 60 }, + }); + + timeline.forEach((entry, i) => { + const y = 1.3 + i * 0.56; + + // Dot + slide.addShape("ellipse", { + x: 1.32, y: y + 0.08, w: 0.18, h: 0.18, + fill: { color: C.accent6 }, + line: { color: C.bg, width: 2 }, + }); + + // Date + slide.addText(entry.date, { + x: 1.7, y: y, w: 1.6, h: 0.22, + fontSize: 7, fontFace: FONT.heading, bold: true, + color: C.accent6, + }); + + // Title + slide.addText(entry.title, { + x: 3.3, y: y, w: 2.0, h: 0.22, + fontSize: 9, fontFace: FONT.heading, bold: true, + color: C.text, + }); + + // Description + slide.addText(entry.desc, { + x: 5.3, y: y, w: 4.2, h: 0.45, + fontSize: 8, fontFace: FONT.body, + color: C.textSec, + valign: "top", + }); + }); + + addSlideNumber(slide, 8, TOTAL); + } + + // ═══════════════════════════════════════════════════════════════════════ + // SLIDE 9 — Closing + // ═══════════════════════════════════════════════════════════════════════ + { + const slide = pptx.addSlide(); + slide.background = { color: C.bg }; + addAccentBar(slide, C.accent3); + + // Big headline + slide.addText("78 commits. 8 days. Ship mode.", { + x: 0.5, y: 1.4, w: 9, h: 0.8, + fontSize: 32, fontFace: FONT.heading, bold: true, + color: C.accent3, align: "center", + }); + + // Ship emoji + slide.addText("🚢", { + x: 4.2, y: 2.3, w: 1.6, h: 0.6, + fontSize: 28, align: "center", + }); + + // Description + slide.addText( + "From browser-native SIP calling to AI-powered recording analysis — Helix Engage is becoming a production contact center platform.", + { + x: 1.5, y: 3.0, w: 7, h: 0.6, + fontSize: 11, fontFace: FONT.body, + color: C.textSec, align: "center", + lineSpacingMultiple: 1.3, + } + ); + + // Achievement pills + const achievements = [ + { text: "SIP Calling ✓", color: C.accent1 }, + { text: "Multi-Agent ✓", color: C.accent2 }, + { text: "Supervisor ✓", color: C.accent3 }, + { text: "AI Copilot ✓", color: C.accent4 }, + { text: "Recording Analysis ✓", color: C.accent5 }, + ]; + + achievements.forEach((a, i) => { + const x = 0.8 + i * 1.8; + slide.addShape("roundRect", { + x, y: 3.9, w: 1.6, h: 0.35, + fill: { color: C.bgCard }, + line: { color: a.color, width: 1 }, + rectRadius: 0.17, + }); + slide.addText(a.text, { + x, y: 3.9, w: 1.6, h: 0.35, + fontSize: 8, fontFace: FONT.heading, bold: true, + color: a.color, align: "center", valign: "middle", + }); + }); + + // Author + slide.addText("Satya Suman Sari · FortyTwo Platform", { + x: 2, y: 4.7, w: 6, h: 0.3, + fontSize: 9, fontFace: FONT.body, + color: C.textMuted, align: "center", + }); + + addSlideNumber(slide, 9, TOTAL); + } + + // ── Save ────────────────────────────────────────────────────────────── + const outPath = "weekly-update-mar18-25.pptx"; + await pptx.writeFile({ fileName: outPath }); + console.log(`✅ Presentation saved: ${outPath}`); +} + +build().catch(err => { + console.error("❌ Failed:", err.message); + process.exit(1); +}); diff --git a/docs/weekly-update-mar18-25.html b/docs/weekly-update-mar18-25.html new file mode 100644 index 0000000..b9cc274 --- /dev/null +++ b/docs/weekly-update-mar18-25.html @@ -0,0 +1,886 @@ + + + + + + Helix Engage — Weekly Update (Mar 18–25, 2026) + + + + + + +
+ + + + + +
+ + +
+ or Space to navigate +
+ + +
+
+ Weekly Engineering Update +
+

Helix Engage

+

Contact Center CRM · Real-time Telephony · AI Copilot

+

March 18 – 25, 2026

+
+ + +
+
+ At a Glance +
+

Week in Numbers

+
+
+
0
+
Total Commits
+
+
+
0
+
Repositories
+
+
+
0
+
Days Active
+
+
+
0
+
Frontend Commits
+
+
+
+ helix-engage 50 + helix-engage-server 27 + FortyTwoApps/SDK 1 +
+
+ + +
+
+
+
📞
+ Core Infrastructure +
+
+

Telephony & SIP Overhaul

+
+
+

Outbound Calling Frontend

+
    +
  • Direct SIP call from browser — no Kookoo bridge needed
  • +
  • Immediate call card UI with auto-answer SIP bridge
  • +
  • End Call label fix, force active state after auto-answer
  • +
  • Reset outboundPending on call end to prevent inbound poisoning
  • +
+
+
+

Ozonetel Integration Server

+
    +
  • Ozonetel V3 dial endpoint + webhook handler for call events
  • +
  • Kookoo IVR outbound bridging (deprecated → direct SIP)
  • +
  • Set Disposition API for ACW release
  • +
  • Force Ready endpoint for agent state management
  • +
  • Token: 10-min cache, 401 invalidation, refresh on login
  • +
+
+
+

SIP & Agent State Frontend

+
    +
  • SIP driven by Agent entity with token refresh
  • +
  • Dynamic SIP from agentConfig, logout cleanup, heartbeat
  • +
  • Centralised outbound dial into useSip().dialOutbound()
  • +
  • UCID tracking from SIP headers for Ozonetel disposition
  • +
  • Network indicator for connection health
  • +
+
+
+

Multi-Agent & Sessions Server

+
    +
  • Multi-agent SIP with Redis session lockout
  • +
  • Strict duplicate login lockout — one device per agent
  • +
  • Session lock stores IP + timestamp for debugging
  • +
  • SSE agent state broadcast for real-time supervisor view
  • +
+
+
+
+ + +
+
+
+
🖥️
+ User Experience +
+
+

Call Desk & Agent UX

+
+
+

Call Desk Redesign

+
    +
  • 2-panel layout with collapsible sidebar & inline AI
  • +
  • Collapsible context panel, worklist/calls tabs, phone numbers
  • +
  • Pinned header & chat input, numpad dialler
  • +
  • Ringtone support for incoming calls
  • +
+
+
+

Post-Call Workflow

+
    +
  • Disposition → appointment booking → follow-up creation
  • +
  • Disposition returns straight to worklist — no intermediate screens
  • +
  • Send disposition to sidecar with UCID for Ozonetel ACW
  • +
  • Enquiry in post-call, appointment skip button
  • +
+
+
+

UI Polish

+
    +
  • FontAwesome Pro Duotone icon migration (all icons)
  • +
  • Tooltips, sticky headers, roles, search, AI prompts
  • +
  • Fix React error #520 (isRowHeader) in production tables
  • +
  • AI scroll containment, brand tokens refresh
  • +
+
+
+
+ + +
+
+
+
🚀
+ Features Shipped +
+
+

Major Features

+
+
+

Supervisor Module

+
    +
  • Team performance analytics page
  • +
  • Live monitor with active calls visibility
  • +
  • Master data management pages
  • +
  • Server: team perf + active calls endpoints
  • +
+
+
+

Missed Call Queue (Phase 2)

+
    +
  • Missed call queue ingestion & worklist
  • +
  • Auto-assignment engine for agents
  • +
  • Login redesign with role-based routing
  • +
  • Lead lookup for missed callers
  • +
+
+
+

Agent Features (Phase 1)

+
    +
  • Agent status toggle (Ready / Not Ready / Break)
  • +
  • Global search across patients, leads, calls
  • +
  • Enquiry form for new patient intake
  • +
  • My Performance page + logout modal
  • +
+
+
+

Recording Analysis

+
    +
  • Deepgram diarization + AI insights
  • +
  • Redis caching layer for analysis results
  • +
  • Full-stack: frontend player + server module
  • +
+
+
+
+ + +
+
+
+
⚙️
+ Backend & Data +
+
+

Backend & Data Layer

+
+
+

Platform Data Wiring

+
    +
  • Migrated frontend to Jotai + Vercel AI SDK
  • +
  • Corrected all 7 GraphQL queries (field names, LINKS/PHONES)
  • +
  • Webhook handler for Ozonetel call records
  • +
  • Complete seeder: 5 doctors, appointments linked, agent names match
  • +
+
+
+

Server Endpoints

+
    +
  • Call control, recording, CDR, missed calls, live call assist
  • +
  • Agent summary, AHT, performance aggregation
  • +
  • Token refresh endpoint for auto-renewal
  • +
  • Search module with full-text capabilities
  • +
+
+
+

Data Pages Built

+
    +
  • Worklist table, call history, patients, dashboard
  • +
  • Reports, team dashboard, campaigns, settings
  • +
  • Agent detail page, campaign edit slideout
  • +
  • Appointments page with data refresh on login
  • +
+
+
+

SDK App FortyTwoApps

+
    +
  • Helix Engage SDK app entity definitions
  • +
  • Call center CRM object model for Fortytwo platform
  • +
  • Foundation for platform-native data integration
  • +
+
+
+
+ + +
+
+
+
🛠️
+ Operations +
+
+

Deployment & DevOps

+
+
+

Deployment

+
    +
  • Deployed to Hostinger VPS with Docker
  • +
  • Switched to global_healthx Ozonetel account
  • +
  • Dockerfile for server-side containerization
  • +
+
+
+

AI & Testing

+
    +
  • Migrated AI to Vercel AI SDK + OpenAI provider
  • +
  • AI flow test script — validates auth, lead, patient, doctor, appointments
  • +
  • Live call assist integration
  • +
+
+
+

Documentation

+
    +
  • Team onboarding README with architecture guide
  • +
  • Supervisor module spec + implementation plan
  • +
  • Multi-agent spec + plan
  • +
  • Next session plans documented in commits
  • +
+
+
+
+ + +
+
+
+
📅
+ Day by Day +
+
+

Development Timeline

+
+
+
MAR 18 (Tue)
+
Foundation Day
+
Call desk redesign, Jotai + Vercel AI SDK migration, seeder with 5 doctors + linked appointments, AI flow test script, deployed to VPS
+
+
+
MAR 19 (Wed)
+
Data Layer Sprint
+
All data pages built (worklist, call history, patients, dashboard, reports), post-call workflow (disposition → booking), GraphQL fixes, Kookoo IVR outbound, outbound call UI
+
+
+
MAR 20 (Thu)
+
Telephony Breakthrough
+
Direct SIP call from browser replacing Kookoo bridge, UCID tracking, Force Ready, Ozonetel Set Disposition, telephony overhaul
+
+
+
MAR 21 (Fri)
+
Agent Experience
+
Phase 1 shipped — agent status toggle, global search, enquiry form, My Performance page, full FontAwesome icon migration, agent summary/AHT endpoints
+
+
+
MAR 23 (Sun)
+
Scale & Reliability
+
Phase 2 — missed call queue + auto-assignment, multi-agent SIP with Redis lockout, duplicate login prevention, Patient 360 rewrite, onboarding docs, SDK entity defs
+
+
+
MAR 24 (Mon)
+
Supervisor Module
+
Supervisor module with team performance + live monitor + master data, SSE agent state, UUID fix, maintenance module, QA bug sweep, supervisor endpoints
+
+
+
MAR 25 (Tue)
+
Intelligence Layer
+
Call recording analysis with Deepgram diarization + AI insights, SIP driven by Agent entity, token refresh, network indicator
+
+
+
+ + +
+

78 commits. 8 days. Ship mode. 🚢

+

+ From browser-native SIP calling to AI-powered recording analysis — Helix Engage is becoming a production contact center platform. +

+
+ SIP Calling ✓ + Multi-Agent ✓ + Supervisor Module ✓ + AI Copilot ✓ + Recording Analysis ✓ +
+

Satya Suman Sari · FortyTwo Platform

+
+ + + + + diff --git a/docs/weekly-update-mar18-25.pptx b/docs/weekly-update-mar18-25.pptx new file mode 100644 index 0000000000000000000000000000000000000000..d61bc12f03c7dc03bb25db50e9874d2f982fdc21 GIT binary patch literal 218927 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

RThK-t({i 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 ( +

+
+ {count} +
+
+); diff --git a/src/components/base/select/select-shared.tsx b/src/components/base/select/select-shared.tsx new file mode 100644 index 0000000..0163b47 --- /dev/null +++ b/src/components/base/select/select-shared.tsx @@ -0,0 +1,49 @@ +import type { FC, ReactNode } from "react"; +import { createContext } from "react"; + +export type SelectItemType = { + /** Unique identifier for the item. */ + id: string | number; + /** The primary display text. */ + label?: string; + /** Avatar image URL. */ + avatarUrl?: string; + /** Whether the item is disabled. */ + isDisabled?: boolean; + /** Secondary text displayed alongside the label. */ + supportingText?: string; + /** Leading icon component or element. */ + icon?: FC | ReactNode; +}; + +export interface CommonProps { + /** Helper text displayed below the input. */ + hint?: string; + /** Field label displayed above the input. */ + label?: string; + /** Tooltip text for the help icon next to the label. */ + tooltip?: string; + /** + * The size of the component. + * @default "md" + */ + size?: "sm" | "md" | "lg"; + /** Placeholder text when no value is selected. */ + placeholder?: string; + /** Whether to hide the required indicator from the label. */ + hideRequiredIndicator?: boolean; +} + +export const sizes = { + sm: { + root: "py-2 pl-3 pr-2.5 gap-2 *:data-icon:size-4 *:data-icon:stroke-[2.25px]", + withIcon: "", + text: "text-sm", + textContainer: "gap-x-1.5", + shortcut: "pr-2.5", + }, + md: { root: "py-2 px-3 gap-2 *:data-icon:size-5", withIcon: "", text: "text-md", textContainer: "gap-x-1.5", shortcut: "pr-2.5" }, + lg: { root: "py-2.5 px-3.5 gap-2 *:data-icon:size-5", withIcon: "", text: "text-md", textContainer: "gap-x-1.5", shortcut: "pr-3" }, +}; + +export const SelectContext = createContext<{ size: "sm" | "md" | "lg" }>({ size: "md" }); diff --git a/src/components/call-desk/context-panel.tsx b/src/components/call-desk/context-panel.tsx index 0b2ec26..ef09508 100644 --- a/src/components/call-desk/context-panel.tsx +++ b/src/components/call-desk/context-panel.tsx @@ -1,18 +1,15 @@ import { useEffect, useState } from 'react'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { faSparkles, faUser, faCalendarCheck } from '@fortawesome/pro-duotone-svg-icons'; +import { faSparkles, faCalendarCheck, faPhone, faUser } from '@fortawesome/pro-duotone-svg-icons'; import { faIcon } from '@/lib/icon-wrapper'; import { AiChatPanel } from './ai-chat-panel'; import { Badge } from '@/components/base/badges/badges'; import { apiClient } from '@/lib/api-client'; import { formatPhone, formatShortDate } from '@/lib/format'; -import { cx } from '@/utils/cx'; import type { Lead, LeadActivity } from '@/types/entities'; const CalendarCheck = faIcon(faCalendarCheck); -type ContextTab = 'ai' | 'lead360'; - interface ContextPanelProps { selectedLead: Lead | null; activities: LeadActivity[]; @@ -21,74 +18,13 @@ interface ContextPanelProps { callUcid?: string | null; } -export const ContextPanel = ({ selectedLead, activities, callerPhone }: ContextPanelProps) => { - const [activeTab, setActiveTab] = useState('ai'); - - // Auto-switch to lead 360 when a lead is selected - useEffect(() => { - if (selectedLead) { - setActiveTab('lead360'); - } - }, [selectedLead?.id]); - - const callerContext = selectedLead ? { - callerPhone: selectedLead.contactPhone?.[0]?.number ?? callerPhone, - leadId: selectedLead.id, - leadName: `${selectedLead.contactName?.firstName ?? ''} ${selectedLead.contactName?.lastName ?? ''}`.trim(), - } : callerPhone ? { callerPhone } : undefined; - - return ( -
- {/* Tab bar */} -
- - -
- - {/* Tab content */} - {activeTab === 'ai' && ( -
- -
- )} - {activeTab === 'lead360' && ( -
- -
- )} -
- ); -}; - -const Lead360Tab = ({ lead, activities }: { lead: Lead | null; activities: LeadActivity[] }) => { +export const ContextPanel = ({ selectedLead, activities, callerPhone, isInCall }: ContextPanelProps) => { const [patientData, setPatientData] = useState(null); const [loadingPatient, setLoadingPatient] = useState(false); - // Fetch patient data when lead has a patientId (returning patient) + // Fetch patient data when lead has a patientId useEffect(() => { - const patientId = (lead as any)?.patientId; + const patientId = (selectedLead as any)?.patientId; if (!patientId) { setPatientData(null); return; @@ -97,10 +33,10 @@ const Lead360Tab = ({ lead, activities }: { lead: Lead | null; activities: LeadA setLoadingPatient(true); apiClient.graphql<{ patients: { edges: Array<{ node: any }> } }>( `query GetPatient($id: UUID!) { patients(filter: { id: { eq: $id } }) { edges { node { - id fullName { firstName lastName } dateOfBirth gender patientType + id fullName { firstName lastName } dateOfBirth gender phones { primaryPhoneNumber } emails { primaryEmail } appointments(first: 5, orderBy: [{ scheduledAt: DescNullsLast }]) { edges { node { - id scheduledAt status doctorName department reasonForVisit appointmentType + id scheduledAt status doctorName department reasonForVisit } } } calls(first: 5, orderBy: [{ startedAt: DescNullsLast }]) { edges { node { id callStatus disposition direction startedAt durationSec agentName @@ -112,153 +48,137 @@ const Lead360Tab = ({ lead, activities }: { lead: Lead | null; activities: LeadA setPatientData(data.patients.edges[0]?.node ?? null); }).catch(() => setPatientData(null)) .finally(() => setLoadingPatient(false)); - }, [(lead as any)?.patientId]); + }, [(selectedLead as any)?.patientId]); - if (!lead) { - return ( -
- -

Select a lead from the worklist to see their full profile.

-
- ); - } - - const firstName = lead.contactName?.firstName ?? ''; - const lastName = lead.contactName?.lastName ?? ''; - const fullName = `${firstName} ${lastName}`.trim() || 'Unknown'; - const phone = lead.contactPhone?.[0]; - const email = lead.contactEmail?.[0]?.address; + const lead = selectedLead; + const firstName = lead?.contactName?.firstName ?? ''; + const lastName = lead?.contactName?.lastName ?? ''; + const fullName = `${firstName} ${lastName}`.trim(); + const phone = lead?.contactPhone?.[0]; + const email = lead?.contactEmail?.[0]?.address; const leadActivities = activities - .filter((a) => a.leadId === lead.id) + .filter((a) => lead && a.leadId === lead.id) .sort((a, b) => new Date(b.occurredAt ?? b.createdAt ?? '').getTime() - new Date(a.occurredAt ?? a.createdAt ?? '').getTime()) .slice(0, 10); - const isReturning = !!patientData; const appointments = patientData?.appointments?.edges?.map((e: any) => e.node) ?? []; - const patientCalls = patientData?.calls?.edges?.map((e: any) => e.node) ?? []; - const patientAge = patientData?.dateOfBirth - ? Math.floor((Date.now() - new Date(patientData.dateOfBirth).getTime()) / (365.25 * 24 * 60 * 60 * 1000)) - : null; - const patientGender = patientData?.gender === 'MALE' ? 'M' : patientData?.gender === 'FEMALE' ? 'F' : null; + const callerContext = lead ? { + callerPhone: phone?.number ?? callerPhone, + leadId: lead.id, + leadName: fullName, + } : callerPhone ? { callerPhone } : undefined; return ( -
- {/* Profile */} -
-

{fullName}

- {phone &&

{formatPhone(phone)}

} - {email &&

{email}

} -
- {isReturning && ( - Returning Patient +
+ {/* Context header — shows caller/lead info when available */} + {lead && ( +
+ {/* Call status banner */} + {isInCall && ( +
+ + + On call with {fullName || callerPhone || 'Unknown'} + +
)} - {patientAge !== null && patientGender && ( - {patientAge}y · {patientGender} - )} - {lead.leadStatus && {lead.leadStatus}} - {lead.leadSource && {lead.leadSource}} - {lead.priority && lead.priority !== 'NORMAL' && ( - {lead.priority} - )} -
- {lead.interestedService && ( -

Interested in: {lead.interestedService}

- )} -
- {/* Returning patient: Appointments */} - {loadingPatient && ( -

Loading patient details...

- )} - {isReturning && appointments.length > 0 && ( -
-

Appointments

-
- {appointments.map((appt: any) => { - const statusColors: Record = { - COMPLETED: 'success', SCHEDULED: 'brand', CONFIRMED: 'brand', - CANCELLED: 'error', NO_SHOW: 'warning', - }; - return ( -
- -
-
- - {appt.doctorName ?? 'Doctor'} · {appt.department ?? ''} - - {appt.status && ( - - {appt.status.toLowerCase()} - - )} -
-

- {appt.scheduledAt ? formatShortDate(appt.scheduledAt) : ''} - {appt.reasonForVisit ? ` — ${appt.reasonForVisit}` : ''} -

+ {/* Lead profile */} +
+

{fullName || 'Unknown'}

+ {phone &&

{formatPhone(phone)}

} + {email &&

{email}

} +
+ + {/* Status badges */} +
+ {!!patientData && ( + Returning Patient + )} + {lead.leadStatus && {lead.leadStatus.replace(/_/g, ' ')}} + {lead.leadSource && {lead.leadSource.replace(/_/g, ' ')}} +
+ + {lead.interestedService && ( +

Interested in: {lead.interestedService}

+ )} + + {/* AI Insight — live from platform */} + {(lead.aiSummary || lead.aiSuggestedAction) && ( +
+
+ + AI Insight +
+ {lead.aiSummary &&

{lead.aiSummary}

} + {lead.aiSuggestedAction && ( +

{lead.aiSuggestedAction}

+ )} +
+ )} + + {/* Upcoming appointments */} + {appointments.length > 0 && ( +
+ Appointments +
+ {appointments.slice(0, 3).map((appt: any) => ( +
+ + + {appt.doctorName ?? 'Doctor'} · {appt.scheduledAt ? formatShortDate(appt.scheduledAt) : ''} + + {appt.status && ( + + {appt.status.toLowerCase()} + + )}
-
- ); - })} -
-
- )} - - {/* Returning patient: Recent calls */} - {isReturning && patientCalls.length > 0 && ( -
-

Recent Calls

-
- {patientCalls.map((call: any) => ( -
-
- - {call.direction === 'INBOUND' ? 'Inbound' : 'Outbound'} - {call.disposition ? ` — ${call.disposition.replace(/_/g, ' ').toLowerCase()}` : ''} - - {call.startedAt ? formatShortDate(call.startedAt) : ''} + ))}
- ))} -
-
- )} +
+ )} - {/* AI Insight */} - {(lead.aiSummary || lead.aiSuggestedAction) && ( -
-
- - AI Insight -
- {lead.aiSummary &&

{lead.aiSummary}

} - {lead.aiSuggestedAction && ( -

{lead.aiSuggestedAction}

+ {loadingPatient &&

Loading patient details...

} + + {/* Recent activity */} + {leadActivities.length > 0 && ( +
+ Recent Activity +
+ {leadActivities.slice(0, 5).map((a) => ( +
+
+
+

{a.summary}

+

+ {a.activityType?.replace(/_/g, ' ')}{a.occurredAt ? ` · ${formatShortDate(a.occurredAt)}` : ''} +

+
+
+ ))} +
+
)}
)} - {/* Activity timeline */} - {leadActivities.length > 0 && ( -
-

Activity

-
- {leadActivities.map((a) => ( -
-
-
-

{a.summary}

-

- {a.activityType}{a.occurredAt ? ` · ${formatShortDate(a.occurredAt)}` : ''} -

-
-
- ))} + {/* No lead selected — empty state */} + {!lead && ( +
+
+ +

Select a lead from the worklist to see context

)} + + {/* AI Chat — always available at the bottom */} +
+ +
); };