/// import { derive, handler, ifElse, JSONSchema, lift, llm, NAME, recipe, str, UI, } from "commontools"; // Reuse email schema from email-summarizer.tsx const EmailProperties = { id: { type: "string", title: "Email ID", description: "Unique identifier for the email", }, threadId: { type: "string", title: "Thread ID", description: "Identifier for the email thread", }, labelIds: { type: "array", items: { type: "string" }, title: "Labels", description: "Gmail labels assigned to the email", }, snippet: { type: "string", title: "Snippet", description: "Brief preview of the email content", }, subject: { type: "string", title: "Subject", description: "Email subject line", }, from: { type: "string", title: "From", description: "Sender's email address", }, date: { type: "string", title: "Date", description: "Date and time when the email was sent", }, to: { type: "string", title: "To", description: "Recipient's email address", }, plainText: { type: "string", title: "Plain Text Content", description: "Email content in plain text format (often empty)", }, htmlContent: { type: "string", title: "HTML Content", description: "Email content in HTML format", }, markdownContent: { type: "string", title: "Markdown Content", description: "Email content converted to Markdown format", }, } as const; const EmailSchema = { type: "object", properties: EmailProperties, required: Object.keys(EmailProperties), } as const satisfies JSONSchema; // Define the date item schema const DateItemSchema = { type: "object", properties: { dateText: { type: "string", title: "Date Text", description: "The raw date text found in the email", }, normalizedDate: { type: "string", title: "Normalized Date", description: "The date in ISO format (YYYY-MM-DD)", }, normalizedTime: { type: "string", title: "Normalized Time", description: "The time in 24-hour format (HH:MM) if available", }, context: { type: "string", title: "Context", description: "Brief context around the date mention", }, confidence: { type: "number", title: "Confidence", description: "Confidence score (0-1) that this is a relevant date", }, }, required: ["dateText", "normalizedDate", "context", "confidence"], } as const satisfies JSONSchema; // Input Schema for Email Date Extractor const EmailDateExtractorInputSchema = { type: "object", properties: { emails: { type: "array", items: EmailSchema, }, settings: { type: "object", properties: { includeEmailDate: { type: "boolean", default: false, description: "Whether to include the email's sent date in results", }, extractTimes: { type: "boolean", default: true, description: "Whether to extract time information along with dates", }, contextLength: { type: "number", default: 100, description: "Length of context to include around each date mention", }, minConfidence: { type: "number", default: 0.7, description: "Minimum confidence threshold for included dates (0-1)", }, }, default: {}, required: [ "includeEmailDate", "extractTimes", "contextLength", "minConfidence", ], }, }, required: ["emails", "settings"], description: "Email Date Extractor", } as const satisfies JSONSchema; // Output Schema const ResultSchema = { type: "object", properties: { emailsWithDates: { type: "array", items: { type: "object", properties: { email: EmailSchema, dates: { type: "array", items: DateItemSchema, }, }, required: ["email", "dates"], }, }, allDates: { type: "array", items: DateItemSchema, }, }, required: ["emailsWithDates", "allDates"], } as const satisfies JSONSchema; // Define a handler for updating the includeEmailDate setting const updateIncludeEmailDate = handler( { type: "object", properties: { detail: { type: "object", properties: { checked: { type: "boolean" }, }, }, }, }, { type: "object", properties: { includeEmailDate: { type: "boolean", asCell: true, }, }, required: ["includeEmailDate"], }, ({ detail }, { includeEmailDate }) => { includeEmailDate.set(detail?.checked ?? false); }, ); // Define a handler for updating the extractTimes setting const updateExtractTimes = handler( { type: "object", properties: { detail: { type: "object", properties: { checked: { type: "boolean" }, }, }, }, }, { type: "object", properties: { extractTimes: { type: "boolean", asCell: true, }, }, required: ["extractTimes"], }, ({ detail }, { extractTimes }) => { extractTimes.set(detail?.checked ?? true); }, ); // Handler for updating context length const updateContextLength = handler( { type: "object", properties: { detail: { type: "object", properties: { value: { type: "string" }, }, }, }, }, { type: "object", properties: { contextLength: { type: "number", asCell: true, }, }, required: ["contextLength"], }, ({ detail }, { contextLength }) => { const value = parseInt(detail?.value ?? "100", 10); contextLength.set(isNaN(value) ? 100 : value); }, ); // Handler for updating confidence threshold const updateMinConfidence = handler( { type: "object", properties: { detail: { type: "object", properties: { value: { type: "string" }, }, }, }, }, { type: "object", properties: { minConfidence: { type: "number", asCell: true, }, }, required: ["minConfidence"], }, ({ detail }, { minConfidence }) => { const value = parseFloat(detail?.value ?? "0.7"); minConfidence.set(isNaN(value) ? 0.7 : Math.max(0, Math.min(1, value))); }, ); // Define a lifted function to extract content from email const getEmailContent = lift( // Input schema { type: "object", properties: { email: EmailSchema, }, required: ["email"], }, // Output schema { type: "object", properties: { email: EmailSchema, content: { type: "string" }, hasContent: { type: "boolean" }, }, required: ["email", "content", "hasContent"], }, // Implementation ({ email }) => { const content = email.markdownContent || email.plainText || email.snippet || ""; return { email, content: content.trim() ? content : "", hasContent: content.trim().length > 0, }; }, ); // The main recipe export default recipe( EmailDateExtractorInputSchema, ResultSchema, ({ emails, settings }) => { // Process each email to extract dates const emailsWithDates = emails.map((email) => { // First get the email content const emailContent = getEmailContent({ email }); // Create LLM prompt for date extraction using ifElse instead of ternary operators const timeInstruction = ifElse( settings.extractTimes, "If time is mentioned, include normalized time in 24-hour format (HH:MM)", "Ignore time information", ); const timeField = ifElse( settings.extractTimes, `"normalizedTime": "14:30",`, "", ); const dateInclusionInstruction = ifElse( settings.includeEmailDate, "Include the email's sent date if it's mentioned in the content.", "Do not include the email's sent date.", ); const systemPrompt = str` You are a specialized date extraction assistant. Extract all dates mentioned in the email. For each date found: 1. Extract the raw text of the date as mentioned 2. Normalize to ISO format (YYYY-MM-DD) 3. ${timeInstruction} 4. Include a brief context snippet (${settings.contextLength} characters) around the date mention 5. Assign a confidence score (0-1) that this is a relevant future date/deadline/appointment Return only JSON in this exact format: { "dates": [ { "dateText": "next Monday", "normalizedDate": "2025-04-07", ${timeField} "context": "Let's meet next Monday to discuss the project timeline.", "confidence": 0.95 }, ...more dates ] } ${dateInclusionInstruction} Only include dates with confidence score >= ${settings.minConfidence}. `; const userPrompt = str` Subject: ${email.subject} Date: ${email.date} From: ${email.from} ${emailContent.content} `; // Call LLM to get structured data - no conditional check needed // The framework will handle empty content cases reactively const extractionResult = llm({ system: systemPrompt, messages: [{ role: "user", content: userPrompt }], model: "google:gemini-2.5-flash", mode: "json", }); // Return email with extracted dates // The framework will handle the async nature of the LLM result return { email: email, dates: derive(extractionResult, (result) => { try { // Handle possible null result during processing if (!result?.result) return []; // Parse the result as JSON const parsed = typeof result.result === "string" ? JSON.parse(result.result) : result.result; return parsed?.dates || []; } catch (_) { // Return empty array if parsing fails return []; } }), }; }); // Derive a flattened list of all dates across all emails const allDates = derive( emailsWithDates, (items) => { // Flatten all dates from all emails into a single array return items.flatMap((item) => item.dates || []); }, ); // Count of emails and dates const emailCount = derive(emails, (emails) => emails?.length); const dateCount = derive(allDates, (dates) => dates?.length); // Instantiate handlers const includeEmailDateHandler = updateIncludeEmailDate({ includeEmailDate: settings.includeEmailDate, }); const extractTimesHandler = updateExtractTimes({ extractTimes: settings.extractTimes, }); const contextLengthHandler = updateContextLength({ contextLength: settings.contextLength, }); const minConfidenceHandler = updateMinConfidence({ minConfidence: settings.minConfidence, }); // Return recipe results return { [NAME]: str`Email Date Extractor (${dateCount} dates from ${emailCount} emails)`, [UI]: (

