/* ===== 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 => `
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
${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());