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 0000000..d61bc12 Binary files /dev/null and b/docs/weekly-update-mar18-25.pptx differ diff --git a/src/components/base/avatar/base-components/avatar-count.tsx b/src/components/base/avatar/base-components/avatar-count.tsx new file mode 100644 index 0000000..b4fa214 --- /dev/null +++ b/src/components/base/avatar/base-components/avatar-count.tsx @@ -0,0 +1,14 @@ +import { cx } from "@/utils/cx"; + +interface AvatarCountProps { + count: number; + className?: string; +} + +export const AvatarCount = ({ count, className }: AvatarCountProps) => ( +
+
+ {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 */} +
+ +
); };