Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions apps/web/utils/ai/meeting-briefs/generate-briefing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,28 @@ 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<typeof briefingSchema>;

export async function aiGenerateMeetingBriefing({
briefingData,
emailAccount,
}: {
briefingData: MeetingBriefingData;
emailAccount: EmailAccountWithAI;
}): Promise<string> {
}): Promise<BriefingContent> {
const system = `You are an AI assistant that prepares concise meeting briefings.

Your task is to prepare a briefing that includes:
Expand All @@ -37,8 +48,10 @@ Guidelines:
- ONLY include information about the specific guests listed in <guest_context>. 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);

Expand All @@ -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 {
Expand Down Expand Up @@ -86,7 +99,7 @@ ${event.description ? `Description: ${event.description}` : ""}
${guestContexts.map((guest) => formatGuestContext(guest)).join("\n")}
</guest_context>

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;
}
Expand Down
3 changes: 2 additions & 1 deletion apps/web/utils/meeting-briefs/send-briefing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -20,7 +21,7 @@ export async function sendBriefingEmail({
logger,
}: {
event: CalendarEvent;
briefingContent: string;
briefingContent: BriefingContent;
emailAccountId: string;
userEmail: string;
provider: string;
Expand Down
113 changes: 55 additions & 58 deletions packages/resend/emails/meeting-briefing.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,58 +10,45 @@ 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;
meetingTitle: string;
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(
<strong key={`bold-${index}-${match.index}`}>{match[1]}</strong>,
);
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 (
<span key={`line-${index}`}>
{parts}
{index < lines.length - 1 && <br />}
</span>
);
});
function renderGuestBriefings(guests: GuestBriefing[]) {
return guests.map((guest, guestIndex) => (
<div key={`guest-${guestIndex}`} className={guestIndex > 0 ? "mt-4" : ""}>
<Text className="text-sm text-gray-800 mt-0 mb-1">
<strong>
{guest.name} ({guest.email})
</strong>
</Text>
{guest.bullets.map((bullet, bulletIndex) => (
<Text
key={`bullet-${guestIndex}-${bulletIndex}`}
className="text-sm text-gray-800 mt-0 mb-0 pl-2"
>
- {bullet}
</Text>
))}
</div>
));
}

export default function MeetingBriefingEmail({
Expand Down Expand Up @@ -102,7 +89,7 @@ export default function MeetingBriefingEmail({
)}
{eventUrl && (
<Text className="text-sm text-gray-700 mt-0 mb-0">
- Event link:{" "}
- Calendar link:{" "}
<Link href={eventUrl} className="text-blue-600 underline">
{eventUrl}
</Link>
Expand All @@ -111,9 +98,7 @@ export default function MeetingBriefingEmail({
</Section>

<Section className="px-8 pb-6">
<div className="text-sm text-gray-800 leading-relaxed">
{renderFormattedContent(briefingContent)}
</div>
{renderGuestBriefings(briefingContent.guests)}
</Section>

<Hr className="border-solid border-gray-300 my-6 mx-8" />
Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v2.23.1
v2.23.2
Loading