/* ===== Assnani Dental Chatbot — Conversation Engine ===== */ const Chatbot = (() => { // --- State --- let state = 'WELCOME'; let symptoms = { has_pain: false, pain_location: '', pain_type: '', pain_intensity: 0, pain_duration: '', pain_triggers: [], has_swelling: false, swelling_severity: '', has_fever: false, difficulty_opening: false, has_trauma: false, has_broken_tooth: false, previous_root_canal: false, last_visit: '', recent_extraction: false }; let detectionData = null; let analysisResult = null; let geminiReport = null; let uploadedFiles = []; let imageDimensions = { width: 0, height: 0 }; // --- DOM refs --- const msgContainer = document.getElementById('messages-container'); const typingIndicator = document.getElementById('typing-indicator'); const quickReplies = document.getElementById('quick-replies-area'); const uploadArea = document.getElementById('upload-area'); const inputArea = document.getElementById('input-area'); const userInput = document.getElementById('user-input'); const btnSend = document.getElementById('btn-send'); const sidePanel = document.getElementById('side-panel'); const btnTogglePanel = document.getElementById('btn-toggle-panel'); const btnClosePanel = document.getElementById('btn-close-panel'); const reportOverlay = document.getElementById('report-overlay'); const reportBody = document.getElementById('report-body'); const uploadDropzone = document.getElementById('upload-dropzone'); const fileInput = document.getElementById('xray-file-input'); const uploadPreview = document.getElementById('upload-preview'); const previewGrid = document.getElementById('upload-preview-grid'); const btnUploadAdd = document.getElementById('btn-upload-add'); const btnUploadSubmit = document.getElementById('btn-upload-submit'); const btnUploadCancel = document.getElementById('btn-upload-cancel'); // --- Helpers --- function scrollToBottom() { setTimeout(() => { msgContainer.scrollTop = msgContainer.scrollHeight; }, 50); } function addMessage(text, type = 'bot', html = false) { const msg = document.createElement('div'); msg.className = `message message-${type}`; const avatar = type === 'bot' ? '🤖' : '👤'; msg.innerHTML = `
${avatar}
${html ? text : escapeHtml(text)}
`; msgContainer.appendChild(msg); scrollToBottom(); } function escapeHtml(t) { const d = document.createElement('div'); d.textContent = t; return d.innerHTML; } function showTyping() { typingIndicator.style.display = 'flex'; scrollToBottom(); } function hideTyping() { typingIndicator.style.display = 'none'; } function botSay(text, html = false, delay = 800) { return new Promise(resolve => { showTyping(); setTimeout(() => { hideTyping(); addMessage(text, 'bot', html); resolve(); }, delay); }); } function showQuickReplies(options) { quickReplies.innerHTML = ''; options.forEach(opt => { const btn = document.createElement('button'); btn.className = 'quick-reply-btn'; btn.textContent = typeof opt === 'string' ? opt : opt.label; btn.addEventListener('click', () => { const val = typeof opt === 'string' ? opt : opt.value; addMessage(typeof opt === 'string' ? opt : opt.label, 'user'); hideQuickReplies(); handleInput(val); }); quickReplies.appendChild(btn); }); quickReplies.style.display = 'flex'; scrollToBottom(); } function hideQuickReplies() { quickReplies.style.display = 'none'; quickReplies.innerHTML = ''; } function showUpload() { uploadArea.style.display = 'block'; scrollToBottom(); } function hideUpload() { uploadArea.style.display = 'none'; uploadPreview.style.display = 'none'; uploadDropzone.style.display = 'flex'; if(previewGrid) previewGrid.innerHTML = ''; } function showInput(placeholder) { inputArea.style.display = 'block'; userInput.placeholder = placeholder || 'Type your message...'; userInput.focus(); } function hideInput() { inputArea.style.display = 'none'; } // --- State Machine --- async function transition(nextState, data) { state = nextState; switch(state) { case 'WELCOME': await botSay("Hello! 👋 I'm Assnani AI, your dental symptom assistant.", true, 600); await botSay("I'll ask you a few questions about your dental health to assess your situation and recommend whether an X-ray examination might be needed.", false, 1000); await botSay("Let's start — are you currently experiencing any dental pain?", true, 800); showQuickReplies(['Yes, I have pain', 'No pain']); break; case 'PAIN_LOCATION': symptoms.has_pain = true; await botSay("I'm sorry to hear that. Let me help figure out what's going on.", false, 600); await botSay("Where exactly do you feel the pain? Please select the area:", false, 800); // Render tooth chart hideQuickReplies(); quickReplies.style.display = 'flex'; ToothChart.render(quickReplies, (key, label) => { symptoms.pain_location = key; addMessage(label, 'user'); hideQuickReplies(); transition('PAIN_TYPE'); }); scrollToBottom(); break; case 'PAIN_TYPE': await botSay("How would you describe the pain?", false, 600); showQuickReplies([ { label: '⚡ Sharp', value: 'sharp' }, { label: 'đŸ˜Ŗ Dull / Aching', value: 'dull' }, { label: '💓 Throbbing', value: 'throbbing' }, { label: '🧊 Sensitivity (hot/cold)', value: 'sensitivity' } ]); break; case 'PAIN_INTENSITY': symptoms.pain_type = data; await botSay("On a scale of 1 to 10, how intense is the pain?", true, 600); showQuickReplies( [1,2,3,4,5,6,7,8,9,10].map(n => ({ label: `${n <= 3 ? '😊' : n <= 6 ? '😐' : n <= 8 ? 'đŸ˜Ŗ' : 'đŸ˜Ģ'} ${n}`, value: String(n) })) ); break; case 'PAIN_DURATION': symptoms.pain_intensity = parseInt(data); await botSay("How long have you been experiencing this pain?", false, 600); showQuickReplies([ { label: '📅 Just today', value: 'today' }, { label: '📅 1-3 days', value: '1-3 days' }, { label: '📅 3-7 days', value: '3-7 days' }, { label: '📅 More than a week', value: '1+ week' } ]); break; case 'PAIN_TRIGGERS': symptoms.pain_duration = data; await botSay("What triggers or worsens the pain? (Select all that apply, then tap Done)", true, 600); renderMultiSelect([ { label: 'đŸ”Ĩ Hot food/drinks', value: 'hot' }, { label: '🧊 Cold food/drinks', value: 'cold' }, { label: 'đŸĻˇ Biting / Chewing', value: 'biting' }, { label: 'đŸ’Ĩ Spontaneous (no trigger)', value: 'spontaneous' }, { label: 'đŸŦ Sweet foods', value: 'sweet' } ], (selected) => { symptoms.pain_triggers = selected; transition('SWELLING_ASK'); }); break; case 'NO_PAIN': symptoms.has_pain = false; await botSay("That's good! Let me ask a few more questions to be thorough.", false, 700); transition('SWELLING_ASK'); break; case 'SWELLING_ASK': await botSay("Do you have any swelling in your mouth, jaw, or face?", true, 700); showQuickReplies(['Yes, I have swelling', 'No swelling']); break; case 'SWELLING_DETAILS': symptoms.has_swelling = true; await botSay("How severe is the swelling?", false, 600); showQuickReplies([ { label: '🟡 Mild', value: 'mild' }, { label: '🟠 Moderate', value: 'moderate' }, { label: '🔴 Severe', value: 'severe' } ]); break; case 'FEVER_ASK': symptoms.swelling_severity = data; await botSay("Do you have a fever or difficulty opening your mouth?", true, 600); showQuickReplies([ { label: '🤒 Yes, fever', value: 'fever' }, { label: '😮 Difficulty opening mouth', value: 'trismus' }, { label: '🤒😮 Both', value: 'both' }, { label: 'Neither', value: 'neither' } ]); break; case 'HISTORY': if (data === 'fever') { symptoms.has_fever = true; } else if (data === 'trismus') { symptoms.difficulty_opening = true; } else if (data === 'both') { symptoms.has_fever = true; symptoms.difficulty_opening = true; } await botSay("A few questions about your dental history:", true, 700); await botSay("When was your last dental visit?", false, 600); showQuickReplies([ { label: '< 6 months ago', value: '< 6 months' }, { label: '6-12 months ago', value: '6-12 months' }, { label: 'Over a year ago', value: '1+ year' }, { label: 'Never / Can\'t remember', value: 'never' } ]); break; case 'HISTORY_TRAUMA': symptoms.last_visit = data; await botSay("Have you experienced any of the following?", false, 600); showQuickReplies([ { label: 'đŸ’Ĩ Recent trauma / injury', value: 'trauma' }, { label: '🔧 Previous root canal (same area)', value: 'root_canal' }, { label: '💔 Broken / chipped tooth', value: 'broken' }, { label: 'None of the above', value: 'none' } ]); break; case 'ANALYSIS': if (data === 'trauma') symptoms.has_trauma = true; else if (data === 'root_canal') symptoms.previous_root_canal = true; else if (data === 'broken') symptoms.has_broken_tooth = true; await botSay("Thank you for answering all the questions! Let me analyze your symptoms... 🔍", false, 600); await performAnalysis(); break; case 'XRAY_ASK': await botSay("Would you like to upload a dental X-ray for AI analysis? I can correlate your symptoms with the X-ray findings.", true, 800); showQuickReplies([ { label: '📤 Upload X-ray', value: 'upload' }, { label: 'â­ī¸ Skip — Show report', value: 'skip' } ]); break; case 'XRAY_UPLOAD': await botSay("Please upload your dental X-ray image(s) below. You can upload multiple files — supported formats: PNG, JPG, PDF.", true, 600); showUpload(); break; case 'XRAY_ANALYZING': { const filesToAnalyze = [...uploadedFiles]; // capture BEFORE hideUpload clears the array hideUpload(); const fileCount = filesToAnalyze.length; await botSay(`Sending ${fileCount} file${fileCount>1?'s':''} to AI detection model... This may take a moment.`, true, 400); await analyzeXray(filesToAnalyze); break; } case 'CORRELATION': await performCorrelation(); break; case 'REPORT': await generateReport(); break; } } // --- Multi-Select Helper --- function renderMultiSelect(options, onDone) { quickReplies.innerHTML = ''; const selected = new Set(); options.forEach(opt => { const btn = document.createElement('button'); btn.className = 'quick-reply-btn'; btn.textContent = opt.label; btn.addEventListener('click', () => { if (selected.has(opt.value)) { selected.delete(opt.value); btn.classList.remove('selected'); } else { selected.add(opt.value); btn.classList.add('selected'); } }); quickReplies.appendChild(btn); }); const doneBtn = document.createElement('button'); doneBtn.className = 'quick-reply-btn'; doneBtn.style.cssText = 'background:var(--accent);color:var(--bg-primary);font-weight:700;border-color:var(--accent);'; doneBtn.textContent = '✓ Done'; doneBtn.addEventListener('click', () => { const sel = Array.from(selected); addMessage(sel.length ? sel.join(', ') : 'No specific trigger', 'user'); hideQuickReplies(); onDone(sel); }); quickReplies.appendChild(doneBtn); quickReplies.style.display = 'flex'; scrollToBottom(); } // --- Input Handler --- function handleInput(value) { const v = value.toLowerCase().trim(); switch(state) { case 'WELCOME': if (v.includes('yes') || v.includes('pain')) transition('PAIN_LOCATION'); else transition('NO_PAIN'); break; case 'PAIN_TYPE': transition('PAIN_INTENSITY', v); break; case 'PAIN_INTENSITY': transition('PAIN_DURATION', v); break; case 'PAIN_DURATION': transition('PAIN_TRIGGERS', v); break; case 'SWELLING_ASK': if (v.includes('yes') || v.includes('swelling')) transition('SWELLING_DETAILS'); else { symptoms.has_swelling = false; transition('HISTORY'); } break; case 'SWELLING_DETAILS': transition('FEVER_ASK', v); break; case 'FEVER_ASK': transition('HISTORY', v); break; case 'HISTORY': transition('HISTORY_TRAUMA', v); break; case 'HISTORY_TRAUMA': transition('ANALYSIS', v); break; case 'XRAY_ASK': if (v.includes('upload')) transition('XRAY_UPLOAD'); else transition('REPORT'); break; } } // --- API Calls --- async function performAnalysis() { try { const resp = await fetch('/api/analyze-symptoms', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(symptoms) }); analysisResult = await resp.json(); const { risk_level, risk_emoji, score, recommendation, factors, home_care, xray_recommended } = analysisResult; const badge = `${risk_emoji} ${risk_level.toUpperCase()} RISK`; let factorsHtml = factors.slice(0, 5).map(f => `â€ĸ ${f[0]} (+${f[1]})`).join('
'); await botSay(`Assessment Result: ${badge} (Score: ${score})

${factorsHtml}`, true, 1200); await botSay(recommendation, false, 800); if (home_care && home_care.length) { const tipsHtml = home_care.map(t => `â€ĸ ${t}`).join('
'); await botSay(`💡 Home Care Tips:
${tipsHtml}`, true, 600); } if (xray_recommended) { transition('XRAY_ASK'); } else { transition('XRAY_ASK'); // Still offer the option } } catch (err) { await botSay("Sorry, there was an error analyzing your symptoms. Please try again.", false, 500); console.error(err); } } async function analyzeXray(files) { try { const formData = new FormData(); files.forEach(f => formData.append('images', f)); const resp = await fetch('/api/detect-xray', { method: 'POST', body: formData }); if (!resp.ok) { const errData = await resp.json(); throw new Error(errData.error || 'Detection failed'); } detectionData = await resp.json(); if (!detectionData.success) throw new Error('Detection API returned unsuccessful'); // Aggregate all detections across results const allDets = []; const allAnnotated = []; for (const result of detectionData.results) { const dets = result.detections || []; allDets.push(...dets); const b64 = result.annotated_image_b64 || result.result_image_b64; if (b64) allAnnotated.push({ b64, filename: result.filename || '' }); } const total = detectionData.total_detections || allDets.length; // Show annotated images in side panel if (allAnnotated.length) showSidePanel(allDets, allAnnotated); // Summary message const counts = {}; allDets.forEach(d => { const c = d.class_name; counts[c] = (counts[c]||0) + 1; }); const summaryParts = Object.entries(counts).map(([k,v]) => `${v} ${k}${v>1?'s':''}`); const summaryText = summaryParts.length ? `AI detected ${total} finding${total>1?'s':''}: ${summaryParts.join(', ')}.` : 'No dental conditions were detected in the X-ray.'; await botSay(`🔍 X-ray Analysis Complete!
${summaryText}`, true, 800); if (allDets.length > 0) { await botSay("Now let me correlate these findings with your symptoms...", false, 600); transition('CORRELATION'); } else { await botSay("No abnormalities detected. Your X-ray appears normal! However, a clinical examination is still recommended.", false, 800); transition('REPORT'); } } catch (err) { await botSay(`❌ Error analyzing X-ray: ${err.message}. Please try again.`, false, 500); transition('XRAY_ASK'); console.error(err); } } async function performCorrelation() { try { const allDets = detectionData.results.flatMap(r => r.detections || []); const resp = await fetch('/api/correlate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ symptoms, detections: allDets, image_width: imageDimensions.width, image_height: imageDimensions.height }) }); const corrResult = await resp.json(); if (corrResult.correlations && corrResult.correlations.length) { await botSay("📋 Symptom ↔ X-ray Correlation:", true, 600); for (const corr of corrResult.correlations) { const badge = `${corr.severity.toUpperCase()}`; await botSay( `${badge} ${corr.clinical_name} (${corr.confidence_label})
${corr.explanation}
→ ${corr.urgency}`, true, 1000 ); } } if (corrResult.unmatched_symptoms && corrResult.unmatched_symptoms.length) { for (const us of corrResult.unmatched_symptoms) { await botSay(`â„šī¸ ${us}`, false, 600); } } transition('REPORT'); } catch (err) { await botSay("Error correlating findings. Generating report anyway...", false, 500); transition('REPORT'); console.error(err); } } async function generateReport() { await botSay("Generating your complete dental assessment report... 📋", false, 800); // Get treatment plan if detections exist let treatmentPlan = null; if (detectionData && detectionData.results) { try { const resp = await fetch('/api/treatment-plan', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ api_response: detectionData }) }); treatmentPlan = await resp.json(); } catch(e) { console.error(e); } } // Get Gemini AI-generated report if X-ray was uploaded if (uploadedFiles.length) { try { await botSay('Generating AI clinical report powered by Gemini... This may take a moment.', true, 400); const formData = new FormData(); uploadedFiles.forEach(f => formData.append('images', f)); const resp = await fetch('/api/ai-report', { method: 'POST', body: formData }); if (resp.ok) { const aiData = await resp.json(); if (aiData.results) { const firstKey = Object.keys(aiData.results)[0]; if (firstKey) { geminiReport = aiData.results[firstKey]; } } } // Show AI report as a chat message NOW, before "report ready" if (geminiReport && geminiReport.report) { let classSummaryHtml = ''; if (geminiReport.by_class) { classSummaryHtml = Object.entries(geminiReport.by_class) .map(([cls, count]) => `${cls}: ${count}`) .join(' '); } const reportChatHtml = `
🤖 AI Clinical Report powered by Gemini
${classSummaryHtml ? `
Detections:${classSummaryHtml}(${geminiReport.total || 0} total)
` : ''}
${geminiReport.report}
`; addMessage(reportChatHtml, 'bot', true); // PDF download button as a follow-up bot message window._geminiReportHtml = geminiReport.report; window._geminiByClass = geminiReport.by_class || {}; window._geminiTotal = geminiReport.total || 0; const pdfBtnHtml = ``; addMessage(pdfBtnHtml, 'bot', true); // Also inject PDF button at the bottom of the X-ray side panel const existingPanelBtn = document.getElementById('panel-pdf-section'); if (existingPanelBtn) existingPanelBtn.remove(); const panelPdfSection = document.createElement('div'); panelPdfSection.id = 'panel-pdf-section'; panelPdfSection.style.cssText = 'padding:12px 16px;border-top:1px solid var(--border);flex-shrink:0;'; panelPdfSection.innerHTML = `
AI Clinical Report
`; sidePanel.appendChild(panelPdfSection); } else if (geminiReport && geminiReport.error) { await botSay(`âš ī¸ AI report note: ${geminiReport.error}`, false, 400); } } catch(e) { console.error('Gemini report error:', e); } } // Build report HTML let reportHtml = ''; // Risk Assessment Section if (analysisResult) { const badge = `${analysisResult.risk_emoji} ${analysisResult.risk_level.toUpperCase()}`; reportHtml += `
đŸŠē Risk Assessment
Risk Level: ${badge}   Score: ${analysisResult.score}
${analysisResult.recommendation}
`; } // Symptoms Summary reportHtml += `
📝 Reported Symptoms
${symptoms.has_pain ? `Pain: ${symptoms.pain_type} pain in ${symptoms.pain_location}, intensity ${symptoms.pain_intensity}/10, duration: ${symptoms.pain_duration}
` : 'Pain: None reported
'} ${symptoms.has_swelling ? `Swelling: ${symptoms.swelling_severity}${symptoms.has_fever ? ' with fever' : ''}
` : 'Swelling: None
'} Last dental visit: ${symptoms.last_visit || 'Not specified'}
`; // Gemini AI Clinical Report (if available) if (geminiReport && geminiReport.report) { // Convert markdown-style formatting to HTML let reportText = geminiReport.report .replace(/\*\*(.*?)\*\*/g, '$1') .replace(/\*(.*?)\*/g, '$1') .replace(/^### (.*$)/gm, '

$1

') .replace(/^## (.*$)/gm, '

$1

') .replace(/^# (.*$)/gm, '

$1

') .replace(/^- (.*$)/gm, 'â€ĸ $1') .replace(/\n/g, '
'); let classSummary = ''; if (geminiReport.by_class) { classSummary = Object.entries(geminiReport.by_class) .map(([cls, count]) => `${cls}: ${count}`) .join(' '); } reportHtml += `
🤖 AI Clinical Report powered by Gemini
${classSummary ? `
Detections: ${classSummary} (${geminiReport.total || 0} total)
` : ''}
${reportText}
`; } // X-ray Findings (rule-based) if (treatmentPlan && treatmentPlan.recommendations) { let recsHtml = treatmentPlan.recommendations.map(r => `
${r.icon} ${r.finding} ${r.severity.toUpperCase()}
${r.treatment}
Specialist: ${r.specialist}
` ).join(''); reportHtml += `
đŸ”Ŧ Detection Findings & Treatment Plan
${recsHtml}
`; if (treatmentPlan.specialists && treatmentPlan.specialists.length) { reportHtml += `
đŸ‘¨â€âš•ī¸ Recommended Specialists
${treatmentPlan.specialists.map(s => `â€ĸ ${s}`).join('
')}
`; } } // Home Care if (analysisResult && analysisResult.home_care) { reportHtml += `
💡 Home Care Advice
${analysisResult.home_care.map(t => `â€ĸ ${t}`).join('
')}
`; } reportBody.innerHTML = reportHtml; reportOverlay.style.display = 'flex'; await botSay("Your assessment report is ready! 📋 Review it in the popup window.", false, 400); } // --- Side Panel --- function showSidePanel(detections, annotatedImages) { // Render annotated images const panelXray = document.getElementById('panel-xray'); panelXray.innerHTML = ''; if (annotatedImages && annotatedImages.length) { annotatedImages.forEach(({ b64, filename }) => { const img = document.createElement('img'); img.src = `data:image/png;base64,${b64}`; img.alt = filename || 'Annotated X-ray'; panelXray.appendChild(img); if (filename) { const label = document.createElement('div'); label.className = 'panel-xray-label'; label.textContent = filename; panelXray.appendChild(label); } }); } // Render detection cards const panelDets = document.getElementById('panel-detections'); panelDets.innerHTML = detections.map(d => `
${d.class_name} ${(d.confidence*100).toFixed(1)}%
Size: ${d.width}×${d.height}px • Position: (${d.x}, ${d.y})
`).join(''); sidePanel.style.display = 'flex'; btnTogglePanel.style.display = 'flex'; } // --- PDF Download --- window._downloadGeminiPDF = function() { const reportHtml = window._geminiReportHtml || ''; const byClass = window._geminiByClass || {}; const total = window._geminiTotal || 0; const classBadges = Object.entries(byClass) .map(([cls, count]) => `${cls}: ${count}`) .join(' '); const printWin = window.open('', '_blank', 'width=850,height=1000'); if (!printWin) { alert('Please allow popups to download the PDF.'); return; } printWin.document.write(` AI Clinical Report — Assnani Dental

đŸĻˇ AI Clinical Dental Report

Generated by Assnani AI  â€ĸ  ${new Date().toLocaleDateString('en-US',{year:'numeric',month:'long',day:'numeric'})}  â€ĸ  Powered by Gemini

${classBadges ? `
Findings (${total} total):${classBadges}
` : ''}
${reportHtml}
âš ī¸ This is an AI-assisted preliminary analysis. Final diagnosis must be verified by a licensed dental professional.
`); printWin.document.close(); printWin.onload = () => { setTimeout(() => printWin.print(), 300); }; }; // --- Event Listeners --- function init() { // Text input btnSend.addEventListener('click', () => submitTextInput()); userInput.addEventListener('keydown', e => { if (e.key === 'Enter') submitTextInput(); }); function submitTextInput() { const val = userInput.value.trim(); if (!val) return; addMessage(val, 'user'); userInput.value = ''; handleInput(val); } // File upload — multi-file support const ALLOWED_TYPES = /^(image\/(png|jpeg|jpg)|application\/pdf)$/; const ALLOWED_EXT = /\.(png|jpe?g|pdf)$/i; uploadDropzone.addEventListener('click', () => fileInput.click()); uploadDropzone.addEventListener('dragover', e => { e.preventDefault(); uploadDropzone.classList.add('drag-over'); }); uploadDropzone.addEventListener('dragleave', () => uploadDropzone.classList.remove('drag-over')); uploadDropzone.addEventListener('drop', e => { e.preventDefault(); uploadDropzone.classList.remove('drag-over'); if (e.dataTransfer.files.length) handleFiles(e.dataTransfer.files); }); fileInput.addEventListener('change', () => { if (fileInput.files.length) handleFiles(fileInput.files); fileInput.value = ''; }); function handleFiles(fileList) { for (const file of fileList) { const typeOk = ALLOWED_TYPES.test(file.type) || ALLOWED_EXT.test(file.name); if (!typeOk) { alert(`Unsupported file: ${file.name}. Use PNG, JPG, or PDF.`); continue; } uploadedFiles.push(file); } if (uploadedFiles.length) { renderPreviewGrid(); extractFirstImageDimensions(); } } function renderPreviewGrid() { previewGrid.innerHTML = ''; uploadedFiles.forEach((file, idx) => { const item = document.createElement('div'); item.className = 'preview-item'; const isPdf = file.type === 'application/pdf' || file.name.toLowerCase().endsWith('.pdf'); if (isPdf) { item.innerHTML = `📄`; } else { const img = document.createElement('img'); img.src = URL.createObjectURL(file); img.alt = file.name; item.appendChild(img); } const nameLabel = document.createElement('span'); nameLabel.className = 'preview-name'; nameLabel.textContent = file.name; item.appendChild(nameLabel); const removeBtn = document.createElement('button'); removeBtn.className = 'preview-remove'; removeBtn.innerHTML = '×'; removeBtn.title = 'Remove'; removeBtn.addEventListener('click', (e) => { e.stopPropagation(); uploadedFiles.splice(idx, 1); if (uploadedFiles.length === 0) { uploadDropzone.style.display = 'flex'; uploadPreview.style.display = 'none'; } else { renderPreviewGrid(); } }); item.appendChild(removeBtn); previewGrid.appendChild(item); }); uploadDropzone.style.display = 'none'; uploadPreview.style.display = 'flex'; } function extractFirstImageDimensions() { const firstImage = uploadedFiles.find(f => f.type.startsWith('image/')); if (!firstImage) return; const img = new window.Image(); img.onload = () => { imageDimensions = { width: img.naturalWidth, height: img.naturalHeight }; URL.revokeObjectURL(img.src); }; img.src = URL.createObjectURL(firstImage); } // Add More button btnUploadAdd.addEventListener('click', () => fileInput.click()); btnUploadSubmit.addEventListener('click', () => { if (!uploadedFiles.length) return; const names = uploadedFiles.map(f => f.name).join(', '); addMessage(`📎 Uploaded ${uploadedFiles.length} file${uploadedFiles.length > 1 ? 's' : ''}: ${names}`, 'user'); transition('XRAY_ANALYZING'); }); btnUploadCancel.addEventListener('click', () => { uploadedFiles = []; imageDimensions = { width: 0, height: 0 }; uploadDropzone.style.display = 'flex'; uploadPreview.style.display = 'none'; previewGrid.innerHTML = ''; fileInput.value = ''; }); // Side panel btnTogglePanel.addEventListener('click', () => { sidePanel.style.display = sidePanel.style.display === 'none' ? 'flex' : 'none'; }); btnClosePanel.addEventListener('click', () => { sidePanel.style.display = 'none'; }); // Report modal document.getElementById('btn-report-close').addEventListener('click', () => { reportOverlay.style.display = 'none'; }); document.getElementById('btn-report-new').addEventListener('click', () => resetChat()); // Print report document.getElementById('btn-report-print').addEventListener('click', () => { const printWin = window.open('', '_blank', 'width=800,height=900'); if (!printWin) { alert('Please allow popups to print the report.'); return; } printWin.document.write(`Dental Assessment Report — Assnani AI

📋 Dental Assessment Report

AI-Assisted Analysis by Assnani — ${new Date().toLocaleDateString()}

${reportBody.innerHTML.replace(/class="report-section"/g,'class="section"').replace(/class="report-section-title"/g,'class="section-title"').replace(/class="report-item"/g,'class="item"')}
âš ī¸ This is an AI-assisted preliminary analysis. Final diagnosis must be verified by a licensed dental professional.
`); printWin.document.close(); printWin.onload = () => { printWin.print(); }; }); // New chat document.getElementById('btn-new-chat').addEventListener('click', () => resetChat()); // Start conversation transition('WELCOME'); } function resetChat() { reportOverlay.style.display = 'none'; sidePanel.style.display = 'none'; btnTogglePanel.style.display = 'none'; msgContainer.innerHTML = ''; hideQuickReplies(); hideUpload(); hideInput(); symptoms = { has_pain:false,pain_location:'',pain_type:'',pain_intensity:0, pain_duration:'',pain_triggers:[],has_swelling:false, swelling_severity:'',has_fever:false,difficulty_opening:false, has_trauma:false,has_broken_tooth:false,previous_root_canal:false, last_visit:'',recent_extraction:false }; detectionData = null; analysisResult = null; geminiReport = null; const oldPdf = document.getElementById('panel-pdf-section'); if (oldPdf) oldPdf.remove(); uploadedFiles = []; imageDimensions = { width: 0, height: 0 }; state = 'WELCOME'; transition('WELCOME'); } return { init }; })(); document.addEventListener('DOMContentLoaded', () => Chatbot.init());