| | const express = require("express"); |
| | const http = require("http"); |
| | const path = require("path"); |
| | const multer = require("multer"); |
| | const fs = require("fs").promises; |
| | const { Server } = require("socket.io"); |
| | const { ChatGroq } = require("@langchain/groq"); |
| | const { HumanMessage, SystemMessage } = require("@langchain/core/messages"); |
| | const mammoth = require("mammoth"); |
| | const pdf = require("pdf-parse"); |
| | const Tesseract = require("tesseract.js"); |
| | const sharp = require("sharp"); |
| | const cors = require("cors"); |
| |
|
| | const app = express(); |
| | const server = http.createServer(app); |
| | const io = new Server(server, { |
| | cors: { origin: "*" }, |
| | maxHttpBufferSize: 1e8 |
| | }); |
| |
|
| | app.use(cors()); |
| | app.use(express.json()); |
| | app.use(express.static(path.resolve("./public"))); |
| | app.use("/uploads", express.static(path.join(__dirname, "uploads"))); |
| |
|
| | |
| | const storage = multer.diskStorage({ |
| | destination: async (req, file, cb) => { |
| | const uploadDir = path.join(__dirname, "uploads"); |
| | await fs.mkdir(uploadDir, { recursive: true }); |
| | cb(null, uploadDir); |
| | }, |
| | filename: (req, file, cb) => { |
| | const uniqueName = `${Date.now()}-${file.originalname}`; |
| | cb(null, uniqueName); |
| | } |
| | }); |
| |
|
| | const upload = multer({ |
| | storage, |
| | limits: { fileSize: 50 * 1024 * 1024 } |
| | }); |
| |
|
| | |
| | const llm = new ChatGroq({ |
| | model: "llama-3.3-70b-versatile", |
| | temperature: 0.7, |
| | maxTokens: 2000, |
| | maxRetries: 2, |
| | apiKey: process.env.GROQ_API_KEY |
| | }); |
| |
|
| |
|
| | |
| | let rooms = {}; |
| | let users = {}; |
| |
|
| | |
| | const EMERGENCY_KEYWORDS = [ |
| | 'chest pain', 'heart attack', 'can\'t breathe', 'breathless', 'severe bleeding', |
| | 'unconscious', 'stroke', 'paralysis', 'severe headache', 'suicide', |
| | 'overdose', 'seizure', 'choking', 'anaphylaxis', 'severe pain' |
| | ]; |
| |
|
| | |
| | const PATIENT_AI_PROMPT = `You are an AI Medical Assistant helping a PATIENT. Your role: |
| | |
| | **SAFETY-FIRST APPROACH** |
| | 1. **Empathetic Support**: Be warm, reassuring, and supportive |
| | 2. **Simple Language**: Avoid medical jargon, explain in simple terms |
| | 3. **Symptom Clarification**: Ask ONE focused question at a time |
| | 4. **No Premature Conclusions**: Never diagnose or interpret lab results |
| | 5. **Safety Boundaries**: If critical values detected, advise immediate medical attention |
| | 6. **Respond Only When**: |
| | - Patient asks direct questions |
| | - Patient is alone and needs guidance |
| | - Someone mentions @ai |
| | |
| | **RISK CONTROL**: Never share detailed medical analysis. Acknowledge uploads and reassure.`; |
| |
|
| | const DOCTOR_AI_PROMPT = `You are an AI Medical Assistant helping a DOCTOR. Your role: |
| | |
| | **CLINICAL-GRADE ANALYSIS** |
| | 1. **Detailed Insights**: Provide comprehensive medical analysis |
| | 2. **Critical Findings**: Highlight abnormal values, red flags with clinical context |
| | 3. **Medical Terminology**: Use appropriate professional language |
| | 4. **Evidence-Based**: Reference standard clinical thresholds |
| | 5. **Explainable AI**: Always explain WHY a finding is significant |
| | 6. **Respond Only When**: |
| | - Doctor asks about files/reports |
| | - Doctor mentions @ai |
| | - Doctor needs clinical summary |
| | |
| | **TRANSPARENCY**: Provide clear reasoning for all flagged findings with confidence levels.`; |
| |
|
| | |
| | async function analyzeFileWithXAI(content, fileName, previousReports = []) { |
| | const analysisPrompt = `Analyze this medical report with EXPLAINABLE AI principles: |
| | |
| | File: ${fileName} |
| | Content: ${content.substring(0, 3000)} |
| | |
| | ${previousReports.length > 0 ? ` |
| | **TEMPORAL CONTEXT** (Previous Reports): |
| | ${previousReports.map((r, i) => `Report ${i+1} (${r.date}): ${r.keyFindings}`).join('\n')} |
| | ` : ''} |
| | |
| | Provide analysis in this EXACT format: |
| | |
| | **CLINICAL SUMMARY** |
| | β’ Main diagnosis/finding (1 line) |
| | |
| | **CRITICAL FINDINGS** |
| | β’ [Value/Finding]: [Normal Range] β [Current Value] β [Deviation %] |
| | Reason: [Clinical explanation] |
| | Confidence: [High/Medium/Low] |
| | |
| | **TEMPORAL TRENDS** (if previous data available) |
| | β’ [Parameter]: [Previous β Current] β [Trend Analysis] |
| | |
| | **IMMEDIATE CONCERNS** |
| | β’ [Priority level]: [Specific concern] |
| | |
| | **RECOMMENDATIONS** |
| | β’ [Actionable next steps] |
| | |
| | Be concise, clinical, and ALWAYS explain the "why" behind findings.`; |
| |
|
| | try { |
| | const analysis = await llm.invoke([ |
| | new SystemMessage("You are a clinical AI analyzer specializing in explainable medical insights."), |
| | new HumanMessage(analysisPrompt) |
| | ]); |
| | return analysis.content; |
| | } catch (error) { |
| | console.error("XAI Analysis error:", error); |
| | return "Unable to analyze with full explainability."; |
| | } |
| | } |
| |
|
| | |
| | function extractTemporalData(room) { |
| | if (!room.files || room.files.length < 2) return []; |
| | |
| | return room.files.map(f => ({ |
| | name: f.name, |
| | date: f.uploadedAt, |
| | keyFindings: f.analysis ? f.analysis.substring(0, 200) : "No analysis", |
| | content: f.content.substring(0, 500) |
| | })); |
| | } |
| |
|
| | async function performTemporalAnalysis(currentContent, fileName, room) { |
| | const previousReports = extractTemporalData(room); |
| | |
| | if (previousReports.length === 0) { |
| | return await analyzeFileWithXAI(currentContent, fileName, []); |
| | } |
| |
|
| | const temporalPrompt = `Perform TEMPORAL HEALTH INTELLIGENCE analysis: |
| | |
| | **CURRENT REPORT**: ${fileName} |
| | ${currentContent.substring(0, 2000)} |
| | |
| | **HISTORICAL DATA**: |
| | ${previousReports.map((r, i) => ` |
| | Report ${i+1} - ${new Date(r.date).toLocaleDateString()}: |
| | ${r.keyFindings} |
| | `).join('\n')} |
| | |
| | Analyze: |
| | 1. **Longitudinal Trends**: Compare current vs historical values |
| | 2. **Progression/Deterioration**: Identify gradual changes over time |
| | 3. **Early Warning Signs**: Flag subtle patterns that indicate future risk |
| | 4. **Clinical Significance**: Is this progression normal or concerning? |
| | |
| | Format as structured clinical analysis with temporal context.`; |
| |
|
| | try { |
| | const analysis = await llm.invoke([ |
| | new SystemMessage("You are a temporal medical intelligence analyzer specializing in longitudinal health trends."), |
| | new HumanMessage(temporalPrompt) |
| | ]); |
| | return analysis.content; |
| | } catch (error) { |
| | console.error("Temporal analysis error:", error); |
| | return await analyzeFileWithXAI(currentContent, fileName, previousReports); |
| | } |
| | } |
| |
|
| | |
| | async function detectEmergency(message, userRole) { |
| | const messageLower = message.toLowerCase(); |
| | |
| | |
| | const hasEmergencyKeyword = EMERGENCY_KEYWORDS.some(keyword => |
| | messageLower.includes(keyword) |
| | ); |
| |
|
| | if (!hasEmergencyKeyword) return { isEmergency: false }; |
| |
|
| | |
| | const emergencyPrompt = `Analyze this message for medical emergency indicators: |
| | |
| | Message: "${message}" |
| | |
| | Classify emergency level: |
| | - CRITICAL: Immediate life threat (chest pain, can't breathe, severe bleeding, stroke symptoms) |
| | - HIGH: Urgent medical attention needed within hours |
| | - MODERATE: Medical evaluation needed soon |
| | - LOW: Non-emergency concern |
| | |
| | Respond ONLY with JSON: |
| | { |
| | "level": "CRITICAL|HIGH|MODERATE|LOW", |
| | "reasoning": "brief explanation", |
| | "urgentAdvice": "immediate action to take" |
| | }`; |
| |
|
| | try { |
| | const response = await llm.invoke([ |
| | new SystemMessage("You are an emergency medical triage AI. Respond ONLY with valid JSON."), |
| | new HumanMessage(emergencyPrompt) |
| | ]); |
| |
|
| | const result = JSON.parse(response.content.replace(/```json|```/g, '').trim()); |
| | |
| | return { |
| | isEmergency: result.level === "CRITICAL" || result.level === "HIGH", |
| | level: result.level, |
| | reasoning: result.reasoning, |
| | urgentAdvice: result.urgentAdvice |
| | }; |
| | } catch (error) { |
| | console.error("Emergency detection error:", error); |
| | return { isEmergency: hasEmergencyKeyword, level: "HIGH", reasoning: "Keyword detected" }; |
| | } |
| | } |
| |
|
| | |
| | async function generateClinicalDocumentation(roomId) { |
| | const room = rooms[roomId]; |
| | if (!room) return null; |
| |
|
| | const conversationHistory = room.messages |
| | .filter(m => m.role === 'Patient' || m.role === 'Doctor') |
| | .map(m => `${m.role}: ${m.content}`) |
| | .join('\n'); |
| |
|
| | const filesSummary = room.files |
| | .map(f => `- ${f.name}: ${f.analysis || 'No analysis'}`) |
| | .join('\n'); |
| |
|
| | const docPrompt = `Generate structured clinical documentation from this consultation: |
| | |
| | **CONVERSATION**: |
| | ${conversationHistory} |
| | |
| | **UPLOADED FILES**: |
| | ${filesSummary} |
| | |
| | Generate SOAP NOTE format: |
| | |
| | **SUBJECTIVE** |
| | - Chief Complaint: [main issue] |
| | - History of Present Illness: [brief narrative] |
| | - Review of Systems: [relevant findings] |
| | |
| | **OBJECTIVE** |
| | - Vital signs/Reports: [from uploaded files] |
| | - Physical findings: [mentioned in chat] |
| | |
| | **ASSESSMENT** |
| | - Primary diagnosis: [clinical impression] |
| | - Differential diagnoses: [alternatives] |
| | |
| | **PLAN** |
| | - Investigations: [tests ordered] |
| | - Treatment: [medications/interventions] |
| | - Follow-up: [next steps] |
| | |
| | Keep concise and clinically accurate.`; |
| |
|
| | try { |
| | const documentation = await llm.invoke([ |
| | new SystemMessage("You are a medical documentation AI specializing in SOAP notes and clinical summaries."), |
| | new HumanMessage(docPrompt) |
| | ]); |
| | return documentation.content; |
| | } catch (error) { |
| | console.error("Documentation generation error:", error); |
| | return null; |
| | } |
| | } |
| |
|
| | |
| | async function extractTextFromImage(imagePath) { |
| | try { |
| | console.log("Starting OCR:", imagePath); |
| | const processedPath = imagePath + "_processed.jpg"; |
| | await sharp(imagePath) |
| | .greyscale() |
| | .normalize() |
| | .sharpen() |
| | .toFile(processedPath); |
| |
|
| | const { data: { text } } = await Tesseract.recognize(processedPath, 'eng'); |
| | |
| | try { await fs.unlink(processedPath); } catch (e) {} |
| | |
| | console.log("OCR completed, text length:", text.length); |
| | return text.trim(); |
| | } catch (error) { |
| | console.error("OCR Error:", error); |
| | return ""; |
| | } |
| | } |
| |
|
| | |
| | async function extractFileContent(filePath, mimeType) { |
| | try { |
| | console.log("Extracting:", filePath, mimeType); |
| | |
| | if (mimeType === "application/pdf") { |
| | const dataBuffer = await fs.readFile(filePath); |
| | const pdfData = await pdf(dataBuffer); |
| | return pdfData.text; |
| | } else if (mimeType.includes("word") || mimeType.includes("document")) { |
| | const result = await mammoth.extractRawText({ path: filePath }); |
| | return result.value; |
| | } else if (mimeType.includes("text")) { |
| | return await fs.readFile(filePath, "utf-8"); |
| | } else if (mimeType.includes("image")) { |
| | const ocrText = await extractTextFromImage(filePath); |
| | return ocrText.length > 10 ? ocrText : "[Image - no text detected]"; |
| | } |
| | return "[Unsupported format]"; |
| | } catch (error) { |
| | console.error("Extraction error:", error); |
| | return "[Extraction failed]"; |
| | } |
| | } |
| |
|
| | |
| | async function getAIResponse(roomId, userMessage, userRole, isFileQuery = false, emergencyContext = null) { |
| | const room = rooms[roomId]; |
| | if (!room) return "Room not found"; |
| |
|
| | |
| | const systemPrompt = userRole === "doctor" ? DOCTOR_AI_PROMPT : PATIENT_AI_PROMPT; |
| | |
| | const roleMessages = room.messages.filter(m => |
| | !m.forRole || m.forRole === userRole || (!m.forRole && m.role !== 'AI Assistant') |
| | ); |
| | |
| | let context = `Room: ${roomId} |
| | User Role: ${userRole} |
| | Patient: ${room.patient || "Waiting"} |
| | Doctor: ${room.doctor || "Not yet joined"} |
| | |
| | ${emergencyContext ? `π¨ EMERGENCY CONTEXT: ${emergencyContext.reasoning}\nLevel: ${emergencyContext.level}` : ''} |
| | |
| | Recent messages (last 5): |
| | ${roleMessages.slice(-5).map(m => `${m.role}: ${m.content}`).join("\n")}`; |
| |
|
| | |
| | if (userRole === "doctor" && isFileQuery && room.files.length > 0) { |
| | context += `\n\n**CLINICAL FILES** (with XAI explanations):\n${room.files.map((f, i) => |
| | `${i+1}. ${f.name}\n Analysis: ${f.analysis}\n Key content: ${f.content.substring(0, 400)}` |
| | ).join("\n\n")}`; |
| | } else if (userRole === "patient" && room.files.length > 0) { |
| | |
| | context += `\n\n**FILES UPLOADED**: ${room.files.map(f => f.name).join(', ')} |
| | Note: Detailed medical analysis is being reviewed by your doctor.`; |
| | } |
| |
|
| | const messages = [ |
| | new SystemMessage(systemPrompt), |
| | new SystemMessage(context), |
| | new HumanMessage(`[${userRole}]: ${userMessage}`) |
| | ]; |
| |
|
| | try { |
| | const response = await llm.invoke(messages); |
| | return response.content; |
| | } catch (error) { |
| | console.error("AI Error:", error); |
| | return "I'm having trouble responding. Please try again."; |
| | } |
| | } |
| |
|
| | |
| | app.post("/upload", upload.single("file"), async (req, res) => { |
| | try { |
| | const { roomId, uploadedBy, uploaderRole } = req.body; |
| | const file = req.file; |
| |
|
| | if (!file || !roomId) { |
| | return res.status(400).json({ error: "File and roomId required" }); |
| | } |
| |
|
| | console.log("Upload:", file.originalname, "by", uploadedBy, "in", roomId); |
| |
|
| | const content = await extractFileContent(file.path, file.mimetype); |
| | console.log("Content extracted, length:", content.length); |
| |
|
| | |
| | let analysis = ""; |
| | if (content && content.length > 20 && !content.includes("no text detected")) { |
| | if (rooms[roomId]) { |
| | analysis = await performTemporalAnalysis(content, file.originalname, rooms[roomId]); |
| | } else { |
| | analysis = await analyzeFileWithXAI(content, file.originalname, []); |
| | } |
| | } |
| |
|
| | const fileInfo = { |
| | name: file.originalname, |
| | path: file.path, |
| | url: `/uploads/${file.filename}`, |
| | type: file.mimetype, |
| | content: content.substring(0, 5000), |
| | analysis: analysis, |
| | uploadedAt: new Date().toISOString(), |
| | uploadedBy: uploadedBy || "Unknown" |
| | }; |
| |
|
| | if (rooms[roomId]) { |
| | rooms[roomId].files.push(fileInfo); |
| | |
| | |
| | const fileMessage = { |
| | role: uploadedBy || "User", |
| | nickname: uploadedBy, |
| | content: `π Uploaded: ${file.originalname}`, |
| | timestamp: new Date().toISOString(), |
| | fileData: { |
| | name: file.originalname, |
| | url: fileInfo.url, |
| | type: file.mimetype, |
| | analysis: analysis |
| | }, |
| | isFile: true |
| | }; |
| |
|
| | rooms[roomId].messages.push(fileMessage); |
| | io.to(roomId).emit("chat-message", fileMessage); |
| | |
| | |
| | io.to(roomId).emit("files-updated", { files: rooms[roomId].files }); |
| |
|
| | |
| | if (content && content.length > 20) { |
| | setTimeout(() => { |
| | const doctorSocketId = Object.keys(users).find( |
| | sid => users[sid].roomId === roomId && users[sid].role === "doctor" |
| | ); |
| | |
| | if (doctorSocketId && rooms[roomId].doctor) { |
| | const doctorAiMessage = `π¬ **Clinical Analysis** (with XAI)\n\n${analysis}`; |
| | io.to(doctorSocketId).emit("ai-message", { |
| | message: doctorAiMessage, |
| | isPrivate: true, |
| | forRole: "doctor" |
| | }); |
| | } |
| | }, 1000); |
| | } |
| |
|
| | if (uploaderRole === "patient") { |
| | setTimeout(() => { |
| | const patientSocketId = Object.keys(users).find( |
| | sid => users[sid].nickname === uploadedBy && users[sid].roomId === roomId |
| | ); |
| | |
| | if (patientSocketId) { |
| | const patientAiMessage = `β
I've received "${file.originalname}". Your doctor will review it shortly.`; |
| | io.to(patientSocketId).emit("ai-message", { |
| | message: patientAiMessage, |
| | isPrivate: true, |
| | forRole: "patient" |
| | }); |
| | } |
| | }, 500); |
| | } |
| | } |
| |
|
| | res.json({ success: true, file: fileInfo }); |
| | } catch (error) { |
| | console.error("Upload error:", error); |
| | res.status(500).json({ error: "Upload failed: " + error.message }); |
| | } |
| | }); |
| |
|
| | |
| | app.post("/generate-documentation", async (req, res) => { |
| | try { |
| | const { roomId } = req.body; |
| | if (!roomId || !rooms[roomId]) { |
| | return res.status(400).json({ error: "Invalid room ID" }); |
| | } |
| |
|
| | const documentation = await generateClinicalDocumentation(roomId); |
| | res.json({ success: true, documentation }); |
| | } catch (error) { |
| | console.error("Documentation error:", error); |
| | res.status(500).json({ error: "Documentation generation failed" }); |
| | } |
| | }); |
| |
|
| | |
| | io.on("connection", (socket) => { |
| | console.log("Connected:", socket.id); |
| |
|
| | socket.on("join-room", async ({ roomId, nickname, role }) => { |
| | socket.join(roomId); |
| | users[socket.id] = { nickname, role, roomId }; |
| |
|
| | if (!rooms[roomId]) { |
| | rooms[roomId] = { |
| | patient: null, |
| | doctor: null, |
| | messages: [], |
| | files: [], |
| | patientData: {}, |
| | emergencyMode: false |
| | }; |
| | } |
| |
|
| | if (role === "patient" && !rooms[roomId].patient) { |
| | rooms[roomId].patient = nickname; |
| | } else if (role === "doctor" && !rooms[roomId].doctor) { |
| | rooms[roomId].doctor = nickname; |
| | } |
| |
|
| | socket.emit("room-history", { |
| | messages: rooms[roomId].messages.filter(m => !m.forRole), |
| | files: rooms[roomId].files |
| | }); |
| |
|
| | io.to(roomId).emit("user-joined", { |
| | nickname, |
| | role, |
| | patient: rooms[roomId].patient, |
| | doctor: rooms[roomId].doctor |
| | }); |
| |
|
| | |
| | let greeting = ""; |
| | if (role === "patient") { |
| | greeting = `Hello ${nickname}! π I'm here to help guide you. What brings you in today?`; |
| | } else if (role === "doctor") { |
| | greeting = `Welcome Dr. ${nickname}! π¨ββοΈ Clinical analysis tools ready. Use "Generate SOAP Note" for documentation.`; |
| | |
| | |
| | if (rooms[roomId].messages.length > 0 || rooms[roomId].files.length > 0) { |
| | setTimeout(async () => { |
| | const briefing = await getAIResponse( |
| | roomId, |
| | "Provide a 3-point clinical summary: chief complaint, temporal trends from files, critical findings.", |
| | "doctor", |
| | true |
| | ); |
| | |
| | socket.emit("ai-message", { |
| | message: `π **Clinical Briefing**:\n${briefing}`, |
| | isPrivate: true, |
| | forRole: "doctor" |
| | }); |
| | }, 1000); |
| | } |
| | } |
| |
|
| | if (greeting) { |
| | socket.emit("ai-message", { |
| | message: greeting, |
| | isPrivate: true, |
| | forRole: role |
| | }); |
| | } |
| | }); |
| |
|
| | socket.on("chat-message", async ({ roomId, message }) => { |
| | const user = users[socket.id]; |
| | if (!user || !rooms[roomId]) return; |
| |
|
| | |
| | const isAIRequest = message.toLowerCase().includes('@ai'); |
| |
|
| | |
| | const emergencyCheck = await detectEmergency(message, user.role); |
| |
|
| | |
| | if (!isAIRequest) { |
| | const chatMessage = { |
| | role: user.role === "patient" ? "Patient" : "Doctor", |
| | nickname: user.nickname, |
| | content: message, |
| | timestamp: new Date().toISOString(), |
| | isEmergency: emergencyCheck.isEmergency |
| | }; |
| |
|
| | rooms[roomId].messages.push(chatMessage); |
| | io.to(roomId).emit("chat-message", chatMessage); |
| | } |
| |
|
| | |
| | if (emergencyCheck.isEmergency) { |
| | rooms[roomId].emergencyMode = true; |
| | |
| | |
| | if (user.role === "patient") { |
| | const urgentMessage = `π¨ **URGENT MEDICAL ATTENTION NEEDED**\n\n${emergencyCheck.urgentAdvice}\n\nCall emergency services (911) immediately if symptoms worsen.`; |
| | socket.emit("ai-message", { |
| | message: urgentMessage, |
| | isPrivate: true, |
| | forRole: "patient", |
| | isEmergency: true |
| | }); |
| | } |
| |
|
| | |
| | const doctorSocketId = Object.keys(users).find( |
| | sid => users[sid].roomId === roomId && users[sid].role === "doctor" |
| | ); |
| | |
| | if (doctorSocketId) { |
| | const doctorAlert = `π¨ **EMERGENCY ALERT**\n\nPatient: ${user.nickname}\nLevel: ${emergencyCheck.level}\nReason: ${emergencyCheck.reasoning}\n\nMessage: "${message}"\n\nImmediate evaluation required.`; |
| | io.to(doctorSocketId).emit("ai-message", { |
| | message: doctorAlert, |
| | isPrivate: true, |
| | forRole: "doctor", |
| | isEmergency: true |
| | }); |
| | } |
| |
|
| | return; |
| | } |
| |
|
| | |
| | if (isAIRequest) { |
| | const messageText = message.toLowerCase(); |
| | const isFileQuery = |
| | messageText.includes("report") || |
| | messageText.includes("file") || |
| | messageText.includes("result") || |
| | messageText.includes("test") || |
| | messageText.includes("value") || |
| | messageText.includes("finding") || |
| | messageText.includes("trend"); |
| |
|
| | setTimeout(async () => { |
| | const aiResponse = await getAIResponse(roomId, message, user.role, isFileQuery); |
| | |
| | |
| | socket.emit("ai-message", { |
| | message: aiResponse, |
| | isPrivate: true, |
| | forRole: user.role |
| | }); |
| | }, 1500); |
| | } else { |
| | |
| | const messageText = message.toLowerCase(); |
| | const isFileQuery = |
| | messageText.includes("report") || |
| | messageText.includes("file") || |
| | messageText.includes("result") || |
| | messageText.includes("test") || |
| | messageText.includes("value") || |
| | messageText.includes("finding") || |
| | messageText.includes("trend"); |
| |
|
| | const shouldAIRespond = |
| | (user.role === "patient" && !rooms[roomId].doctor && message.endsWith("?")) || |
| | (user.role === "doctor" && isFileQuery); |
| |
|
| | if (shouldAIRespond) { |
| | setTimeout(async () => { |
| | const aiResponse = await getAIResponse(roomId, message, user.role, isFileQuery); |
| | |
| | socket.emit("ai-message", { |
| | message: aiResponse, |
| | isPrivate: true, |
| | forRole: user.role |
| | }); |
| | }, 1500); |
| | } |
| | } |
| | }); |
| |
|
| | |
| | socket.on("request-documentation", async ({ roomId }) => { |
| | const user = users[socket.id]; |
| | if (!user || user.role !== "doctor") return; |
| |
|
| | const documentation = await generateClinicalDocumentation(roomId); |
| | if (documentation) { |
| | socket.emit("documentation-generated", { documentation }); |
| | } |
| | }); |
| |
|
| | socket.on("typing", ({ roomId }) => { |
| | const user = users[socket.id]; |
| | if (user) { |
| | socket.to(roomId).emit("user-typing", { nickname: user.nickname }); |
| | } |
| | }); |
| |
|
| | socket.on("disconnect", () => { |
| | const user = users[socket.id]; |
| | if (user) { |
| | const { roomId, nickname, role } = user; |
| | |
| | if (rooms[roomId]) { |
| | if (role === "patient") rooms[roomId].patient = null; |
| | if (role === "doctor") rooms[roomId].doctor = null; |
| |
|
| | io.to(roomId).emit("user-left", { |
| | nickname, |
| | role, |
| | patient: rooms[roomId].patient, |
| | doctor: rooms[roomId].doctor |
| | }); |
| | } |
| |
|
| | delete users[socket.id]; |
| | } |
| | }); |
| | }); |
| |
|
| | const PORT = process.env.PORT || 7860; |
| | server.listen(PORT, "0.0.0.0", () => |
| | console.log(`π₯ Enhanced Medical Chat Server running on port ${PORT}`) |
| | ); |
| |
|