mirror of
https://dev.azure.com/globalhealthx/EMR/_git/helix-engage
synced 2026-04-11 10:23:27 +00:00
feat: unified context panel, remove tabs, context-aware layout
- 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) <noreply@anthropic.com>
This commit is contained in:
680
docs/generate-pptx.cjs
Normal file
680
docs/generate-pptx.cjs
Normal file
@@ -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);
|
||||
});
|
||||
680
docs/generate-pptx.js
Normal file
680
docs/generate-pptx.js
Normal file
@@ -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);
|
||||
});
|
||||
886
docs/weekly-update-mar18-25.html
Normal file
886
docs/weekly-update-mar18-25.html
Normal file
@@ -0,0 +1,886 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Helix Engage — Weekly Update (Mar 18–25, 2026)</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
/* ===========================================
|
||||
CSS CUSTOM PROPERTIES (DARK EXECUTIVE THEME)
|
||||
=========================================== */
|
||||
:root {
|
||||
--bg-primary: #0b0e17;
|
||||
--bg-secondary: #111827;
|
||||
--bg-card: rgba(255,255,255,0.04);
|
||||
--bg-card-hover: rgba(255,255,255,0.07);
|
||||
--text-primary: #f0f2f5;
|
||||
--text-secondary: #8892a4;
|
||||
--text-muted: #4b5563;
|
||||
--accent-cyan: #22d3ee;
|
||||
--accent-violet: #a78bfa;
|
||||
--accent-emerald: #34d399;
|
||||
--accent-amber: #fbbf24;
|
||||
--accent-rose: #fb7185;
|
||||
--accent-blue: #60a5fa;
|
||||
--glow-cyan: rgba(34,211,238,0.15);
|
||||
--glow-violet: rgba(167,139,250,0.15);
|
||||
--glow-emerald: rgba(52,211,153,0.15);
|
||||
--font-display: 'Space Grotesk', sans-serif;
|
||||
--font-body: 'DM Sans', sans-serif;
|
||||
--slide-padding: clamp(2rem, 6vw, 5rem);
|
||||
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
--duration: 0.7s;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
BASE RESET
|
||||
=========================================== */
|
||||
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
html { scroll-behavior: smooth; scroll-snap-type: y mandatory; }
|
||||
body {
|
||||
font-family: var(--font-body);
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
overflow-x: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
SLIDE CONTAINER
|
||||
=========================================== */
|
||||
.slide {
|
||||
min-height: 100vh;
|
||||
padding: var(--slide-padding);
|
||||
scroll-snap-align: start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
PROGRESS BAR
|
||||
=========================================== */
|
||||
.progress-bar {
|
||||
position: fixed; top: 0; left: 0;
|
||||
height: 3px; width: 0%;
|
||||
background: linear-gradient(90deg, var(--accent-cyan), var(--accent-violet));
|
||||
z-index: 100;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
NAVIGATION DOTS
|
||||
=========================================== */
|
||||
.nav-dots {
|
||||
position: fixed; right: 1.5rem; top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: flex; flex-direction: column; gap: 10px;
|
||||
z-index: 100;
|
||||
}
|
||||
.nav-dot {
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--text-muted);
|
||||
border: none; cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.nav-dot.active {
|
||||
background: var(--accent-cyan);
|
||||
box-shadow: 0 0 12px var(--glow-cyan);
|
||||
transform: scale(1.3);
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
SLIDE COUNTER
|
||||
=========================================== */
|
||||
.slide-counter {
|
||||
position: fixed; bottom: 1.5rem; right: 2rem;
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
z-index: 100;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
REVEAL ANIMATIONS
|
||||
=========================================== */
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(35px);
|
||||
transition: opacity var(--duration) var(--ease-out-expo),
|
||||
transform var(--duration) var(--ease-out-expo);
|
||||
}
|
||||
.slide.visible .reveal { opacity: 1; transform: translateY(0); }
|
||||
.reveal:nth-child(1) { transition-delay: 0.08s; }
|
||||
.reveal:nth-child(2) { transition-delay: 0.16s; }
|
||||
.reveal:nth-child(3) { transition-delay: 0.24s; }
|
||||
.reveal:nth-child(4) { transition-delay: 0.32s; }
|
||||
.reveal:nth-child(5) { transition-delay: 0.40s; }
|
||||
.reveal:nth-child(6) { transition-delay: 0.48s; }
|
||||
.reveal:nth-child(7) { transition-delay: 0.56s; }
|
||||
.reveal:nth-child(8) { transition-delay: 0.64s; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.reveal { transition: opacity 0.3s ease; transform: none; }
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
TYPOGRAPHY
|
||||
=========================================== */
|
||||
h1 { font-family: var(--font-display); font-weight: 700; }
|
||||
h2 { font-family: var(--font-display); font-weight: 600; font-size: clamp(1.6rem, 4vw, 2.5rem); margin-bottom: 0.5em; }
|
||||
h3 { font-family: var(--font-display); font-weight: 500; font-size: 1.1rem; }
|
||||
p, li { line-height: 1.65; }
|
||||
|
||||
.label {
|
||||
display: inline-block;
|
||||
font-family: var(--font-display);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.15em;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
padding: 0.3em 0.9em;
|
||||
border-radius: 100px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
TITLE SLIDE
|
||||
=========================================== */
|
||||
.title-slide {
|
||||
text-align: center;
|
||||
background:
|
||||
radial-gradient(ellipse at 30% 70%, rgba(34,211,238,0.08) 0%, transparent 50%),
|
||||
radial-gradient(ellipse at 70% 30%, rgba(167,139,250,0.08) 0%, transparent 50%),
|
||||
var(--bg-primary);
|
||||
}
|
||||
.title-slide h1 {
|
||||
font-size: clamp(2.5rem, 6vw, 4.5rem);
|
||||
background: linear-gradient(135deg, var(--accent-cyan) 0%, var(--accent-violet) 50%, var(--accent-emerald) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
line-height: 1.15;
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
.title-slide .subtitle {
|
||||
font-size: clamp(1rem, 2vw, 1.4rem);
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
.title-slide .date-range {
|
||||
font-family: var(--font-display);
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
STAT CARDS
|
||||
=========================================== */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 1.2rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.stat-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
border-radius: 16px;
|
||||
padding: 1.8rem 1.5rem;
|
||||
text-align: center;
|
||||
transition: all 0.4s var(--ease-out-expo);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.stat-card::after {
|
||||
content: '';
|
||||
position: absolute; top: 0; left: 0; right: 0;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent);
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
.stat-card:hover { background: var(--bg-card-hover); transform: translateY(-4px); }
|
||||
.stat-card:hover::after { opacity: 1; }
|
||||
.stat-number {
|
||||
font-family: var(--font-display);
|
||||
font-size: 3rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
margin-bottom: 0.3em;
|
||||
}
|
||||
.stat-number.cyan { color: var(--accent-cyan); }
|
||||
.stat-number.violet { color: var(--accent-violet); }
|
||||
.stat-number.emerald { color: var(--accent-emerald); }
|
||||
.stat-number.amber { color: var(--accent-amber); }
|
||||
.stat-label { color: var(--text-secondary); font-size: 0.85rem; }
|
||||
|
||||
/* ===========================================
|
||||
CONTENT CARDS
|
||||
=========================================== */
|
||||
.card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 1.2rem;
|
||||
margin-top: 1.2rem;
|
||||
}
|
||||
.content-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
border-radius: 14px;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s var(--ease-out-expo);
|
||||
}
|
||||
.content-card:hover { background: var(--bg-card-hover); border-color: rgba(255,255,255,0.1); }
|
||||
.content-card h3 { margin-bottom: 0.6rem; }
|
||||
.content-card ul {
|
||||
list-style: none; padding: 0;
|
||||
}
|
||||
.content-card li {
|
||||
position: relative;
|
||||
padding-left: 1.2em;
|
||||
margin-bottom: 0.45em;
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.content-card li::before {
|
||||
content: '›';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
color: var(--accent-cyan);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
TIMELINE
|
||||
=========================================== */
|
||||
.timeline {
|
||||
position: relative;
|
||||
padding-left: 2rem;
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
.timeline::before {
|
||||
content: '';
|
||||
position: absolute; left: 0; top: 0; bottom: 0;
|
||||
width: 2px;
|
||||
background: linear-gradient(to bottom, var(--accent-cyan), var(--accent-violet), var(--accent-emerald));
|
||||
opacity: 0.4;
|
||||
}
|
||||
.tl-item {
|
||||
position: relative;
|
||||
margin-bottom: 1.5rem;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
.tl-item::before {
|
||||
content: '';
|
||||
position: absolute; left: -2.35rem; top: 0.3em;
|
||||
width: 10px; height: 10px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent-cyan);
|
||||
border: 2px solid var(--bg-primary);
|
||||
}
|
||||
.tl-date {
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.75rem;
|
||||
color: var(--accent-cyan);
|
||||
letter-spacing: 0.08em;
|
||||
margin-bottom: 0.15em;
|
||||
}
|
||||
.tl-title {
|
||||
font-weight: 600;
|
||||
font-size: 1rem;
|
||||
margin-bottom: 0.15em;
|
||||
}
|
||||
.tl-desc {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
REPO BADGE
|
||||
=========================================== */
|
||||
.repo-badge {
|
||||
display: inline-block;
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.65rem;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
margin-left: 0.5em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.badge-frontend { background: rgba(34,211,238,0.15); color: var(--accent-cyan); }
|
||||
.badge-server { background: rgba(167,139,250,0.15); color: var(--accent-violet); }
|
||||
.badge-sdk { background: rgba(52,211,153,0.15); color: var(--accent-emerald); }
|
||||
|
||||
/* ===========================================
|
||||
PILL LIST
|
||||
=========================================== */
|
||||
.pill-list {
|
||||
display: flex; flex-wrap: wrap; gap: 0.5rem;
|
||||
margin-top: 0.8rem;
|
||||
}
|
||||
.pill {
|
||||
display: inline-block;
|
||||
font-size: 0.78rem;
|
||||
padding: 0.3em 0.9em;
|
||||
border-radius: 100px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid rgba(255,255,255,0.08);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
SECTION HEADER
|
||||
=========================================== */
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.7rem;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
.section-icon {
|
||||
width: 36px; height: 36px;
|
||||
border-radius: 10px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
KEYBOARD HINT
|
||||
=========================================== */
|
||||
.keyboard-hint {
|
||||
position: fixed; bottom: 1.5rem; left: 2rem;
|
||||
font-size: 0.75rem; color: var(--text-muted);
|
||||
z-index: 100;
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
opacity: 0;
|
||||
animation: hintFade 0.6s 2s forwards;
|
||||
}
|
||||
.key {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border: 1px solid var(--text-muted);
|
||||
border-radius: 4px;
|
||||
font-family: var(--font-display);
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
@keyframes hintFade { to { opacity: 1; } }
|
||||
|
||||
/* ===========================================
|
||||
CLOSING SLIDE
|
||||
=========================================== */
|
||||
.closing-slide {
|
||||
text-align: center;
|
||||
background:
|
||||
radial-gradient(ellipse at 50% 50%, rgba(34,211,238,0.06) 0%, transparent 60%),
|
||||
var(--bg-primary);
|
||||
}
|
||||
.closing-slide h2 {
|
||||
font-size: clamp(1.8rem, 4vw, 3rem);
|
||||
background: linear-gradient(135deg, var(--accent-emerald), var(--accent-cyan));
|
||||
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
|
||||
/* ===========================================
|
||||
RESPONSIVE
|
||||
=========================================== */
|
||||
@media (max-width: 768px) {
|
||||
.nav-dots, .keyboard-hint { display: none; }
|
||||
.stats-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.card-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<!-- Progress bar -->
|
||||
<div class="progress-bar" id="progressBar"></div>
|
||||
|
||||
<!-- Navigation dots -->
|
||||
<nav class="nav-dots" id="navDots"></nav>
|
||||
|
||||
<!-- Slide counter -->
|
||||
<div class="slide-counter" id="slideCounter"></div>
|
||||
|
||||
<!-- Keyboard hint -->
|
||||
<div class="keyboard-hint">
|
||||
<span class="key">↑</span><span class="key">↓</span> or <span class="key">Space</span> to navigate
|
||||
</div>
|
||||
|
||||
<!-- ======================================
|
||||
SLIDE 1: TITLE
|
||||
====================================== -->
|
||||
<section class="slide title-slide">
|
||||
<div class="reveal">
|
||||
<span class="label" style="background: var(--glow-cyan); color: var(--accent-cyan);">Weekly Engineering Update</span>
|
||||
</div>
|
||||
<h1 class="reveal">Helix Engage</h1>
|
||||
<p class="subtitle reveal">Contact Center CRM · Real-time Telephony · AI Copilot</p>
|
||||
<p class="date-range reveal">March 18 – 25, 2026</p>
|
||||
</section>
|
||||
|
||||
<!-- ======================================
|
||||
SLIDE 2: AT A GLANCE
|
||||
====================================== -->
|
||||
<section class="slide">
|
||||
<div class="reveal">
|
||||
<span class="label" style="background: var(--glow-violet); color: var(--accent-violet);">At a Glance</span>
|
||||
</div>
|
||||
<h2 class="reveal">Week in Numbers</h2>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card reveal">
|
||||
<div class="stat-number cyan" data-count="78">0</div>
|
||||
<div class="stat-label">Total Commits</div>
|
||||
</div>
|
||||
<div class="stat-card reveal">
|
||||
<div class="stat-number violet" data-count="3">0</div>
|
||||
<div class="stat-label">Repositories</div>
|
||||
</div>
|
||||
<div class="stat-card reveal">
|
||||
<div class="stat-number emerald" data-count="8">0</div>
|
||||
<div class="stat-label">Days Active</div>
|
||||
</div>
|
||||
<div class="stat-card reveal">
|
||||
<div class="stat-number amber" data-count="50">0</div>
|
||||
<div class="stat-label">Frontend Commits</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="pill-list reveal" style="margin-top:1.5rem; justify-content: center;">
|
||||
<span class="pill" style="border-color: rgba(34,211,238,0.3); color: var(--accent-cyan);">helix-engage <b>50</b></span>
|
||||
<span class="pill" style="border-color: rgba(167,139,250,0.3); color: var(--accent-violet);">helix-engage-server <b>27</b></span>
|
||||
<span class="pill" style="border-color: rgba(52,211,153,0.3); color: var(--accent-emerald);">FortyTwoApps/SDK <b>1</b></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ======================================
|
||||
SLIDE 3: TELEPHONY & SIP
|
||||
====================================== -->
|
||||
<section class="slide">
|
||||
<div class="reveal">
|
||||
<div class="section-header">
|
||||
<div class="section-icon" style="background: var(--glow-cyan);">📞</div>
|
||||
<span class="label" style="background: var(--glow-cyan); color: var(--accent-cyan);">Core Infrastructure</span>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="reveal">Telephony & SIP Overhaul</h2>
|
||||
<div class="card-grid">
|
||||
<div class="content-card reveal">
|
||||
<h3 style="color: var(--accent-cyan);">Outbound Calling <span class="repo-badge badge-frontend">Frontend</span></h3>
|
||||
<ul>
|
||||
<li>Direct SIP call from browser — no Kookoo bridge needed</li>
|
||||
<li>Immediate call card UI with auto-answer SIP bridge</li>
|
||||
<li>End Call label fix, force active state after auto-answer</li>
|
||||
<li>Reset outboundPending on call end to prevent inbound poisoning</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content-card reveal">
|
||||
<h3 style="color: var(--accent-violet);">Ozonetel Integration <span class="repo-badge badge-server">Server</span></h3>
|
||||
<ul>
|
||||
<li>Ozonetel V3 dial endpoint + webhook handler for call events</li>
|
||||
<li>Kookoo IVR outbound bridging (deprecated → direct SIP)</li>
|
||||
<li>Set Disposition API for ACW release</li>
|
||||
<li>Force Ready endpoint for agent state management</li>
|
||||
<li>Token: 10-min cache, 401 invalidation, refresh on login</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content-card reveal">
|
||||
<h3 style="color: var(--accent-cyan);">SIP & Agent State <span class="repo-badge badge-frontend">Frontend</span></h3>
|
||||
<ul>
|
||||
<li>SIP driven by Agent entity with token refresh</li>
|
||||
<li>Dynamic SIP from agentConfig, logout cleanup, heartbeat</li>
|
||||
<li>Centralised outbound dial into <code>useSip().dialOutbound()</code></li>
|
||||
<li>UCID tracking from SIP headers for Ozonetel disposition</li>
|
||||
<li>Network indicator for connection health</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content-card reveal">
|
||||
<h3 style="color: var(--accent-violet);">Multi-Agent & Sessions <span class="repo-badge badge-server">Server</span></h3>
|
||||
<ul>
|
||||
<li>Multi-agent SIP with Redis session lockout</li>
|
||||
<li>Strict duplicate login lockout — one device per agent</li>
|
||||
<li>Session lock stores IP + timestamp for debugging</li>
|
||||
<li>SSE agent state broadcast for real-time supervisor view</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ======================================
|
||||
SLIDE 4: CALL DESK & UX
|
||||
====================================== -->
|
||||
<section class="slide">
|
||||
<div class="reveal">
|
||||
<div class="section-header">
|
||||
<div class="section-icon" style="background: var(--glow-emerald);">🖥️</div>
|
||||
<span class="label" style="background: var(--glow-emerald); color: var(--accent-emerald);">User Experience</span>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="reveal">Call Desk & Agent UX</h2>
|
||||
<div class="card-grid">
|
||||
<div class="content-card reveal">
|
||||
<h3 style="color: var(--accent-emerald);">Call Desk Redesign</h3>
|
||||
<ul>
|
||||
<li>2-panel layout with collapsible sidebar & inline AI</li>
|
||||
<li>Collapsible context panel, worklist/calls tabs, phone numbers</li>
|
||||
<li>Pinned header & chat input, numpad dialler</li>
|
||||
<li>Ringtone support for incoming calls</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content-card reveal">
|
||||
<h3 style="color: var(--accent-emerald);">Post-Call Workflow</h3>
|
||||
<ul>
|
||||
<li>Disposition → appointment booking → follow-up creation</li>
|
||||
<li>Disposition returns straight to worklist — no intermediate screens</li>
|
||||
<li>Send disposition to sidecar with UCID for Ozonetel ACW</li>
|
||||
<li>Enquiry in post-call, appointment skip button</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content-card reveal">
|
||||
<h3 style="color: var(--accent-emerald);">UI Polish</h3>
|
||||
<ul>
|
||||
<li>FontAwesome Pro Duotone icon migration (all icons)</li>
|
||||
<li>Tooltips, sticky headers, roles, search, AI prompts</li>
|
||||
<li>Fix React error #520 (isRowHeader) in production tables</li>
|
||||
<li>AI scroll containment, brand tokens refresh</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ======================================
|
||||
SLIDE 5: FEATURES SHIPPED
|
||||
====================================== -->
|
||||
<section class="slide">
|
||||
<div class="reveal">
|
||||
<div class="section-header">
|
||||
<div class="section-icon" style="background: rgba(251,191,36,0.15);">🚀</div>
|
||||
<span class="label" style="background: rgba(251,191,36,0.15); color: var(--accent-amber);">Features Shipped</span>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="reveal">Major Features</h2>
|
||||
<div class="card-grid">
|
||||
<div class="content-card reveal">
|
||||
<h3 style="color: var(--accent-amber);">Supervisor Module</h3>
|
||||
<ul>
|
||||
<li>Team performance analytics page</li>
|
||||
<li>Live monitor with active calls visibility</li>
|
||||
<li>Master data management pages</li>
|
||||
<li>Server: team perf + active calls endpoints</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content-card reveal">
|
||||
<h3 style="color: var(--accent-amber);">Missed Call Queue (Phase 2)</h3>
|
||||
<ul>
|
||||
<li>Missed call queue ingestion & worklist</li>
|
||||
<li>Auto-assignment engine for agents</li>
|
||||
<li>Login redesign with role-based routing</li>
|
||||
<li>Lead lookup for missed callers</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content-card reveal">
|
||||
<h3 style="color: var(--accent-amber);">Agent Features (Phase 1)</h3>
|
||||
<ul>
|
||||
<li>Agent status toggle (Ready / Not Ready / Break)</li>
|
||||
<li>Global search across patients, leads, calls</li>
|
||||
<li>Enquiry form for new patient intake</li>
|
||||
<li>My Performance page + logout modal</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content-card reveal">
|
||||
<h3 style="color: var(--accent-amber);">Recording Analysis</h3>
|
||||
<ul>
|
||||
<li>Deepgram diarization + AI insights</li>
|
||||
<li>Redis caching layer for analysis results</li>
|
||||
<li>Full-stack: frontend player + server module</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ======================================
|
||||
SLIDE 6: DATA & BACKEND
|
||||
====================================== -->
|
||||
<section class="slide">
|
||||
<div class="reveal">
|
||||
<div class="section-header">
|
||||
<div class="section-icon" style="background: var(--glow-violet);">⚙️</div>
|
||||
<span class="label" style="background: var(--glow-violet); color: var(--accent-violet);">Backend & Data</span>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="reveal">Backend & Data Layer</h2>
|
||||
<div class="card-grid">
|
||||
<div class="content-card reveal">
|
||||
<h3 style="color: var(--accent-violet);">Platform Data Wiring</h3>
|
||||
<ul>
|
||||
<li>Migrated frontend to Jotai + Vercel AI SDK</li>
|
||||
<li>Corrected all 7 GraphQL queries (field names, LINKS/PHONES)</li>
|
||||
<li>Webhook handler for Ozonetel call records</li>
|
||||
<li>Complete seeder: 5 doctors, appointments linked, agent names match</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content-card reveal">
|
||||
<h3 style="color: var(--accent-violet);">Server Endpoints</h3>
|
||||
<ul>
|
||||
<li>Call control, recording, CDR, missed calls, live call assist</li>
|
||||
<li>Agent summary, AHT, performance aggregation</li>
|
||||
<li>Token refresh endpoint for auto-renewal</li>
|
||||
<li>Search module with full-text capabilities</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content-card reveal">
|
||||
<h3 style="color: var(--accent-violet);">Data Pages Built</h3>
|
||||
<ul>
|
||||
<li>Worklist table, call history, patients, dashboard</li>
|
||||
<li>Reports, team dashboard, campaigns, settings</li>
|
||||
<li>Agent detail page, campaign edit slideout</li>
|
||||
<li>Appointments page with data refresh on login</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content-card reveal">
|
||||
<h3 style="color: var(--accent-emerald);">SDK App <span class="repo-badge badge-sdk">FortyTwoApps</span></h3>
|
||||
<ul>
|
||||
<li>Helix Engage SDK app entity definitions</li>
|
||||
<li>Call center CRM object model for Fortytwo platform</li>
|
||||
<li>Foundation for platform-native data integration</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ======================================
|
||||
SLIDE 7: DEPLOYMENT & OPS
|
||||
====================================== -->
|
||||
<section class="slide">
|
||||
<div class="reveal">
|
||||
<div class="section-header">
|
||||
<div class="section-icon" style="background: rgba(251,113,133,0.15);">🛠️</div>
|
||||
<span class="label" style="background: rgba(251,113,133,0.15); color: var(--accent-rose);">Operations</span>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="reveal">Deployment & DevOps</h2>
|
||||
<div class="card-grid">
|
||||
<div class="content-card reveal">
|
||||
<h3 style="color: var(--accent-rose);">Deployment</h3>
|
||||
<ul>
|
||||
<li>Deployed to Hostinger VPS with Docker</li>
|
||||
<li>Switched to global_healthx Ozonetel account</li>
|
||||
<li>Dockerfile for server-side containerization</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content-card reveal">
|
||||
<h3 style="color: var(--accent-rose);">AI & Testing</h3>
|
||||
<ul>
|
||||
<li>Migrated AI to Vercel AI SDK + OpenAI provider</li>
|
||||
<li>AI flow test script — validates auth, lead, patient, doctor, appointments</li>
|
||||
<li>Live call assist integration</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="content-card reveal">
|
||||
<h3 style="color: var(--accent-rose);">Documentation</h3>
|
||||
<ul>
|
||||
<li>Team onboarding README with architecture guide</li>
|
||||
<li>Supervisor module spec + implementation plan</li>
|
||||
<li>Multi-agent spec + plan</li>
|
||||
<li>Next session plans documented in commits</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ======================================
|
||||
SLIDE 8: TIMELINE
|
||||
====================================== -->
|
||||
<section class="slide">
|
||||
<div class="reveal">
|
||||
<div class="section-header">
|
||||
<div class="section-icon" style="background: var(--glow-cyan);">📅</div>
|
||||
<span class="label" style="background: var(--glow-cyan); color: var(--accent-cyan);">Day by Day</span>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="reveal">Development Timeline</h2>
|
||||
<div class="timeline reveal" style="max-height: 60vh; overflow-y: auto; padding-right: 1rem;">
|
||||
<div class="tl-item">
|
||||
<div class="tl-date">MAR 18 (Tue)</div>
|
||||
<div class="tl-title">Foundation Day</div>
|
||||
<div class="tl-desc">Call desk redesign, Jotai + Vercel AI SDK migration, seeder with 5 doctors + linked appointments, AI flow test script, deployed to VPS</div>
|
||||
</div>
|
||||
<div class="tl-item">
|
||||
<div class="tl-date">MAR 19 (Wed)</div>
|
||||
<div class="tl-title">Data Layer Sprint</div>
|
||||
<div class="tl-desc">All data pages built (worklist, call history, patients, dashboard, reports), post-call workflow (disposition → booking), GraphQL fixes, Kookoo IVR outbound, outbound call UI</div>
|
||||
</div>
|
||||
<div class="tl-item">
|
||||
<div class="tl-date">MAR 20 (Thu)</div>
|
||||
<div class="tl-title">Telephony Breakthrough</div>
|
||||
<div class="tl-desc">Direct SIP call from browser replacing Kookoo bridge, UCID tracking, Force Ready, Ozonetel Set Disposition, telephony overhaul</div>
|
||||
</div>
|
||||
<div class="tl-item">
|
||||
<div class="tl-date">MAR 21 (Fri)</div>
|
||||
<div class="tl-title">Agent Experience</div>
|
||||
<div class="tl-desc">Phase 1 shipped — agent status toggle, global search, enquiry form, My Performance page, full FontAwesome icon migration, agent summary/AHT endpoints</div>
|
||||
</div>
|
||||
<div class="tl-item">
|
||||
<div class="tl-date">MAR 23 (Sun)</div>
|
||||
<div class="tl-title">Scale & Reliability</div>
|
||||
<div class="tl-desc">Phase 2 — missed call queue + auto-assignment, multi-agent SIP with Redis lockout, duplicate login prevention, Patient 360 rewrite, onboarding docs, SDK entity defs</div>
|
||||
</div>
|
||||
<div class="tl-item">
|
||||
<div class="tl-date">MAR 24 (Mon)</div>
|
||||
<div class="tl-title">Supervisor Module</div>
|
||||
<div class="tl-desc">Supervisor module with team performance + live monitor + master data, SSE agent state, UUID fix, maintenance module, QA bug sweep, supervisor endpoints</div>
|
||||
</div>
|
||||
<div class="tl-item">
|
||||
<div class="tl-date">MAR 25 (Tue)</div>
|
||||
<div class="tl-title">Intelligence Layer</div>
|
||||
<div class="tl-desc">Call recording analysis with Deepgram diarization + AI insights, SIP driven by Agent entity, token refresh, network indicator</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ======================================
|
||||
SLIDE 9: CLOSING
|
||||
====================================== -->
|
||||
<section class="slide closing-slide">
|
||||
<h2 class="reveal">78 commits. 8 days. Ship mode. 🚢</h2>
|
||||
<p class="reveal" style="color: var(--text-secondary); margin-top: 0.6em; font-size: 1.1rem; max-width: 600px; margin-inline: auto;">
|
||||
From browser-native SIP calling to AI-powered recording analysis — Helix Engage is becoming a production contact center platform.
|
||||
</p>
|
||||
<div class="pill-list reveal" style="justify-content: center; margin-top: 1.5rem;">
|
||||
<span class="pill" style="border-color: rgba(34,211,238,0.3); color: var(--accent-cyan);">SIP Calling ✓</span>
|
||||
<span class="pill" style="border-color: rgba(167,139,250,0.3); color: var(--accent-violet);">Multi-Agent ✓</span>
|
||||
<span class="pill" style="border-color: rgba(52,211,153,0.3); color: var(--accent-emerald);">Supervisor Module ✓</span>
|
||||
<span class="pill" style="border-color: rgba(251,191,36,0.3); color: var(--accent-amber);">AI Copilot ✓</span>
|
||||
<span class="pill" style="border-color: rgba(251,113,133,0.3); color: var(--accent-rose);">Recording Analysis ✓</span>
|
||||
</div>
|
||||
<p class="reveal" style="color: var(--text-muted); margin-top: 2rem; font-size: 0.8rem;">Satya Suman Sari · FortyTwo Platform</p>
|
||||
</section>
|
||||
|
||||
<!-- ===========================================
|
||||
SLIDE PRESENTATION CONTROLLER
|
||||
=========================================== -->
|
||||
<script>
|
||||
class SlidePresentation {
|
||||
constructor() {
|
||||
this.slides = document.querySelectorAll('.slide');
|
||||
this.progressBar = document.getElementById('progressBar');
|
||||
this.navDots = document.getElementById('navDots');
|
||||
this.slideCounter = document.getElementById('slideCounter');
|
||||
this.currentSlide = 0;
|
||||
|
||||
this.createNavDots();
|
||||
this.setupObserver();
|
||||
this.setupKeyboard();
|
||||
this.setupTouch();
|
||||
this.animateCounters();
|
||||
this.updateCounter();
|
||||
}
|
||||
|
||||
/* --- Navigation dots --- */
|
||||
createNavDots() {
|
||||
this.slides.forEach((_, i) => {
|
||||
const dot = document.createElement('button');
|
||||
dot.classList.add('nav-dot');
|
||||
dot.setAttribute('aria-label', `Go to slide ${i + 1}`);
|
||||
dot.addEventListener('click', () => this.goToSlide(i));
|
||||
this.navDots.appendChild(dot);
|
||||
});
|
||||
}
|
||||
|
||||
/* --- Intersection Observer for reveal animations --- */
|
||||
setupObserver() {
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
entry.target.classList.add('visible');
|
||||
const idx = Array.from(this.slides).indexOf(entry.target);
|
||||
if (idx !== -1) {
|
||||
this.currentSlide = idx;
|
||||
this.updateProgress();
|
||||
this.updateDots();
|
||||
this.updateCounter();
|
||||
if (idx === 1) this.animateCounters();
|
||||
}
|
||||
}
|
||||
});
|
||||
}, { threshold: 0.45 });
|
||||
|
||||
this.slides.forEach(slide => observer.observe(slide));
|
||||
}
|
||||
|
||||
/* --- Keyboard navigation --- */
|
||||
setupKeyboard() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'ArrowDown' || e.key === ' ' || e.key === 'ArrowRight') {
|
||||
e.preventDefault();
|
||||
this.goToSlide(Math.min(this.currentSlide + 1, this.slides.length - 1));
|
||||
} else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
|
||||
e.preventDefault();
|
||||
this.goToSlide(Math.max(this.currentSlide - 1, 0));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* --- Touch swipe support --- */
|
||||
setupTouch() {
|
||||
let startY = 0;
|
||||
document.addEventListener('touchstart', (e) => { startY = e.touches[0].clientY; });
|
||||
document.addEventListener('touchend', (e) => {
|
||||
const dy = startY - e.changedTouches[0].clientY;
|
||||
if (Math.abs(dy) > 50) {
|
||||
if (dy > 0) this.goToSlide(Math.min(this.currentSlide + 1, this.slides.length - 1));
|
||||
else this.goToSlide(Math.max(this.currentSlide - 1, 0));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
goToSlide(idx) {
|
||||
this.slides[idx].scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
|
||||
updateProgress() {
|
||||
const pct = ((this.currentSlide) / (this.slides.length - 1)) * 100;
|
||||
this.progressBar.style.width = pct + '%';
|
||||
}
|
||||
|
||||
updateDots() {
|
||||
this.navDots.querySelectorAll('.nav-dot').forEach((dot, i) => {
|
||||
dot.classList.toggle('active', i === this.currentSlide);
|
||||
});
|
||||
}
|
||||
|
||||
updateCounter() {
|
||||
this.slideCounter.textContent = `${this.currentSlide + 1} / ${this.slides.length}`;
|
||||
}
|
||||
|
||||
/* --- Animate counter numbers --- */
|
||||
animateCounters() {
|
||||
document.querySelectorAll('[data-count]').forEach(el => {
|
||||
const target = parseInt(el.dataset.count);
|
||||
const duration = 1200;
|
||||
const start = performance.now();
|
||||
const animate = (now) => {
|
||||
const elapsed = now - start;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const eased = 1 - Math.pow(1 - progress, 3);
|
||||
el.textContent = Math.round(eased * target);
|
||||
if (progress < 1) requestAnimationFrame(animate);
|
||||
};
|
||||
requestAnimationFrame(animate);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize
|
||||
new SlidePresentation();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
docs/weekly-update-mar18-25.pptx
Normal file
BIN
docs/weekly-update-mar18-25.pptx
Normal file
Binary file not shown.
14
src/components/base/avatar/base-components/avatar-count.tsx
Normal file
14
src/components/base/avatar/base-components/avatar-count.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import { cx } from "@/utils/cx";
|
||||
|
||||
interface AvatarCountProps {
|
||||
count: number;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const AvatarCount = ({ count, className }: AvatarCountProps) => (
|
||||
<div className={cx("absolute right-0 bottom-0 p-px", className)}>
|
||||
<div className="flex size-3.5 items-center justify-center rounded-full bg-fg-error-primary text-center text-[10px] leading-[13px] font-bold text-white">
|
||||
{count}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
49
src/components/base/select/select-shared.tsx
Normal file
49
src/components/base/select/select-shared.tsx
Normal file
@@ -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" });
|
||||
@@ -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<ContextTab>('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 (
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Tab bar */}
|
||||
<div className="flex shrink-0 border-b border-secondary">
|
||||
<button
|
||||
onClick={() => setActiveTab('ai')}
|
||||
className={cx(
|
||||
"flex flex-1 items-center justify-center gap-1.5 px-3 py-2.5 text-xs font-semibold transition duration-100 ease-linear",
|
||||
activeTab === 'ai'
|
||||
? "border-b-2 border-brand text-brand-secondary"
|
||||
: "text-tertiary hover:text-secondary",
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSparkles} className="size-3.5" />
|
||||
AI Assistant
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('lead360')}
|
||||
className={cx(
|
||||
"flex flex-1 items-center justify-center gap-1.5 px-3 py-2.5 text-xs font-semibold transition duration-100 ease-linear",
|
||||
activeTab === 'lead360'
|
||||
? "border-b-2 border-brand text-brand-secondary"
|
||||
: "text-tertiary hover:text-secondary",
|
||||
)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUser} className="size-3.5" />
|
||||
{(selectedLead as any)?.patientId ? 'Patient 360' : 'Lead 360'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
{activeTab === 'ai' && (
|
||||
<div className="flex flex-1 flex-col overflow-hidden p-4">
|
||||
<AiChatPanel callerContext={callerContext} />
|
||||
</div>
|
||||
)}
|
||||
{activeTab === 'lead360' && (
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<Lead360Tab lead={selectedLead} activities={activities} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Lead360Tab = ({ lead, activities }: { lead: Lead | null; activities: LeadActivity[] }) => {
|
||||
export const ContextPanel = ({ selectedLead, activities, callerPhone, isInCall }: ContextPanelProps) => {
|
||||
const [patientData, setPatientData] = useState<any>(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,121 +48,64 @@ 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 (
|
||||
<div className="flex flex-col items-center justify-center py-16 text-center px-4">
|
||||
<FontAwesomeIcon icon={faUser} className="mb-3 size-8 text-fg-quaternary" />
|
||||
<p className="text-sm text-tertiary">Select a lead from the worklist to see their full profile.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className="p-4 space-y-4">
|
||||
{/* Profile */}
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Context header — shows caller/lead info when available */}
|
||||
{lead && (
|
||||
<div className="shrink-0 border-b border-secondary p-4 space-y-3">
|
||||
{/* Call status banner */}
|
||||
{isInCall && (
|
||||
<div className="flex items-center gap-2 rounded-lg bg-success-primary px-3 py-2">
|
||||
<FontAwesomeIcon icon={faPhone} className="size-3 text-fg-success-primary" />
|
||||
<span className="text-xs font-semibold text-success-primary">
|
||||
On call with {fullName || callerPhone || 'Unknown'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Lead profile */}
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-primary">{fullName}</h3>
|
||||
<h3 className="text-lg font-bold text-primary">{fullName || 'Unknown'}</h3>
|
||||
{phone && <p className="text-sm text-secondary">{formatPhone(phone)}</p>}
|
||||
{email && <p className="text-xs text-tertiary">{email}</p>}
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{isReturning && (
|
||||
</div>
|
||||
|
||||
{/* Status badges */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{!!patientData && (
|
||||
<Badge size="sm" color="brand" type="pill-color">Returning Patient</Badge>
|
||||
)}
|
||||
{patientAge !== null && patientGender && (
|
||||
<Badge size="sm" color="gray" type="pill-color">{patientAge}y · {patientGender}</Badge>
|
||||
)}
|
||||
{lead.leadStatus && <Badge size="sm" color="brand">{lead.leadStatus}</Badge>}
|
||||
{lead.leadSource && <Badge size="sm" color="gray">{lead.leadSource}</Badge>}
|
||||
{lead.priority && lead.priority !== 'NORMAL' && (
|
||||
<Badge size="sm" color={lead.priority === 'URGENT' ? 'error' : 'warning'}>{lead.priority}</Badge>
|
||||
)}
|
||||
{lead.leadStatus && <Badge size="sm" color="brand">{lead.leadStatus.replace(/_/g, ' ')}</Badge>}
|
||||
{lead.leadSource && <Badge size="sm" color="gray">{lead.leadSource.replace(/_/g, ' ')}</Badge>}
|
||||
</div>
|
||||
|
||||
{lead.interestedService && (
|
||||
<p className="mt-2 text-sm text-secondary">Interested in: {lead.interestedService}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Returning patient: Appointments */}
|
||||
{loadingPatient && (
|
||||
<p className="text-xs text-tertiary">Loading patient details...</p>
|
||||
)}
|
||||
{isReturning && appointments.length > 0 && (
|
||||
<div>
|
||||
<h4 className="mb-2 text-xs font-bold text-secondary uppercase">Appointments</h4>
|
||||
<div className="space-y-2">
|
||||
{appointments.map((appt: any) => {
|
||||
const statusColors: Record<string, 'success' | 'brand' | 'warning' | 'error' | 'gray'> = {
|
||||
COMPLETED: 'success', SCHEDULED: 'brand', CONFIRMED: 'brand',
|
||||
CANCELLED: 'error', NO_SHOW: 'warning',
|
||||
};
|
||||
return (
|
||||
<div key={appt.id} className="flex items-start gap-2 rounded-lg bg-secondary p-2">
|
||||
<CalendarCheck className="mt-0.5 size-3.5 text-fg-brand-primary shrink-0" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-xs font-semibold text-primary">
|
||||
{appt.doctorName ?? 'Doctor'} · {appt.department ?? ''}
|
||||
</span>
|
||||
{appt.status && (
|
||||
<Badge size="sm" color={statusColors[appt.status] ?? 'gray'}>
|
||||
{appt.status.toLowerCase()}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-[10px] text-quaternary">
|
||||
{appt.scheduledAt ? formatShortDate(appt.scheduledAt) : ''}
|
||||
{appt.reasonForVisit ? ` — ${appt.reasonForVisit}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-secondary">Interested in: {lead.interestedService}</p>
|
||||
)}
|
||||
|
||||
{/* Returning patient: Recent calls */}
|
||||
{isReturning && patientCalls.length > 0 && (
|
||||
<div>
|
||||
<h4 className="mb-2 text-xs font-bold text-secondary uppercase">Recent Calls</h4>
|
||||
<div className="space-y-1">
|
||||
{patientCalls.map((call: any) => (
|
||||
<div key={call.id} className="flex items-center gap-2 text-xs">
|
||||
<div className="mt-0.5 size-1.5 shrink-0 rounded-full bg-fg-quaternary" />
|
||||
<span className="text-primary">
|
||||
{call.direction === 'INBOUND' ? 'Inbound' : 'Outbound'}
|
||||
{call.disposition ? ` — ${call.disposition.replace(/_/g, ' ').toLowerCase()}` : ''}
|
||||
</span>
|
||||
<span className="text-quaternary ml-auto">{call.startedAt ? formatShortDate(call.startedAt) : ''}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AI Insight */}
|
||||
{/* AI Insight — live from platform */}
|
||||
{(lead.aiSummary || lead.aiSuggestedAction) && (
|
||||
<div className="rounded-lg bg-brand-primary p-3">
|
||||
<div className="mb-1 flex items-center gap-1.5">
|
||||
@@ -240,18 +119,42 @@ const Lead360Tab = ({ lead, activities }: { lead: Lead | null; activities: LeadA
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Activity timeline */}
|
||||
{/* Upcoming appointments */}
|
||||
{appointments.length > 0 && (
|
||||
<div>
|
||||
<span className="text-[10px] font-bold text-tertiary uppercase">Appointments</span>
|
||||
<div className="mt-1 space-y-1">
|
||||
{appointments.slice(0, 3).map((appt: any) => (
|
||||
<div key={appt.id} className="flex items-center gap-2 rounded-md bg-secondary px-2 py-1.5">
|
||||
<CalendarCheck className="size-3 text-fg-brand-primary shrink-0" />
|
||||
<span className="text-xs text-primary truncate">
|
||||
{appt.doctorName ?? 'Doctor'} · {appt.scheduledAt ? formatShortDate(appt.scheduledAt) : ''}
|
||||
</span>
|
||||
{appt.status && (
|
||||
<Badge size="sm" color={appt.status === 'COMPLETED' ? 'success' : appt.status === 'CANCELLED' ? 'error' : 'brand'}>
|
||||
{appt.status.toLowerCase()}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loadingPatient && <p className="text-[10px] text-quaternary">Loading patient details...</p>}
|
||||
|
||||
{/* Recent activity */}
|
||||
{leadActivities.length > 0 && (
|
||||
<div>
|
||||
<h4 className="mb-2 text-xs font-bold text-secondary uppercase">Activity</h4>
|
||||
<div className="space-y-2">
|
||||
{leadActivities.map((a) => (
|
||||
<span className="text-[10px] font-bold text-tertiary uppercase">Recent Activity</span>
|
||||
<div className="mt-1 space-y-1">
|
||||
{leadActivities.slice(0, 5).map((a) => (
|
||||
<div key={a.id} className="flex items-start gap-2">
|
||||
<div className="mt-1.5 size-1.5 shrink-0 rounded-full bg-fg-quaternary" />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-xs text-primary">{a.summary}</p>
|
||||
<p className="text-xs text-primary truncate">{a.summary}</p>
|
||||
<p className="text-[10px] text-quaternary">
|
||||
{a.activityType}{a.occurredAt ? ` · ${formatShortDate(a.occurredAt)}` : ''}
|
||||
{a.activityType?.replace(/_/g, ' ')}{a.occurredAt ? ` · ${formatShortDate(a.occurredAt)}` : ''}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -260,5 +163,22 @@ const Lead360Tab = ({ lead, activities }: { lead: Lead | null; activities: LeadA
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No lead selected — empty state */}
|
||||
{!lead && (
|
||||
<div className="shrink-0 flex items-center justify-center border-b border-secondary px-4 py-6">
|
||||
<div className="text-center">
|
||||
<FontAwesomeIcon icon={faUser} className="mb-2 size-6 text-fg-quaternary" />
|
||||
<p className="text-xs text-tertiary">Select a lead from the worklist to see context</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* AI Chat — always available at the bottom */}
|
||||
<div className="flex flex-1 flex-col overflow-hidden">
|
||||
<AiChatPanel callerContext={callerContext} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user