Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Neuro-Symbolic Pain Assessment System - Demo</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| } | |
| .header { | |
| text-align: center; | |
| color: white; | |
| margin-bottom: 30px; | |
| } | |
| .header h1 { | |
| font-size: 2.5em; | |
| margin-bottom: 10px; | |
| } | |
| .header p { | |
| font-size: 1.2em; | |
| opacity: 0.9; | |
| } | |
| .modules { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| grid-template-rows: auto auto; | |
| gap: 20px; | |
| margin-bottom: 20px; | |
| } | |
| .module-1 { | |
| grid-column: 1; | |
| grid-row: 1; | |
| } | |
| .module-2 { | |
| grid-column: 1; | |
| grid-row: 2; | |
| } | |
| .report-panel { | |
| grid-column: 2; | |
| grid-row: 1 / span 2; | |
| } | |
| .module { | |
| background: white; | |
| border-radius: 15px; | |
| padding: 25px; | |
| box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); | |
| } | |
| .module h2 { | |
| color: #667eea; | |
| margin-bottom: 15px; | |
| font-size: 1.5em; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .module-badge { | |
| background: #667eea; | |
| color: white; | |
| padding: 5px 12px; | |
| border-radius: 20px; | |
| font-size: 0.7em; | |
| } | |
| textarea { | |
| width: 100%; | |
| padding: 15px; | |
| border: 2px solid #e0e0e0; | |
| border-radius: 8px; | |
| font-size: 16px; | |
| resize: vertical; | |
| min-height: 120px; | |
| font-family: inherit; | |
| } | |
| textarea:focus { | |
| outline: none; | |
| border-color: #667eea; | |
| } | |
| button { | |
| background: #667eea; | |
| color: white; | |
| border: none; | |
| padding: 12px 30px; | |
| border-radius: 8px; | |
| font-size: 16px; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| margin-top: 10px; | |
| } | |
| button:hover { | |
| background: #5568d3; | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); | |
| } | |
| button:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| transform: none; | |
| } | |
| .result { | |
| margin-top: 20px; | |
| padding: 20px; | |
| background: #f8f9fa; | |
| border-radius: 8px; | |
| border-left: 4px solid #667eea; | |
| max-height: 600px; | |
| overflow-y: auto; | |
| } | |
| .result h3 { | |
| color: #667eea; | |
| margin-bottom: 10px; | |
| } | |
| .result-section { | |
| margin-bottom: 15px; | |
| } | |
| .result-section h4 { | |
| color: #444; | |
| margin-bottom: 8px; | |
| font-size: 1.1em; | |
| } | |
| .mapping-item { | |
| background: white; | |
| padding: 10px; | |
| margin: 5px 0; | |
| border-radius: 5px; | |
| border-left: 3px solid #28a745; | |
| } | |
| .recommendation-item { | |
| background: #fff3cd; | |
| padding: 15px; | |
| margin: 10px 0; | |
| border-radius: 5px; | |
| border-left: 4px solid #ffc107; | |
| } | |
| .reasoning-chain { | |
| background: white; | |
| padding: 10px; | |
| margin: 5px 0; | |
| border-left: 3px solid #17a2b8; | |
| font-family: 'Courier New', monospace; | |
| font-size: 0.9em; | |
| } | |
| .question-options { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); | |
| gap: 15px; | |
| margin-top: 15px; | |
| } | |
| .option-card { | |
| background: white; | |
| border: 2px solid #e0e0e0; | |
| border-radius: 10px; | |
| padding: 15px; | |
| cursor: pointer; | |
| transition: all 0.3s; | |
| text-align: center; | |
| } | |
| .option-card:hover { | |
| border-color: #667eea; | |
| transform: translateY(-3px); | |
| box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3); | |
| } | |
| .option-card.selected { | |
| border-color: #667eea; | |
| background: #f0f4ff; | |
| } | |
| .option-image { | |
| width: 100%; | |
| height: 150px; | |
| object-fit: cover; | |
| border-radius: 8px; | |
| margin-bottom: 10px; | |
| } | |
| .loading { | |
| text-align: center; | |
| padding: 20px; | |
| color: #667eea; | |
| } | |
| .spinner { | |
| border: 4px solid #f3f3f3; | |
| border-top: 4px solid #667eea; | |
| border-radius: 50%; | |
| width: 40px; | |
| height: 40px; | |
| animation: spin 1s linear infinite; | |
| margin: 0 auto; | |
| } | |
| @keyframes spin { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| .badge { | |
| display: inline-block; | |
| padding: 3px 8px; | |
| border-radius: 12px; | |
| font-size: 0.8em; | |
| font-weight: bold; | |
| margin-left: 5px; | |
| } | |
| .badge-neuropathic { | |
| background: #dc3545; | |
| color: white; | |
| } | |
| .badge-nociceptive { | |
| background: #fd7e14; | |
| color: white; | |
| } | |
| .badge-affective { | |
| background: #6f42c1; | |
| color: white; | |
| } | |
| .example-buttons { | |
| display: flex; | |
| gap: 10px; | |
| margin-top: 10px; | |
| flex-wrap: wrap; | |
| } | |
| .example-btn { | |
| background: #f8f9fa; | |
| color: #667eea; | |
| border: 1px solid #667eea; | |
| padding: 8px 15px; | |
| font-size: 14px; | |
| } | |
| .example-btn:hover { | |
| background: #667eea; | |
| color: white; | |
| } | |
| /* New styles for improved readability */ | |
| details { | |
| cursor: pointer; | |
| user-select: none; | |
| } | |
| details summary { | |
| padding: 8px; | |
| border-radius: 6px; | |
| background: rgba(0,0,0,0.02); | |
| transition: background 0.2s; | |
| } | |
| details summary:hover { | |
| background: rgba(0,0,0,0.05); | |
| } | |
| details[open] summary { | |
| margin-bottom: 10px; | |
| border-bottom: 1px solid rgba(0,0,0,0.1); | |
| } | |
| .result-section { | |
| padding: 20px; | |
| margin-bottom: 15px; | |
| border-radius: 12px; | |
| } | |
| /* Smooth scrollbar for results */ | |
| .result::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| .result::-webkit-scrollbar-track { | |
| background: #f1f1f1; | |
| border-radius: 4px; | |
| } | |
| .result::-webkit-scrollbar-thumb { | |
| background: #667eea; | |
| border-radius: 4px; | |
| } | |
| .result::-webkit-scrollbar-thumb:hover { | |
| background: #5568d3; | |
| } | |
| @media (max-width: 1024px) { | |
| .modules { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>🏥 Neuro-Symbolic Pain Assessment System</h1> | |
| <p>Hybrid AI Architecture: LLM Entity Extraction + Deterministic Clinical Reasoning</p> | |
| </div> | |
| <div class="modules"> | |
| <!-- MODULE 1: Text + Voice Input (Left Top) --> | |
| <div class="module module-1"> | |
| <h2> | |
| <span class="module-badge">Module 1</span> | |
| Pain Assessment Pipeline | |
| </h2> | |
| <p style="margin-bottom: 15px; color: #666;"> | |
| Enter a pain description to see the complete neuro-symbolic analysis pipeline in action. | |
| </p> | |
| <textarea id="painInput" placeholder="Example: I've had constant electric-shock-like pain in my lower back for 4 months, can't sleep at night, feeling very depressed..."></textarea> | |
| <div class="example-buttons"> | |
| <button class="example-btn" onclick="loadExample('neuropathic')">Example: Neuropathic</button> | |
| <button class="example-btn" onclick="loadExample('chronic')">Example: Chronic Pain</button> | |
| <button class="example-btn" onclick="loadExample('chinese')">Example: Chinese</button> | |
| </div> | |
| <button id="analyzeBtn" onclick="analyzePain()">🔬 Analyze Pain Description</button> | |
| <!-- Voice Recording --> | |
| <div style="margin-top: 15px; padding: 15px; background: #f0f4ff; border-radius: 8px; border: 2px dashed #667eea;"> | |
| <p style="margin-bottom: 10px; color: #667eea; font-weight: bold;">🎙️ OR Record Your Voice:</p> | |
| <div style="display: flex; gap: 10px; align-items: center;"> | |
| <button id="recordBtn" onclick="toggleRecording()" style="background: #dc3545;"> | |
| 🎙️ Start Recording | |
| </button> | |
| <span id="recordStatus" style="color: #666;">Ready to record</span> | |
| </div> | |
| <audio id="audioPlayback" controls style="width: 100%; margin-top: 10px; display: none;"></audio> | |
| </div> | |
| <!-- Structured Data Results --> | |
| <div id="pipelineResult"></div> | |
| </div> | |
| <!-- MODULE 2: Visual Q&A (Left Bottom) --> | |
| <div class="module module-2"> | |
| <h2> | |
| <span class="module-badge" style="background: #28a745;">Module 2</span> | |
| Visual Follow-up Questions | |
| </h2> | |
| <p style="margin-bottom: 15px; color: #666;"> | |
| Interactive image-based pain assessment for better characterization. | |
| </p> | |
| <button onclick="generateQuestion()">🖼️ Generate Visual Question</button> | |
| <div id="questionResult"></div> | |
| </div> | |
| <!-- Right Panel: Clinical Report (Full Height) --> | |
| <div class="module report-panel"> | |
| <h2> | |
| <span class="module-badge" style="background: #13547a;">📋 Report</span> | |
| Physician Summary (Clinical Report) | |
| </h2> | |
| <p style="margin-bottom: 15px; color: #666;"> | |
| Comprehensive medical anthropologist analysis with 4-layer framework. | |
| </p> | |
| <div id="physicianReport"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const API_BASE = 'http://localhost:8000'; | |
| // Example texts | |
| const examples = { | |
| neuropathic: "I've had constant electric-shock-like tingling pain in my lower back and legs for 4 months. The pain wakes me up at night, I can't sleep properly. Feeling exhausted and depressed.", | |
| chronic: "My knees have been aching constantly for several months now. The pain is dull and throbbing, especially during the night. I'm exhausted from dealing with it.", | |
| chinese: "最近四个月腰部到腿部总是像触电一样的麻痛,晚上痛得睡不着,心情很郁闷" | |
| }; | |
| function loadExample(type) { | |
| document.getElementById('painInput').value = examples[type]; | |
| } | |
| async function analyzePain() { | |
| const text = document.getElementById('painInput').value.trim(); | |
| if (!text) { | |
| alert('Please enter a pain description'); | |
| return; | |
| } | |
| const resultDiv = document.getElementById('pipelineResult'); | |
| const btn = document.getElementById('analyzeBtn'); | |
| btn.disabled = true; | |
| resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div><p>Analyzing pain description...</p></div>'; | |
| try { | |
| const response = await fetch(`${API_BASE}/api/analyze-text-neuro-symbolic`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ text }) | |
| }); | |
| const data = await response.json(); | |
| if (data.status === 'success') { | |
| displayPipelineResult(data); | |
| } else { | |
| resultDiv.innerHTML = `<div class="result"><h3>❌ Error</h3><p>${data.message}</p></div>`; | |
| } | |
| } catch (error) { | |
| resultDiv.innerHTML = `<div class="result"><h3>❌ Connection Error</h3><p>Make sure the backend server is running on ${API_BASE}</p></div>`; | |
| } finally { | |
| btn.disabled = false; | |
| } | |
| } | |
| /** | |
| * Parse bilingual text format: "原文 [English translation]" | |
| * Returns object with {original, translation} or {original: text} if no translation | |
| */ | |
| function parseBilingualText(text) { | |
| if (!text) return { original: '' }; | |
| // Check for format: "原文 [English translation]" | |
| const match = text.match(/^(.+?)\s*\[(.+?)\]$/); | |
| if (match) { | |
| return { | |
| original: match[1].trim(), | |
| translation: match[2].trim() | |
| }; | |
| } | |
| // No translation format, return as-is | |
| return { original: text }; | |
| } | |
| /** | |
| * Format bilingual text for display: large original + small translation | |
| */ | |
| function formatBilingualDisplay(text) { | |
| const parsed = parseBilingualText(text); | |
| if (parsed.translation) { | |
| // Has translation - display original prominently with translation below | |
| return ` | |
| <div style="font-size: 1.05em; font-weight: 600; margin-top: 4px; line-height: 1.4;"> | |
| ${parsed.original} | |
| </div> | |
| <div style="font-size: 0.8em; color: rgba(255,255,255,0.75); margin-top: 4px; font-style: italic;"> | |
| ${parsed.translation} | |
| </div> | |
| `; | |
| } else { | |
| // No translation - display plain text | |
| return `<div style="font-size: 1.05em; font-weight: 600; margin-top: 4px;">${parsed.original}</div>`; | |
| } | |
| } | |
| function displayPipelineResult(data) { | |
| const resultDiv = document.getElementById('pipelineResult'); | |
| // Validate data structure | |
| if (!data || !data.structured_data) { | |
| resultDiv.innerHTML = `<div class="result"><h3>⚠️ Analysis Issue</h3><p>Unable to process the pain description. Please try again with more details.</p></div>`; | |
| return; | |
| } | |
| const sd = data.structured_data; | |
| const mappings = data.ontology_mapping_trace || []; | |
| const recommendations = data.clinical_recommendations || []; | |
| const reasoning = data.reasoning_chain || []; | |
| const transcription = data.transcription || null; | |
| let html = '<div class="result">'; | |
| html += '<h3 style="color: #28a745;">✅ Analysis Complete</h3>'; | |
| // Transcription Normalization (if available) | |
| if (transcription && transcription.original !== transcription.normalized) { | |
| html += '<div class="result-section" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">'; | |
| html += '<h4 style="color: white; margin-bottom: 15px;">📝 Speech Recognition & Correction</h4>'; | |
| html += '<div style="background: rgba(255,255,255,0.15); padding: 12px; border-radius: 8px; margin-bottom: 12px;">'; | |
| html += '<div style="font-size: 0.85em; opacity: 0.9; margin-bottom: 5px;">🎤 Original Transcription</div>'; | |
| html += `<div style="font-size: 1em; font-style: italic;">${transcription.original}</div>`; | |
| html += '</div>'; | |
| html += '<div style="text-align: center; margin: 10px 0; opacity: 0.7;">⬇️</div>'; | |
| html += '<div style="background: rgba(255,255,255,0.25); padding: 12px; border-radius: 8px; border: 2px solid rgba(255,255,255,0.4);">'; | |
| html += '<div style="font-size: 0.85em; opacity: 0.9; margin-bottom: 5px;">✨ AI-Enhanced</div>'; | |
| html += `<div style="font-size: 1.1em; font-weight: 600;">${transcription.normalized}</div>`; | |
| html += '</div>'; | |
| // English Translation (if available) | |
| if (transcription.english_translation) { | |
| html += '<div style="text-align: center; margin: 10px 0; opacity: 0.7;">⬇️</div>'; | |
| html += '<div style="background: rgba(255,255,255,0.35); padding: 12px; border-radius: 8px; border: 2px solid rgba(255,255,255,0.6);">'; | |
| html += '<div style="font-size: 0.85em; opacity: 0.9; margin-bottom: 5px;">🌐 English Translation</div>'; | |
| html += `<div style="font-size: 1.1em; font-weight: 600;">${transcription.english_translation}</div>`; | |
| html += '</div>'; | |
| } | |
| if (transcription.corrections_applied && transcription.corrections_applied.length > 0) { | |
| html += '<details style="margin-top: 15px; cursor: pointer;">'; | |
| html += '<summary style="font-size: 0.9em; opacity: 0.9;">🔧 Optimization Details (' + transcription.corrections_applied.length + ' items)</summary>'; | |
| html += '<div style="background: rgba(0,0,0,0.1); padding: 10px; border-radius: 6px; margin-top: 8px;">'; | |
| transcription.corrections_applied.forEach(correction => { | |
| html += `<div style="padding: 4px 0; font-size: 0.85em;">• ${correction}</div>`; | |
| }); | |
| html += '</div></details>'; | |
| } | |
| html += '</div>'; | |
| } | |
| // Pain Assessment Summary (Main Card) | |
| html += '<div class="result-section" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; border: none; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">'; | |
| html += '<h4 style="color: white; margin-bottom: 15px;">🩺 Pain Assessment Results</h4>'; | |
| // Pain Type with visual indicator | |
| const painTypeColor = sd.pain_type && sd.pain_type.toLowerCase().includes('neuropathic') ? '#ff6b6b' : | |
| sd.pain_type && sd.pain_type.toLowerCase().includes('nociceptive') ? '#4ecdc4' : '#95e1d3'; | |
| html += '<div style="background: rgba(255,255,255,0.2); padding: 15px; border-radius: 8px; margin-bottom: 12px;">'; | |
| html += `<div style="font-size: 0.9em; opacity: 0.9; margin-bottom: 5px;">Pain Type</div>`; | |
| html += `<div style="font-size: 1.3em; font-weight: 700; display: flex; align-items: center;">`; | |
| html += `<span style="background: ${painTypeColor}; width: 12px; height: 12px; border-radius: 50%; display: inline-block; margin-right: 10px;"></span>`; | |
| html += `${sd.pain_type || 'Not detected'}`; | |
| html += `</div></div>`; | |
| // Grid layout for other info | |
| html += '<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">'; | |
| if (sd.location && sd.location !== 'Not specified') { | |
| html += '<div style="background: rgba(255,255,255,0.15); padding: 10px; border-radius: 6px;">'; | |
| html += '<div style="font-size: 0.85em; opacity: 0.9;">📍 Location</div>'; | |
| html += `<div style="font-size: 1.05em; font-weight: 600; margin-top: 4px;">${sd.location}</div>`; | |
| html += '</div>'; | |
| } | |
| if (sd.temporal_pattern && sd.temporal_pattern !== 'Not specified') { | |
| html += '<div style="background: rgba(255,255,255,0.15); padding: 10px; border-radius: 6px;">'; | |
| html += '<div style="font-size: 0.85em; opacity: 0.9;">⏱️ Temporal Pattern</div>'; | |
| html += `<div style="font-size: 1.05em; font-weight: 600; margin-top: 4px;">${sd.temporal_pattern}</div>`; | |
| html += '</div>'; | |
| } | |
| if (sd.intensity && sd.intensity !== 'Not stated') { | |
| html += '<div style="background: rgba(255,255,255,0.15); padding: 10px; border-radius: 6px;">'; | |
| html += '<div style="font-size: 0.85em; opacity: 0.9;">💪 Intensity</div>'; | |
| html += formatBilingualDisplay(sd.intensity); | |
| html += '</div>'; | |
| } | |
| if (sd.emotion && sd.emotion !== 'None detected') { | |
| html += '<div style="background: rgba(255,255,255,0.15); padding: 10px; border-radius: 6px;">'; | |
| html += '<div style="font-size: 0.85em; opacity: 0.9;">😔 Emotional Impact</div>'; | |
| html += `<div style="font-size: 1.05em; font-weight: 600; margin-top: 4px;">${sd.emotion}</div>`; | |
| html += '</div>'; | |
| } | |
| if (sd.functional_impact && sd.functional_impact !== 'Not stated') { | |
| html += '<div style="background: rgba(255,255,255,0.15); padding: 10px; border-radius: 6px; grid-column: 1 / -1;">'; | |
| html += '<div style="font-size: 0.85em; opacity: 0.9;">🚶 Functional Impact</div>'; | |
| html += formatBilingualDisplay(sd.functional_impact); | |
| html += '</div>'; | |
| } | |
| html += '</div></div>'; | |
| // Unmapped/Unique Pain Descriptors (if any) | |
| // Check if pain_type contains [Unmapped terms: ...] | |
| if (sd.pain_type && sd.pain_type.includes('[Unmapped terms:')) { | |
| const unmappedMatch = sd.pain_type.match(/\[Unmapped terms: ([^\]]+)\]/); | |
| if (unmappedMatch) { | |
| const unmappedTerms = unmappedMatch[1].split(', '); | |
| html += '<div class="result-section" style="background: #fff3cd; border-left: 4px solid #ffc107; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">'; | |
| html += '<h4 style="color: #856404; margin-bottom: 15px;">⚠️ Unique Pain Descriptors (Not in Medical Dictionary)</h4>'; | |
| html += '<div style="color: #856404; line-height: 1.6; margin-bottom: 10px;">The patient used creative/metaphorical expressions that are not in our standardized pain terminology database. These should be noted for clinical context:</div>'; | |
| unmappedTerms.forEach(term => { | |
| html += `<div style="background: #ffffff; padding: 10px 12px; border-radius: 6px; margin-bottom: 8px; border-left: 3px solid #ffc107; font-weight: 500; color: #333;">`; | |
| html += `📝 "${term.trim()}"`; | |
| html += `</div>`; | |
| }); | |
| html += '<div style="font-size: 0.85em; color: #856404; margin-top: 10px; font-style: italic;">💡 Recommendation: Consider asking follow-up questions to understand these expressions in clinical terms.</div>'; | |
| html += '</div>'; | |
| } | |
| } | |
| // Physician Summary moved to right panel - display it there instead | |
| if (data.physician_summary) { | |
| displayPhysicianReport(data.physician_summary); | |
| } | |
| // Clinical Recommendations (if available) | |
| if (recommendations.length > 0) { | |
| html += '<div class="result-section" style="background: #fff; border-left: 4px solid #ff6b6b; box-shadow: 0 2px 4px rgba(0,0,0,0.1);">'; | |
| html += '<h4 style="color: #ff6b6b; margin-bottom: 15px;">💡 Clinical Recommendations</h4>'; | |
| recommendations.forEach(rec => { | |
| html += `<div style="background: #fff5f5; padding: 15px; border-radius: 8px; margin-bottom: 10px; border-left: 3px solid #ff6b6b;">`; | |
| html += `<div style="font-weight: 600; color: #333; margin-bottom: 8px;">${rec.triggered_by_rule || 'Clinical Recommendation'}</div>`; | |
| html += `<div style="color: #555; line-height: 1.6;">${rec.recommendation}</div>`; | |
| html += `<div style="margin-top: 8px; font-size: 0.85em; color: #999;">Confidence: ${rec.confidence || 'medium'}</div>`; | |
| html += `</div>`; | |
| }); | |
| html += '</div>'; | |
| } | |
| // Ontology Mappings (Collapsible Technical Details) | |
| if (mappings.length > 0) { | |
| // Separate confirmed mappings from suggestions | |
| const confirmedMappings = mappings.filter(m => !m.is_suggestion); | |
| const suggestedMappings = mappings.filter(m => m.is_suggestion); | |
| html += '<div class="result-section" style="background: #f8f9fa; border: 1px solid #e9ecef;">'; | |
| html += '<details style="cursor: pointer;">'; | |
| html += '<summary style="font-weight: 600; color: #495057; padding: 5px 0; user-select: none;">🔬 Terminology Mapping Details (' + confirmedMappings.length + ' confirmed'; | |
| if (suggestedMappings.length > 0) { | |
| html += ', ' + suggestedMappings.length + ' suggested'; | |
| } | |
| html += ') ▼</summary>'; | |
| html += '<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #dee2e6;">'; | |
| // Display confirmed mappings | |
| if (confirmedMappings.length > 0) { | |
| html += '<div style="margin-bottom: 20px;">'; | |
| html += '<div style="font-size: 0.9em; color: #28a745; font-weight: 600; margin-bottom: 10px;">✅ Confirmed Mappings (Dictionary Matches)</div>'; | |
| confirmedMappings.forEach(m => { | |
| const badgeClass = m.pain_type === 'neuropathic' ? 'badge-neuropathic' : | |
| m.pain_type === 'nociceptive' ? 'badge-nociceptive' : 'badge-affective'; | |
| html += `<div style="background: white; padding: 12px; border-radius: 6px; margin-bottom: 8px; border-left: 3px solid #28a745;">`; | |
| html += `<div style="display: flex; align-items: center; justify-content: space-between;">`; | |
| html += `<div><strong style="color: #495057;">"${m.original_term || m.chinese_input}"</strong> → <strong style="color: #007bff;">${m.mapped_english}</strong></div>`; | |
| html += `<span class="badge ${badgeClass}" style="margin-left: 10px;">${m.pain_type || m.dimension}</span>`; | |
| html += `</div>`; | |
| if (m.matched_text && m.matched_text !== m.original_term) { | |
| html += `<div style="margin-top: 5px; font-size: 0.85em; color: #6c757d;">Matched: "${m.matched_text}"</div>`; | |
| } | |
| html += `</div>`; | |
| }); | |
| html += '</div>'; | |
| } | |
| // Display suggested mappings (warnings) | |
| if (suggestedMappings.length > 0) { | |
| html += '<div style="margin-top: 15px; padding-top: 15px; border-top: 2px dashed #ffc107;">'; | |
| html += '<div style="font-size: 0.9em; color: #856404; font-weight: 600; margin-bottom: 10px;">⚠️ Similarity Suggestions (Not in Dictionary)</div>'; | |
| html += '<div style="background: #fff3cd; padding: 12px; border-radius: 6px; margin-bottom: 10px; font-size: 0.85em; color: #856404;">'; | |
| html += '💡 These terms were not found in our medical dictionary. The system suggests similar terms below, but these are NOT definitive mappings. Clinical review is required.'; | |
| html += '</div>'; | |
| suggestedMappings.forEach(m => { | |
| html += `<div style="background: #fff3cd; padding: 12px; border-radius: 6px; margin-bottom: 8px; border-left: 3px dashed #ffc107;">`; | |
| html += `<div style="display: flex; align-items: center; justify-content: space-between;">`; | |
| html += `<div><strong style="color: #856404;">"${m.original_term}"</strong> <span style="opacity: 0.7;">→ might be similar to →</span> <strong style="color: #d39e00;">${m.mapped_english}</strong></div>`; | |
| html += `<span class="badge" style="background: #ffc107; color: #000; margin-left: 10px;">suggestion</span>`; | |
| html += `</div>`; | |
| if (m.similarity_reason) { | |
| html += `<div style="margin-top: 8px; font-size: 0.85em; color: #856404; background: rgba(255,255,255,0.5); padding: 6px 8px; border-radius: 4px;">`; | |
| html += `🔍 Similarity: ${m.similarity_reason}`; | |
| html += `</div>`; | |
| } | |
| if (m.suggestion_note) { | |
| html += `<div style="margin-top: 8px; font-size: 0.8em; color: #856404; font-style: italic;">`; | |
| html += `${m.suggestion_note}`; | |
| html += `</div>`; | |
| } | |
| html += `</div>`; | |
| }); | |
| html += '</div>'; | |
| } | |
| html += '</div></details></div>'; | |
| } | |
| // Reasoning Chain (Collapsible Technical Details) | |
| if (reasoning.length > 0) { | |
| html += '<div class="result-section" style="background: #f8f9fa; border: 1px solid #e9ecef;">'; | |
| html += '<details style="cursor: pointer;">'; | |
| html += '<summary style="font-weight: 600; color: #495057; padding: 5px 0; user-select: none;">🧠 AI Reasoning Process (' + reasoning.length + ' steps) ▼</summary>'; | |
| html += '<div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid #dee2e6;">'; | |
| reasoning.forEach((step, index) => { | |
| // Parse and format reasoning steps | |
| let formattedStep = step.replace(/===/g, '').replace(/\n\n/g, '<br>'); | |
| const isHeader = step.includes('===') || step.match(/^[A-Z\s]+$/); | |
| const style = isHeader ? | |
| 'background: #e7f3ff; padding: 8px 12px; border-radius: 4px; font-weight: 600; color: #0066cc; margin: 10px 0 5px 0;' : | |
| 'background: white; padding: 10px 12px; border-radius: 4px; color: #495057; margin: 5px 0; border-left: 2px solid #dee2e6; font-family: monospace; font-size: 0.9em; white-space: pre-wrap;'; | |
| html += `<div style="${style}">${formattedStep}</div>`; | |
| }); | |
| html += '</div></details></div>'; | |
| } | |
| html += '</div>'; | |
| resultDiv.innerHTML = html; | |
| } | |
| function displayPhysicianReport(summary) { | |
| const reportDiv = document.getElementById('physicianReport'); | |
| let html = '<div style="background: rgba(255,255,255,0.95); color: #333; padding: 20px; border-radius: 8px; line-height: 1.6;">'; | |
| // Clean up excessive whitespace first | |
| let cleanedSummary = summary | |
| .replace(/\n{3,}/g, '\n\n') // Replace 3+ newlines with 2 | |
| .trim(); | |
| // Enhanced Markdown formatting with better spacing | |
| let formattedSummary = cleanedSummary | |
| // Headers (## ) - with proper spacing | |
| .replace(/^## (.+)$/gm, '<h3 style="color: #667eea; margin: 25px 0 12px 0; font-size: 1.3em; font-weight: 600;">$1</h3>') | |
| // Horizontal rules (---) - thinner with less margin | |
| .replace(/^---$/gm, '<hr style="border: none; border-top: 1px solid #e0e0e0; margin: 15px 0;">') | |
| // Blockquotes (> ) - more compact | |
| .replace(/^> (.+)$/gm, '<div style="border-left: 3px solid #667eea; padding: 8px 12px; margin: 8px 0; background: #f8f9fa; color: #555; font-style: italic; border-radius: 3px;">$1</div>') | |
| // Bold (**text**) | |
| .replace(/\*\*(.+?)\*\*/g, '<strong style="color: #333;">$1</strong>') | |
| // Unordered lists (- ) - more compact | |
| .replace(/^- (.+)$/gm, '<li style="margin: 3px 0 3px 20px; line-height: 1.5;">$1</li>') | |
| // Wrap consecutive <li> in <ul> with tighter spacing | |
| .replace(/(<li[^>]*>.*?<\/li>\s*)+/g, '<ul style="list-style-type: disc; margin: 8px 0; padding-left: 0;">$&</ul>') | |
| // Convert remaining double newlines to paragraph breaks (smaller gap) | |
| .replace(/\n\n/g, '<div style="height: 10px;"></div>') | |
| // Convert single newlines to line breaks | |
| .replace(/\n/g, '<br>'); | |
| html += formattedSummary; | |
| html += '</div>'; | |
| reportDiv.innerHTML = html; | |
| } | |
| async function generateQuestion() { | |
| const resultDiv = document.getElementById('questionResult'); | |
| resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div><p>Generating visual question...</p></div>'; | |
| try { | |
| const response = await fetch(`${API_BASE}/api/follow-up`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ history: [] }) | |
| }); | |
| const data = await response.json(); | |
| if (data.status === 'success') { | |
| displayQuestion(data.followup); | |
| } else { | |
| resultDiv.innerHTML = `<div class="result"><h3>❌ Error</h3><p>${data.message}</p></div>`; | |
| } | |
| } catch (error) { | |
| resultDiv.innerHTML = `<div class="result"><h3>❌ Connection Error</h3><p>${error.message}</p></div>`; | |
| } | |
| } | |
| function displayQuestion(question) { | |
| const resultDiv = document.getElementById('questionResult'); | |
| let html = '<div class="result">'; | |
| html += `<h3 style="line-height: 1.6;">❓ ${question.question}</h3>`; | |
| html += '<div class="question-options">'; | |
| question.options.forEach(option => { | |
| // Split bilingual text for better display | |
| const textParts = option.text.split('|').map(t => t.trim()); | |
| const displayText = textParts.length > 1 | |
| ? `<strong>${option.id}</strong>: ${textParts[0]}<br><span style="color: #666; font-size: 0.9em;">${textParts[1]}</span>` | |
| : `<strong>${option.id}</strong>: ${option.text}`; | |
| html += `<div class="option-card" onclick="selectOption(this, '${option.id}')">`; | |
| html += `<img src="${API_BASE}${option.image_url}" class="option-image" alt="${option.text}">`; | |
| html += `<p>${displayText}</p>`; | |
| html += `</div>`; | |
| }); | |
| html += '</div>'; | |
| html += '</div>'; | |
| resultDiv.innerHTML = html; | |
| } | |
| function selectOption(element, optionId) { | |
| document.querySelectorAll('.option-card').forEach(card => { | |
| card.classList.remove('selected'); | |
| }); | |
| element.classList.add('selected'); | |
| console.log('Selected option:', optionId); | |
| } | |
| // Audio Recording Functions | |
| let mediaRecorder; | |
| let audioChunks = []; | |
| async function toggleRecording() { | |
| const btn = document.getElementById('recordBtn'); | |
| const status = document.getElementById('recordStatus'); | |
| const audioPlayback = document.getElementById('audioPlayback'); | |
| if (!mediaRecorder || mediaRecorder.state === 'inactive') { | |
| // Start recording | |
| try { | |
| const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| mediaRecorder = new MediaRecorder(stream); | |
| audioChunks = []; | |
| mediaRecorder.ondataavailable = (event) => { | |
| audioChunks.push(event.data); | |
| }; | |
| mediaRecorder.onstop = async () => { | |
| const audioBlob = new Blob(audioChunks, { type: 'audio/webm' }); | |
| const audioUrl = URL.createObjectURL(audioBlob); | |
| audioPlayback.src = audioUrl; | |
| audioPlayback.style.display = 'block'; | |
| status.textContent = 'Processing audio...'; | |
| status.style.color = '#007bff'; | |
| await analyzeAudio(audioBlob); | |
| }; | |
| mediaRecorder.start(); | |
| btn.textContent = '⏹️ Stop Recording'; | |
| btn.style.background = '#dc3545'; | |
| status.textContent = 'Recording... speak now'; | |
| status.style.color = '#dc3545'; | |
| } catch (error) { | |
| alert('Microphone access denied: ' + error.message); | |
| } | |
| } else { | |
| // Stop recording | |
| mediaRecorder.stop(); | |
| mediaRecorder.stream.getTracks().forEach(track => track.stop()); | |
| btn.textContent = '🎙️ Start Recording'; | |
| btn.style.background = '#28a745'; | |
| status.textContent = 'Analyzing...'; | |
| status.style.color = '#007bff'; | |
| } | |
| } | |
| async function analyzeAudio(audioBlob) { | |
| const resultDiv = document.getElementById('pipelineResult'); | |
| const status = document.getElementById('recordStatus'); | |
| resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div><p>Transcribing and analyzing audio...</p></div>'; | |
| try { | |
| const formData = new FormData(); | |
| formData.append('file', audioBlob, 'recording.webm'); | |
| const response = await fetch(`${API_BASE}/api/analyze-audio-neuro-symbolic`, { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const data = await response.json(); | |
| if (data.status === 'success') { | |
| // Show normalized transcription in input field (cleaner for display) | |
| if (data.transcription) { | |
| // Use normalized text if available, otherwise fall back to original | |
| const displayText = data.transcription.normalized || data.transcription.original || data.transcription; | |
| document.getElementById('painInput').value = displayText; | |
| } | |
| // Display results only if structured data exists | |
| if (data.structured_data) { | |
| displayPipelineResult(data); | |
| status.textContent = 'Analysis complete!'; | |
| status.style.color = '#28a745'; | |
| } else { | |
| // Show transcription but indicate analysis failed | |
| const transcriptionText = data.transcription?.normalized || data.transcription?.original || data.transcription || 'Unknown'; | |
| resultDiv.innerHTML = `<div class="result"> | |
| <h3>⚠️ Transcription Only</h3> | |
| <p><strong>Transcribed text:</strong> ${transcriptionText}</p> | |
| <p style="color: #666; margin-top: 10px;">Analysis could not be completed. The system may not recognize the pain description yet.</p> | |
| </div>`; | |
| status.textContent = 'Transcription done, analysis incomplete'; | |
| status.style.color = '#ff9800'; | |
| } | |
| } else { | |
| resultDiv.innerHTML = `<div class="result"><h3>❌ Error</h3><p>${data.message || 'Unknown error'}</p></div>`; | |
| status.textContent = 'Error occurred'; | |
| status.style.color = '#dc3545'; | |
| } | |
| } catch (error) { | |
| resultDiv.innerHTML = `<div class="result"><h3>❌ Connection Error</h3><p>${error.message}</p></div>`; | |
| status.textContent = 'Connection failed'; | |
| status.style.color = '#dc3545'; | |
| } | |
| } | |
| // Load first example on page load | |
| window.onload = () => { | |
| loadExample('neuropathic'); | |
| }; | |
| </script> | |
| </body> | |
| </html> |