DIrtyCha's picture
Initial commit from PainReport
acaf471
<!DOCTYPE html>
<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>