meetingEmailPrompts.ts5.4 KBView on GitHub
import type { CalendarEvent } from '../types/calendar-types';
import { useCedarStore } from '@/modules/store';

/**
 * Prompts that ask the agent to draft an email about a specific meeting.
 *
 * These build the WHOLE prompt from the event — title, window, attendees, conferencing —
 * because they're fired from a button or a hotkey with nothing typed. They go out via
 * `setOverrideInputContent(..., { autoSend: true })`, which submits immediately instead of
 * parking the text in the composer for the user to edit.
 */

/** ISO strings are unreadable in a prompt; give the agent something it can quote back. */
function formatWindow(event: CalendarEvent): string {
  const start = event.start?.dateTime ?? event.start?.date;
  const end = event.end?.dateTime ?? event.end?.date;
  if (!start) return 'an unknown time';

  const startDate = new Date(start);
  const startLabel = startDate.toLocaleString('en-US', {
    weekday: 'long',
    month: 'long',
    day: 'numeric',
    hour: 'numeric',
    minute: '2-digit',
  });
  if (!end) return startLabel;

  const endLabel = new Date(end).toLocaleTimeString('en-US', {
    hour: 'numeric',
    minute: '2-digit',
  });
  return `${startLabel} – ${endLabel}`;
}

/** Everyone but the user — they're the ones the email goes to. */
function describeAttendees(event: CalendarEvent): string {
  const others = (event.attendees ?? []).filter((a) => !a.self && a.email);
  if (others.length === 0) return 'no other attendees listed';
  return others
    .map((a) => (a.displayName ? `${a.displayName} <${a.email}>` : (a.email ?? '')))
    .join(', ');
}

/**
 * "They didn't show" — ask for a combined no-show + reschedule email for a meeting that is
 * happening right now. One draft rather than two: in practice the no-show note and the
 * "when can we redo this?" ask are the same email.
 */
export function buildNoShowPrompt(event: CalendarEvent): string {
  const title = event.summary || 'Untitled meeting';
  const meetLink = event.conferenceData?.entryPoints?.[0]?.uri;

  return [
    `The attendees did not show up to "${title}" (${formatWindow(event)}).`,
    `Attendees: ${describeAttendees(event)}.`,
    meetLink ? `Meeting link: ${meetLink}.` : null,
    '',
    'Draft a short, friendly email to them that notes we missed each other, keeps all blame off',
    'them, and proposes rescheduling — offer a couple of concrete alternative times based on my',
    'calendar availability. Keep it brief and in my voice.',
  ]
    .filter(Boolean)
    .join('\n');
}

/**
 * "We need to move this" — a reschedule ask for a meeting that hasn't happened yet (or is
 * happening now). Unlike the no-show prompt this carries no implication that anyone missed it.
 */
export function buildReschedulePrompt(event: CalendarEvent): string {
  const title = event.summary || 'Untitled meeting';

  return [
    `I need to reschedule "${title}" (${formatWindow(event)}).`,
    `Attendees: ${describeAttendees(event)}.`,
    '',
    'Draft a short, apologetic email asking to move it. Offer a couple of concrete alternative',
    'times based on my calendar availability, and keep it brief and in my voice.',
  ].join('\n');
}

/**
 * Send a meeting prompt to the chat and submit it straight away. Used from surfaces that have
 * no composer of their own (the agenda block's "No show" button, the calendar hotkeys).
 */
export function sendMeetingPrompt(prompt: string): void {
  useCedarStore.getState().setOverrideInputContent(prompt, { autoSend: true });
}

/** Convenience wrapper: build the no-show prompt for `event` and fire it. */
export function sendNoShowPrompt(event: CalendarEvent): void {
  sendMeetingPrompt(buildNoShowPrompt(event));
}

/** Convenience wrapper: build the reschedule prompt for `event` and fire it. */
export function sendReschedulePrompt(event: CalendarEvent): void {
  sendMeetingPrompt(buildReschedulePrompt(event));
}

/**
 * "That's a wrap" — a recap-and-next-steps email for a meeting that has already happened.
 *
 * The third member of this family, added for the Meetings tab's `after` phase. It differs
 * from the other two in what it points the agent AT: a no-show or reschedule ask can be
 * written from the invitation alone, but a recap is only worth sending if it reflects what
 * was actually said. So when the meeting has notes, the prompt names them and tells the
 * agent to lead with them — the notes doc and any transcript are already in the agent's
 * reach, and asking for a recap without pointing at them produces generic filler.
 */
export function buildRecapPrompt(event: CalendarEvent, hasNotes: boolean): string {
  const title = event.summary || 'Untitled meeting';

  return [
    `We just finished "${title}" (${formatWindow(event)}).`,
    `Attendees: ${describeAttendees(event)}.`,
    hasNotes
      ? 'Read my notes on this meeting and the transcript if there is one, and lead with what I wrote — my notes are what I actually care about.'
      : 'Use the transcript if there is one; otherwise work from the deal context.',
    '',
    'Draft a short recap email to them: what we agreed, the open questions, and the concrete',
    'next step with an owner and a date. No preamble, no restating the agenda, in my voice.',
  ].join('\n');
}

/** Convenience wrapper: build the recap prompt for `event` and fire it. */
export function sendRecapPrompt(event: CalendarEvent, hasNotes: boolean): void {
  sendMeetingPrompt(buildRecapPrompt(event, hasNotes));
}