diff --git a/apps/web/utils/ai/meeting-briefs/generate-briefing.ts b/apps/web/utils/ai/meeting-briefs/generate-briefing.ts index a728497c58..397ca1f911 100644 --- a/apps/web/utils/ai/meeting-briefs/generate-briefing.ts +++ b/apps/web/utils/ai/meeting-briefs/generate-briefing.ts @@ -9,9 +9,20 @@ import { stringifyEmailSimple } from "@/utils/stringify-email"; import { getEmailForLLM } from "@/utils/get-email-from-message"; import type { ParsedMessage } from "@/utils/types"; +const guestBriefingSchema = z.object({ + name: z.string().describe("The guest's name"), + email: z.string().describe("The guest's email address"), + bullets: z + .array(z.string()) + .describe("Brief bullet points about this guest (max 10 words each)"), +}); + const briefingSchema = z.object({ - briefing: z.string().describe("The meeting briefing content"), + guests: z + .array(guestBriefingSchema) + .describe("Briefing information for each meeting guest"), }); +type BriefingContent = z.infer; export async function aiGenerateMeetingBriefing({ briefingData, @@ -19,7 +30,7 @@ export async function aiGenerateMeetingBriefing({ }: { briefingData: MeetingBriefingData; emailAccount: EmailAccountWithAI; -}): Promise { +}): Promise { const system = `You are an AI assistant that prepares concise meeting briefings. Your task is to prepare a briefing that includes: @@ -37,8 +48,10 @@ Guidelines: - ONLY include information about the specific guests listed in . Do NOT mention other meeting attendees, organizers, or colleagues. - AI research may be inaccurate for common names or generic email addresses -Output the briefing as plain text with bullet points using "-" for each point. -Group information by guest if there are multiple external guests.`; +Return a structured JSON object with a "guests" array. Each guest should have: +- "name": The guest's display name +- "email": The guest's email address +- "bullets": An array of brief bullet points about them (max 10 words each)`; const prompt = buildPrompt(briefingData); @@ -57,7 +70,7 @@ Group information by guest if there are multiple external guests.`; schema: briefingSchema, }); - return result.object.briefing; + return result.object; } function buildPrompt(briefingData: MeetingBriefingData): string { @@ -86,7 +99,7 @@ ${event.description ? `Description: ${event.description}` : ""} ${guestContexts.map((guest) => formatGuestContext(guest)).join("\n")} -Return the briefing as JSON with a "briefing" field containing the formatted text.`; +Return the briefing as a JSON object with a "guests" array containing structured information for each guest.`; return prompt; } diff --git a/apps/web/utils/meeting-briefs/send-briefing.ts b/apps/web/utils/meeting-briefs/send-briefing.ts index bc545cbece..2d949a380e 100644 --- a/apps/web/utils/meeting-briefs/send-briefing.ts +++ b/apps/web/utils/meeting-briefs/send-briefing.ts @@ -6,6 +6,7 @@ import { sendMeetingBriefingEmail } from "@inboxzero/resend"; import MeetingBriefingEmail, { generateMeetingBriefingSubject, type MeetingBriefingEmailProps, + type BriefingContent, } from "@inboxzero/resend/emails/meeting-briefing"; import type { CalendarEvent } from "@/utils/calendar/event-types"; import type { Logger } from "@/utils/logger"; @@ -20,7 +21,7 @@ export async function sendBriefingEmail({ logger, }: { event: CalendarEvent; - briefingContent: string; + briefingContent: BriefingContent; emailAccountId: string; userEmail: string; provider: string; diff --git a/packages/resend/emails/meeting-briefing.tsx b/packages/resend/emails/meeting-briefing.tsx index ee4e338603..c3c39ce9da 100644 --- a/packages/resend/emails/meeting-briefing.tsx +++ b/packages/resend/emails/meeting-briefing.tsx @@ -10,6 +10,16 @@ import { Text, } from "@react-email/components"; +export type GuestBriefing = { + name: string; + email: string; + bullets: string[]; +}; + +export type BriefingContent = { + guests: GuestBriefing[]; +}; + export type MeetingBriefingEmailProps = { baseUrl: string; emailAccountId: string; @@ -17,51 +27,28 @@ export type MeetingBriefingEmailProps = { formattedTime: string; // e.g., "2:00 PM" videoConferenceLink?: string; eventUrl: string; - briefingContent: string; + briefingContent: BriefingContent; unsubscribeToken: string; }; -// Helper function to parse content and render with formatting -function renderFormattedContent(content: string) { - const lines = content.split("\n"); - - return lines.map((line, index) => { - // Check if line contains **bold** markdown - const parts: (string | React.ReactElement)[] = []; - let lastIndex = 0; - const boldRegex = /\*\*(.+?)\*\*/g; - - let match = boldRegex.exec(line); - while (match !== null) { - // Add text before the match - if (match.index > lastIndex) { - parts.push(line.substring(lastIndex, match.index)); - } - // Add the bold text - parts.push( - {match[1]}, - ); - lastIndex = match.index + match[0].length; - match = boldRegex.exec(line); - } - - // Add remaining text - if (lastIndex < line.length) { - parts.push(line.substring(lastIndex)); - } - - // If no bold formatting was found, just return the line - if (parts.length === 0) { - parts.push(line); - } - - return ( - - {parts} - {index < lines.length - 1 &&
} -
- ); - }); +function renderGuestBriefings(guests: GuestBriefing[]) { + return guests.map((guest, guestIndex) => ( +
0 ? "mt-4" : ""}> + + + {guest.name} ({guest.email}) + + + {guest.bullets.map((bullet, bulletIndex) => ( + + - {bullet} + + ))} +
+ )); } export default function MeetingBriefingEmail({ @@ -102,7 +89,7 @@ export default function MeetingBriefingEmail({ )} {eventUrl && ( - - Event link:{" "} + - Calendar link:{" "} {eventUrl} @@ -111,9 +98,7 @@ export default function MeetingBriefingEmail({
-
- {renderFormattedContent(briefingContent)} -
+ {renderGuestBriefings(briefingContent.guests)}

@@ -147,18 +132,30 @@ MeetingBriefingEmail.PreviewProps = { formattedTime: "2:00 PM", videoConferenceLink: "https://meet.google.com/abc-defg-hij", eventUrl: "https://calendar.google.com/event/123", - guestCount: 2, - briefingContent: `**John Smith (john@acmecorp.com)** -- CEO of Acme Corp, joined 2019 -- Last met 3 weeks ago for quarterly review -- Recent email: Discussed pricing for enterprise tier -- Interested in API integrations -- Decision maker for their team of 50+ - -**Sarah Johnson (sarah@acmecorp.com)** -- VP of Engineering at Acme Corp -- First time meeting this contact -- Technical evaluator for the deal`, + briefingContent: { + guests: [ + { + name: "John Smith", + email: "john@acmecorp.com", + bullets: [ + "CEO of Acme Corp, joined 2019", + "Last met 3 weeks ago for quarterly review", + "Recent email: Discussed pricing for enterprise tier", + "Interested in API integrations", + "Decision maker for their team of 50+", + ], + }, + { + name: "Sarah Johnson", + email: "sarah@acmecorp.com", + bullets: [ + "VP of Engineering at Acme Corp", + "First time meeting this contact", + "Technical evaluator for the deal", + ], + }, + ], + }, }; export function generateMeetingBriefingSubject( diff --git a/version.txt b/version.txt index aaccdefabd..1a09cfd62a 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -v2.23.1 +v2.23.2