import express, { Request, Response } from "express"; import { storage } from "../storage"; import { classifyInternalChat } from "../ai/internal-chat-graph"; const router = express.Router(); // GET /api/ai/settings router.get("/settings", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const settings = await storage.getAiSettings(userId); if (!settings) return res.status(200).json(null); return res.status(200).json({ id: settings.id, apiKey: settings.apiKey, openPhoneReply: settings.openPhoneReply ?? false, }); } catch (err) { return res.status(500).json({ error: "Failed to fetch AI settings", details: String(err) }); } }); // PUT /api/ai/settings router.put("/settings", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const { apiKey } = req.body; if (!apiKey?.trim()) { return res.status(400).json({ message: "apiKey is required" }); } const settings = await storage.upsertAiSettings(userId, apiKey.trim()); return res.status(200).json({ id: settings.id, apiKey: settings.apiKey }); } catch (err) { return res.status(500).json({ error: "Failed to save AI settings", details: String(err) }); } }); // GET /api/ai/advanced-settings router.get("/advanced-settings", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const openPhoneReply = await storage.getOpenPhoneReply(userId); return res.status(200).json({ openPhoneReply }); } catch (err) { return res.status(500).json({ error: "Failed to fetch advanced settings", details: String(err) }); } }); // PUT /api/ai/advanced-settings router.put("/advanced-settings", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const { openPhoneReply } = req.body; if (typeof openPhoneReply !== "boolean") { return res.status(400).json({ message: "openPhoneReply must be a boolean" }); } await storage.setOpenPhoneReply(userId, openPhoneReply); return res.status(200).json({ openPhoneReply }); } catch (err) { return res.status(500).json({ error: "Failed to save advanced settings", details: String(err) }); } }); // GET /api/ai/chat-templates router.get("/chat-templates", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const templates = await storage.getAiChatTemplates(userId); return res.status(200).json(templates); } catch (err) { return res.status(500).json({ error: "Failed to fetch AI chat templates", details: String(err) }); } }); // PUT /api/ai/chat-templates router.put("/chat-templates", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const { reminderGreeting, newPatientGreeting, generalFallback, rescheduleGreeting, reminderSms } = req.body; await storage.saveAiChatTemplates(userId, { reminderGreeting, newPatientGreeting, generalFallback, rescheduleGreeting, reminderSms }); const updated = await storage.getAiChatTemplates(userId); return res.status(200).json(updated); } catch (err) { return res.status(500).json({ error: "Failed to save AI chat templates", details: String(err) }); } }); // GET /api/ai/internal-chat-settings router.get("/internal-chat-settings", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const systemPrompt = await storage.getInternalChatSystemPrompt(userId); return res.status(200).json({ systemPrompt }); } catch (err) { return res.status(500).json({ error: "Failed to fetch internal chat settings", details: String(err) }); } }); // PUT /api/ai/internal-chat-settings router.put("/internal-chat-settings", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const { systemPrompt } = req.body; if (typeof systemPrompt !== "string") return res.status(400).json({ message: "systemPrompt must be a string" }); await storage.saveInternalChatSystemPrompt(userId, systemPrompt.trim()); return res.status(200).json({ systemPrompt: systemPrompt.trim() }); } catch (err) { return res.status(500).json({ error: "Failed to save internal chat settings", details: String(err) }); } }); // POST /api/ai/internal-chat router.post("/internal-chat", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const { message } = req.body; if (!message?.trim()) return res.status(400).json({ message: "message is required" }); const aiSettings = await storage.getAiSettings(userId); if (!aiSettings?.apiKey) { return res.status(200).json({ reply: "AI is not configured. Please add a Google AI API key in Settings.", }); } const extraSystemPrompt = await storage.getInternalChatSystemPrompt(userId); const classification = await classifyInternalChat(message.trim(), aiSettings.apiKey, extraSystemPrompt || undefined); // Handle navigation intents immediately if (classification.intent === "navigate_claims") { return res.status(200).json({ reply: classification.fallbackReply, action: "navigate", actionData: { url: "/claims" } }); } if (classification.intent === "navigate_schedule") { return res.status(200).json({ reply: classification.fallbackReply, action: "navigate", actionData: { url: "/appointments" } }); } // Handle patient intents — search DB if (classification.intent === "check_eligibility" || classification.intent === "find_patient") { const name = classification.patientName?.trim(); if (!name) { return res.status(200).json({ reply: "Please include the patient's name in your message." }); } const patients = await storage.searchPatients({ filters: { OR: [ { firstName: { contains: name, mode: "insensitive" } }, { lastName: { contains: name, mode: "insensitive" } }, { AND: name.split(/\s+/).map((part: string) => ({ OR: [ { firstName: { contains: part, mode: "insensitive" } }, { lastName: { contains: part, mode: "insensitive" } }, ], })), }, ], }, limit: 5, offset: 0, }); if (!patients || patients.length === 0) { return res.status(200).json({ reply: `No patient found matching "${name}". Try a different spelling or search on the Patients page.`, }); } const patient = patients[0]!; const fullName = `${patient.firstName ?? ""} ${patient.lastName ?? ""}`.trim(); if (classification.intent === "find_patient") { const ins = patient.insuranceProvider ? ` · ${patient.insuranceProvider}` : ""; const id = patient.insuranceId ? ` (ID: ${patient.insuranceId})` : ""; return res.status(200).json({ reply: `Found: ${fullName}${ins}${id}`, action: "show_patient", actionData: { patient: { id: patient.id, firstName: patient.firstName, lastName: patient.lastName, insuranceId: patient.insuranceId ?? null, insuranceProvider: patient.insuranceProvider ?? null, dateOfBirth: patient.dateOfBirth ? patient.dateOfBirth.toISOString().split("T")[0] : null, }, }, }); } // check_eligibility if (!patient.insuranceId) { return res.status(200).json({ reply: `Found ${fullName} but no Member ID is on file. Please add their insurance info first.`, action: "show_patient", actionData: { patient: { id: patient.id, firstName: patient.firstName, lastName: patient.lastName, insuranceId: null, insuranceProvider: patient.insuranceProvider ?? null, dateOfBirth: patient.dateOfBirth ? patient.dateOfBirth.toISOString().split("T")[0] : null, }, }, }); } return res.status(200).json({ reply: `Found ${fullName}. Ready to check eligibility.`, action: "check_eligibility_prefill", actionData: { patient: { id: patient.id, firstName: patient.firstName, lastName: patient.lastName, insuranceId: patient.insuranceId, insuranceProvider: patient.insuranceProvider ?? null, dateOfBirth: patient.dateOfBirth ? patient.dateOfBirth.toISOString().split("T")[0] : null, }, }, }); } // General intent — return Gemini's reply return res.status(200).json({ reply: classification.fallbackReply }); } catch (err) { return res.status(500).json({ error: "Internal chat error", details: String(err) }); } }); export default router;