The scheduling system orchestrates the entire flow from initial time proposal to confirmed date, handling multi-turn SMS negotiations between matched users. It uses Mistral AI to analyze natural language responses, detect acceptances vs. counterproposals, and enforce minimum lead time requirements.
The system progresses through a state machine with 9 distinct phases:
type SchedulingPhase = | "PROPOSING_TO_A" // Generating and sending proposal to User A | "WAITING_FOR_A_REPLY" // Waiting for User A's response | "PROPOSING_TO_B" // Generating and sending proposal to User B | "WAITING_FOR_B_REPLY" // Waiting for User B's response | "WAITING_FOR_A_ALTERNATIVE" // A declined, needs to propose new time | "WAITING_FOR_B_ALTERNATIVE" // B declined, needs to propose new time | "AGREED" // Both confirmed, portal enabled | "FAILED" // Scheduling attempts exhausted | "ESCALATED"; // Flagged for manual intervention
PROPOSING_TO_A └─ Initial proposal sent → WAITING_FOR_A_REPLYWAITING_FOR_A_REPLY ├─ A accepts → PROPOSING_TO_B (or WAITING_FOR_B_REPLY) ├─ A proposes alternative → PROPOSING_TO_B (update proposedSlot) └─ A declines without alternative → WAITING_FOR_A_ALTERNATIVEWAITING_FOR_A_ALTERNATIVE └─ A proposes time → PROPOSING_TO_BPROPOSING_TO_B └─ Proposal sent to B → WAITING_FOR_B_REPLYWAITING_FOR_B_REPLY ├─ B accepts → AGREED (generate venue, enable portal) ├─ B proposes alternative → PROPOSING_TO_A (update proposedSlot) └─ B declines without alternative → WAITING_FOR_B_ALTERNATIVEWAITING_FOR_B_ALTERNATIVE └─ B proposes time → PROPOSING_TO_AAGREED └─ Terminal state: messaging portal activeFAILED └─ Terminal state: scheduling unsuccessfulESCALATED └─ Terminal state: requires manual admin intervention
When a match is created, the system generates an initial date/time suggestion at least 2 days in the future:
src/lib/tpoScheduling.ts
export async function proposeInitialTimeSlot(params: { today: Date;}): Promise<string> { const { today } = params; const minDate = addDays(today, 2); const minDateStr = formatDateFriendly(minDate); const fallback = formatDateFriendly(addDays(today, 3)) + " at 7pm"; const { text } = await generateText({ model: "mistral/mistral-medium", prompt: `You are scheduling a first date for a dating app.Today is ${todayStr}.The earliest allowed date is ${minDateStr} (must be at least 2 full days from today).Suggest ONE specific date and time for a first date. Pick a weekend evening or weekday evening after work — something that naturally fits a casual first date.Rules:- The date must be on or after ${minDateStr}.- Return ONLY the date/time string. No explanation, no quotes.- Format example: "Saturday, March 8th at 7pm"`, maxOutputTokens: 30, }); const cleaned = text.trim().replace(/^["']|["']$/g, ""); if (cleaned.length > 5 && cleaned.length < 80) { return cleaned; } return fallback;}
The AI generates natural-sounding time proposals like “Saturday, March 8th at 7pm” rather than rigid formats.
export interface SchedulingAnalysis { accepted: boolean; // User confirmed the proposed time proposedAlternative: string | null; // User suggested different time tooSoon: boolean; // Proposed date is less than 2 days away needsClarification: boolean; // Intent is ambiguous clarificationQuestion: string | null; // Follow-up to ask user}
// User clearly agrees with no counter-suggestionSignal words: "yes", "yeah", "yep", "sure", "works", "sounds good", "that works", "perfect", "great", "ok", "okay"// Example:{ role: "user", content: "yeah that works for me!" }// → accepted: true
Outcome 2: Alternative Proposed
// User suggests a different time (even if phrased positively)Signal phrases: "how about X", "what about X", "can we do X", "X works better", "X instead", "rather do X"// Example:{ role: "user", content: "how about 8pm instead?" }// → proposedAlternative: "Saturday, March 8th at 8pm"// → proposedDateYMD: "2026-03-08"// Relative date resolution:{ role: "user", content: "can we do next Friday?" }// Today = "Monday, March 3, 2026"// → proposedAlternative: "Friday, March 7th"// → proposedDateYMD: "2026-03-07"
Outcome 3: Needs Clarification
// Intent is genuinely ambiguous// Example:{ role: "user", content: "hmm maybe" }// → needsClarification: true// → clarificationQuestion: "just to confirm — does that time work for you?"
Once both users agree, the system generates a personalized venue suggestion:
src/lib/tpoScheduling.ts
export async function suggestDateSpot(params: { userAProfile: object | null; userBProfile: object | null; city: string; agreedTime: string;}): Promise<string> { const { text } = await generateText({ model: "mistral/mistral-medium", prompt: `You are suggesting a first date venue for two people on a dating app.City: ${city}Date/time: ${agreedTime}Person A's vibe: ${profileSummary(userAProfile)}Person B's vibe: ${profileSummary(userBProfile)}Suggest ONE real, specific establishment in ${city} that exists in real life and matches both people's energy.Rules:- Under 180 characters total- Must include the real venue name and a neighborhood/area- Do NOT suggest a generic vibe-only place (e.g., "a cozy bistro")- If uncertain about niche spots, pick a widely known real place in ${city}- End with a short reason it fits them both- Format: "[real venue name] in [neighborhood/area], [city] — [why it fits you both]"`, maxOutputTokens: 80, }); return text.trim();}
The system validates that suggestions are specific venues, not generic categories:
src/lib/tpoScheduling.ts
function isSpecificVenueSuggestion(value: string): boolean { // Must include "—" separator and "in" for location if (!trimmed.includes("—") || !/\sin\s/i.test(venue)) return false; // Reject generic phrases const genericPhrases = [ "a cozy", "a casual", "a lively", "a rooftop", "a coffee shop", "a wine bar", "a cocktail spot" ]; if (genericPhrases.some((phrase) => lowerVenue.includes(phrase))) return false; // Require at least one capitalized token (proper noun) return /\b[A-Z][a-zA-Z0-9&'.-]*\b/.test(venue);}
If validation fails, the system retries with stricter constraints or returns a hardcoded fallback.