import express, { Request, Response } from "express"; import fs from "fs"; import path from "path"; import { storage } from "../storage"; import { classifyInternalChat } from "../ai/internal-chat-graph"; import { runInternalChatWorkflow, createAppointmentToday } from "../ai/internal-chat-workflow"; import { resolveAiProvider } from "../ai/llm-factory"; const CHAT_HISTORY_DIR = path.join(__dirname, "..", "..", "chat-history"); function getChatHistoryPath(userId: number): string { return path.join(CHAT_HISTORY_DIR, `user-${userId}.json`); } 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, aiEnabled: settings.aiEnabled ?? true, openAiKey: settings.openAiKey ?? "", openAiEnabled: settings.openAiEnabled ?? false, openAiModel: settings.openAiModel ?? "gpt-5.2", claudeAiKey: settings.claudeAiKey ?? "", claudeAiEnabled: settings.claudeAiEnabled ?? false, claudeAiModel: settings.claudeAiModel ?? "claude-haiku-4-5-20251001", googleAiModel: settings.googleAiModel ?? "gemini-2.5-flash", dentalMgmtKey: settings.dentalMgmtKey ?? "", dentalMgmtEnabled: settings.dentalMgmtEnabled ?? false, 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, aiEnabled } = req.body; if (!apiKey?.trim()) { return res.status(400).json({ message: "apiKey is required" }); } const settings = await storage.upsertAiSettings(userId, apiKey.trim(), aiEnabled); return res.status(200).json({ id: settings.id, apiKey: settings.apiKey, aiEnabled: settings.aiEnabled ?? true }); } catch (err) { return res.status(500).json({ error: "Failed to save AI settings", details: String(err) }); } }); // PUT /api/ai/enabled router.put("/enabled", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const { aiEnabled } = req.body; if (typeof aiEnabled !== "boolean") { return res.status(400).json({ message: "aiEnabled must be a boolean" }); } await storage.setAiEnabled(userId, aiEnabled); return res.status(200).json({ aiEnabled }); } catch (err) { return res.status(500).json({ error: "Failed to save AI enabled setting", details: String(err) }); } }); // PUT /api/ai/provider-key router.put("/provider-key", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const { provider, apiKey } = req.body; if (!["openAi", "claudeAi", "dentalMgmt"].includes(provider)) { return res.status(400).json({ message: "Invalid provider" }); } if (!apiKey?.trim()) { return res.status(400).json({ message: "apiKey is required" }); } await storage.upsertProviderKey(userId, provider, apiKey.trim()); return res.status(200).json({ provider, apiKey: apiKey.trim() }); } catch (err) { return res.status(500).json({ error: "Failed to save provider key", details: String(err) }); } }); // PUT /api/ai/provider-enabled router.put("/provider-enabled", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const { provider, enabled } = req.body; if (!["openAi", "claudeAi", "dentalMgmt"].includes(provider)) { return res.status(400).json({ message: "Invalid provider" }); } if (typeof enabled !== "boolean") { return res.status(400).json({ message: "enabled must be a boolean" }); } await storage.setProviderEnabled(userId, provider, enabled); return res.status(200).json({ provider, enabled }); } catch (err) { return res.status(500).json({ error: "Failed to save provider enabled setting", details: String(err) }); } }); // PUT /api/ai/provider-model router.put("/provider-model", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const { provider, model } = req.body; if (!["claudeAi", "openAi", "googleAi"].includes(provider)) { return res.status(400).json({ message: "Invalid provider" }); } if (!model?.trim()) { return res.status(400).json({ message: "model is required" }); } await storage.setProviderModel(userId, provider, model.trim()); return res.status(200).json({ provider, model: model.trim() }); } catch (err) { return res.status(500).json({ error: "Failed to save provider model", 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) }); } }); // GET /api/ai/cdt-aliases router.get("/cdt-aliases", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const aliases = await storage.getCdtAliases(userId); return res.status(200).json(aliases); } catch (err) { return res.status(500).json({ error: "Failed to fetch CDT aliases", details: String(err) }); } }); // PUT /api/ai/cdt-aliases router.put("/cdt-aliases", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const aliases = req.body; if (!Array.isArray(aliases)) { return res.status(400).json({ message: "Body must be an array of { phrase, cdtCode }" }); } const cleaned = aliases .filter((a: any) => typeof a?.phrase === "string" && typeof a?.cdtCode === "string") .map((a: any) => ({ phrase: a.phrase.trim().toLowerCase(), cdtCode: a.cdtCode.trim().toUpperCase(), })); await storage.saveCdtAliases(userId, cleaned); return res.status(200).json(cleaned); } catch (err) { return res.status(500).json({ error: "Failed to save CDT aliases", details: String(err) }); } }); // POST /api/ai/cdt-aliases/add — add/update a single alias without overwriting others router.post("/cdt-aliases/add", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const { phrase, cdtCode } = req.body; if (typeof phrase !== "string" || typeof cdtCode !== "string") { return res.status(400).json({ message: "Body must be { phrase, cdtCode }" }); } const existing = await storage.getCdtAliases(userId); const newEntry = { phrase: phrase.trim().toLowerCase(), cdtCode: cdtCode.trim().toUpperCase() }; const updated = [...existing.filter((a) => a.phrase !== newEntry.phrase), newEntry]; await storage.saveCdtAliases(userId, updated); return res.status(200).json(newEntry); } catch (err) { return res.status(500).json({ error: "Failed to add CDT alias", 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, history, clientDate } = req.body; if (!message?.trim()) return res.status(400).json({ message: "message is required" }); const aiSettings = await storage.getAiSettings(userId); const activeAi = resolveAiProvider(aiSettings ?? {}); if (!activeAi) { return res.status(200).json({ reply: "AI is not configured. Please add an API key in AI Settings.", }); } const [extraSystemPrompt, customAliases] = await Promise.all([ storage.getInternalChatSystemPrompt(userId), storage.getCdtAliases(userId), ]); const classification = await classifyInternalChat( message.trim(), activeAi.key, extraSystemPrompt || undefined, Array.isArray(history) ? history : [], activeAi.provider, activeAi.model, typeof clientDate === "string" ? clientDate : undefined ); const response = await runInternalChatWorkflow(classification, userId, storage, customAliases); return res.status(200).json(response); } catch (err) { return res.status(500).json({ error: "Internal chat error", details: String(err) }); } }); router.post("/create-appointment-today", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const { patientId, date } = req.body; if (!patientId) return res.status(400).json({ message: "patientId is required" }); const result = await createAppointmentToday(Number(patientId), userId, storage, date ?? undefined); if ("error" in result) return res.status(409).json({ message: result.error }); return res.status(200).json(result); } catch (err) { return res.status(500).json({ error: "Failed to create appointment", details: String(err) }); } }); // ─── Chat history persistence (JSON file per user) ────────────────────────── router.get("/chat-history", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const filePath = getChatHistoryPath(userId); if (!fs.existsSync(filePath)) return res.status(200).json({ messages: [] }); const raw = fs.readFileSync(filePath, "utf-8"); return res.status(200).json(JSON.parse(raw)); } catch (err) { return res.status(500).json({ error: "Failed to load chat history", details: String(err) }); } }); router.put("/chat-history", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const { messages } = req.body; if (!Array.isArray(messages)) return res.status(400).json({ message: "messages array is required" }); if (!fs.existsSync(CHAT_HISTORY_DIR)) fs.mkdirSync(CHAT_HISTORY_DIR, { recursive: true }); fs.writeFileSync(getChatHistoryPath(userId), JSON.stringify({ messages }, null, 2)); return res.status(200).json({ ok: true }); } catch (err) { return res.status(500).json({ error: "Failed to save chat history", details: String(err) }); } }); router.delete("/chat-history", async (req: Request, res: Response): Promise => { try { const userId = req.user?.id; if (!userId) return res.status(401).json({ message: "Unauthorized" }); const filePath = getChatHistoryPath(userId); if (fs.existsSync(filePath)) fs.unlinkSync(filePath); return res.status(200).json({ ok: true }); } catch (err) { return res.status(500).json({ error: "Failed to clear chat history", details: String(err) }); } }); export default router;