Email Date Extractor

Emails processed: {emailCount} Dates found: {dateCount}

All Extracted Dates

{ifElse(settings.extractTimes, , null)} {allDates.map((date) => ( {ifElse( settings.extractTimes, , null, )} ))}
DATE TEXT NORMALIZEDTIMECONTEXT CONFIDENCE EMAIL
{date.dateText} {date.normalizedDate} {ifElse(date.normalizedTime, date.normalizedTime, "-")} {date.context} {derive( date, (d) => ((d?.confidence ?? 0) * 100).toFixed(0), )}% {derive(emailsWithDates, (items) => items.find((e) => e.dates.includes(date))?.email .subject || "")}

Dates by Email

{derive(emailsWithDates, (items) => items.filter((item) => item.dates && item.dates.length > 0 )) .map((item) => (

{item.email.subject}

{ifElse(settings.extractTimes, (), null)} {item.dates.map((date) => ( {ifElse( settings.extractTimes, (), null, )} ))}
DATE TEXT NORMALIZEDTIMECONTEXT CONFIDENCE
{date.dateText} {date.normalizedDate} {ifElse( date.normalizedTime, date.normalizedTime, "-", )} {date.context} {derive( date, (d) => (d?.confidence ?? 0 * 100).toFixed(0), )}%
))}
), emailsWithDates, allDates, }; }, );