|
|
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}`) |
|
|
); |
|
|
|