|
|
<!doctype html> |
|
|
<html lang="en"> |
|
|
<head> |
|
|
<meta charset="utf-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1"> |
|
|
<title>Chest X-ray Report Generation Application Via VLM and LLM</title> |
|
|
|
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;500;700&display=swap" rel="stylesheet"> |
|
|
|
|
|
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> |
|
|
|
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.2.0/css/all.min.css"> |
|
|
<style> |
|
|
:root { |
|
|
|
|
|
--lm-primary-color: #0d6efd; |
|
|
--lm-primary-color-rgb: 13, 110, 253; |
|
|
--lm-bg-primary: linear-gradient(135deg, #eef2f7 0%, #f0f5ff 100%); |
|
|
--lm-bg-container: rgba(255, 255, 255, 0.85); |
|
|
--lm-bg-content: #fdfdff; |
|
|
--lm-bg-input: #ffffff; |
|
|
--lm-bg-user-msg: #dbeafe; |
|
|
--lm-bg-bot-msg: #e9ecef; |
|
|
--lm-bg-suggestion: #f0f2f5; |
|
|
--lm-bg-suggestion-hover: #e2e6ea; |
|
|
--lm-text-primary: #333; |
|
|
--lm-text-secondary: #495057; |
|
|
--lm-text-muted: #6c757d; |
|
|
--lm-text-report: #212529; |
|
|
--lm-border-primary: var(--lm-primary-color); |
|
|
--lm-border-secondary: #dee2e6; |
|
|
--lm-border-suggestion: #d5d9de; |
|
|
--lm-border-suggestion-hover: #c8ced3; |
|
|
--lm-shadow-soft: 0 4px 12px rgba(0, 0, 0, 0.08); |
|
|
--lm-shadow-hover: 0 6px 16px rgba(0, 0, 0, 0.12); |
|
|
--lm-shadow-inset: inset 0 1px 4px rgba(0,0,0,0.04); |
|
|
--lm-highlight-finding: #dc3545; |
|
|
--lm-highlight-device: var(--lm-primary-color); |
|
|
--lm-scrollbar-thumb: var(--lm-primary-color); |
|
|
--lm-scrollbar-track: #f0f0f0; |
|
|
|
|
|
|
|
|
--dm-primary-color: #e53935; |
|
|
--dm-primary-color-rgb: 229, 57, 53; |
|
|
--dm-bg-primary: #1a1a1a; |
|
|
--dm-bg-container: rgba(33, 33, 33, 0.9); |
|
|
--dm-bg-content: #2d2d2d; |
|
|
--dm-bg-input: #3a3a3a; |
|
|
--dm-bg-user-msg: #5e2828; |
|
|
--dm-bg-bot-msg: #3f3f3f; |
|
|
--dm-bg-suggestion: #4a4a4a; |
|
|
--dm-bg-suggestion-hover: #5a5a5a; |
|
|
--dm-text-primary: #f5f5f5; |
|
|
--dm-text-secondary: #bdbdbd; |
|
|
--dm-text-muted: #9e9e9e; |
|
|
--dm-text-report: #f5f5f5; |
|
|
--dm-text-input-placeholder: #a0a0a0; |
|
|
--dm-border-primary: var(--dm-primary-color); |
|
|
--dm-border-secondary: #555555; |
|
|
--dm-border-suggestion: #666666; |
|
|
--dm-border-suggestion-hover: #777777; |
|
|
--dm-shadow-soft: 0 4px 12px rgba(0, 0, 0, 0.4); |
|
|
--dm-shadow-hover: 0 6px 16px rgba(0, 0, 0, 0.5); |
|
|
--dm-shadow-inset: inset 0 1px 4px rgba(0,0,0,0.2); |
|
|
--dm-highlight-finding: #ff7961; |
|
|
--dm-highlight-device: #6ec6ff; |
|
|
--dm-scrollbar-thumb: var(--dm-primary-color); |
|
|
--dm-scrollbar-track: #424242; |
|
|
|
|
|
|
|
|
--border-radius-sm: 6px; |
|
|
--border-radius-md: 8px; |
|
|
--border-radius-lg: 12px; |
|
|
--transition-speed: 0.3s; |
|
|
--transition-easing: ease; |
|
|
|
|
|
|
|
|
--primary-color: var(--lm-primary-color); |
|
|
--primary-color-rgb: var(--lm-primary-color-rgb); |
|
|
--bg-primary: var(--lm-bg-primary); |
|
|
--bg-container: var(--lm-bg-container); |
|
|
--bg-content: var(--lm-bg-content); |
|
|
--bg-input: var(--lm-bg-input); |
|
|
--bg-user-msg: var(--lm-bg-user-msg); |
|
|
--bg-bot-msg: var(--lm-bg-bot-msg); |
|
|
--bg-suggestion: var(--lm-bg-suggestion); |
|
|
--bg-suggestion-hover: var(--lm-bg-suggestion-hover); |
|
|
--text-primary: var(--lm-text-primary); |
|
|
--text-secondary: var(--lm-text-secondary); |
|
|
--text-muted: var(--lm-text-muted); |
|
|
--text-report: var(--lm-text-report); |
|
|
--text-input-placeholder: inherit; |
|
|
--border-primary: var(--lm-border-primary); |
|
|
--border-secondary: var(--lm-border-secondary); |
|
|
--border-suggestion: var(--lm-border-suggestion); |
|
|
--border-suggestion-hover: var(--lm-border-suggestion-hover); |
|
|
--shadow-soft: var(--lm-shadow-soft); |
|
|
--shadow-hover: var(--lm-shadow-hover); |
|
|
--shadow-inset: var(--lm-shadow-inset); |
|
|
--highlight-finding-color: var(--lm-highlight-finding); |
|
|
--highlight-device-color: var(--lm-highlight-device); |
|
|
--scrollbar-thumb: var(--lm-scrollbar-thumb); |
|
|
--scrollbar-track: var(--lm-scrollbar-track); |
|
|
} |
|
|
|
|
|
|
|
|
body.dark-mode { |
|
|
--primary-color: var(--dm-primary-color); |
|
|
--primary-color-rgb: var(--dm-primary-color-rgb); |
|
|
--bg-primary: var(--dm-bg-primary); |
|
|
--bg-container: var(--dm-bg-container); |
|
|
--bg-content: var(--dm-bg-content); |
|
|
--bg-input: var(--dm-bg-input); |
|
|
--bg-user-msg: var(--dm-bg-user-msg); |
|
|
--bg-bot-msg: var(--dm-bg-bot-msg); |
|
|
--bg-suggestion: var(--dm-bg-suggestion); |
|
|
--bg-suggestion-hover: var(--dm-bg-suggestion-hover); |
|
|
--text-primary: var(--dm-text-primary); |
|
|
--text-secondary: var(--dm-text-secondary); |
|
|
--text-muted: var(--dm-text-muted); |
|
|
--text-report: var(--dm-text-report); |
|
|
--text-input-placeholder: var(--dm-text-input-placeholder); |
|
|
--border-primary: var(--dm-border-primary); |
|
|
--border-secondary: var(--dm-border-secondary); |
|
|
--border-suggestion: var(--dm-border-suggestion); |
|
|
--border-suggestion-hover: var(--dm-border-suggestion-hover); |
|
|
--shadow-soft: var(--dm-shadow-soft); |
|
|
--shadow-hover: var(--dm-shadow-hover); |
|
|
--shadow-inset: var(--dm-shadow-inset); |
|
|
--highlight-finding-color: var(--dm-highlight-finding); |
|
|
--highlight-device-color: var(--dm-highlight-device); |
|
|
--scrollbar-thumb: var(--dm-scrollbar-thumb); |
|
|
--scrollbar-track: var(--dm-scrollbar-track); |
|
|
} |
|
|
|
|
|
|
|
|
body { |
|
|
padding-top: 20px; |
|
|
padding-bottom: 30px; |
|
|
background: var(--bg-primary); |
|
|
color: var(--text-primary); |
|
|
font-family: 'Roboto', -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif; |
|
|
overflow-x: hidden; |
|
|
transition: background-color var(--transition-speed) var(--transition-easing), color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
|
|
|
.container-fluid { |
|
|
max-width: 1400px; |
|
|
background-color: var(--bg-container); |
|
|
backdrop-filter: blur(10px); |
|
|
-webkit-backdrop-filter: blur(10px); |
|
|
padding: 35px; |
|
|
border-radius: var(--border-radius-lg); |
|
|
box-shadow: var(--shadow-soft); |
|
|
border: 3px solid var(--border-primary); |
|
|
transition: background-color var(--transition-speed) var(--transition-easing), border-color var(--transition-speed) var(--transition-easing), box-shadow var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
|
|
|
|
|
|
#darkModeToggle { |
|
|
border: 1px solid var(--border-secondary); |
|
|
color: var(--text-secondary); |
|
|
background-color: transparent; |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
padding: 0; |
|
|
border-radius: 50%; |
|
|
transition: background-color var(--transition-speed) var(--transition-easing), color var(--transition-speed) var(--transition-easing), border-color var(--transition-speed) var(--transition-easing), transform 0.2s ease; |
|
|
} |
|
|
#darkModeToggle:hover { |
|
|
background-color: rgba(var(--primary-color-rgb), 0.1); |
|
|
border-color: var(--primary-color); |
|
|
color: var(--primary-color); |
|
|
transform: scale(1.05); |
|
|
} |
|
|
body.dark-mode #darkModeToggle { |
|
|
border-color: var(--dm-border-secondary); |
|
|
color: var(--dm-text-secondary); |
|
|
background-color: #444; |
|
|
} |
|
|
body.dark-mode #darkModeToggle:hover { |
|
|
background-color: #555; |
|
|
border-color: var(--dm-primary-color); |
|
|
color: var(--dm-primary-color); |
|
|
transform: scale(1.05); |
|
|
} |
|
|
|
|
|
h1 { |
|
|
color: var(--primary-color); |
|
|
font-weight: 700; |
|
|
margin-bottom: 0 !important; |
|
|
font-size: 2.1rem; |
|
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.05); |
|
|
transition: color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
body.dark-mode h1 { |
|
|
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3); |
|
|
} |
|
|
|
|
|
h2 { |
|
|
font-size: 1.5rem; |
|
|
font-weight: 500; |
|
|
color: var(--text-secondary); |
|
|
margin-bottom: 20px; |
|
|
padding-bottom: 10px; |
|
|
border-bottom: 3px solid var(--primary-color); |
|
|
display: inline-block; |
|
|
transition: color var(--transition-speed) var(--transition-easing), border-color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
|
|
|
|
|
|
.main-row { |
|
|
margin-top: 30px; |
|
|
} |
|
|
|
|
|
|
|
|
#chat-column { |
|
|
border-right: 1px solid var(--border-secondary); |
|
|
padding-right: 35px; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
height: calc(85vh - 110px); |
|
|
min-height: 500px; |
|
|
transition: opacity 0.5s ease, filter 0.5s ease, border-color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
#chat-container { |
|
|
flex-grow: 1; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
background-color: var(--bg-content); |
|
|
border: 1px solid var(--border-secondary); |
|
|
border-radius: var(--border-radius-md); |
|
|
padding: 20px; |
|
|
overflow: hidden; |
|
|
box-shadow: var(--shadow-inset); |
|
|
transition: background-color var(--transition-speed) var(--transition-easing), border-color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
#chat-column.disabled { |
|
|
opacity: 0.5; |
|
|
filter: grayscale(50%); |
|
|
pointer-events: none; |
|
|
} |
|
|
body.dark-mode #chat-column.disabled { |
|
|
filter: grayscale(70%); |
|
|
} |
|
|
#chat-column.disabled #chat-placeholder { display: block; } |
|
|
#chat-placeholder { |
|
|
display: none; |
|
|
text-align: center; padding: 60px 15px; |
|
|
color: var(--text-muted); font-style: italic; font-size: 0.9rem; |
|
|
align-self: center; margin-top: auto; margin-bottom: auto; |
|
|
} |
|
|
#chat-messages { |
|
|
flex-grow: 1; overflow-y: auto; margin-bottom: 15px; padding-right: 10px; |
|
|
scrollbar-width: thin; |
|
|
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); |
|
|
} |
|
|
#chat-messages::-webkit-scrollbar { width: 6px; } |
|
|
#chat-messages::-webkit-scrollbar-track { background: var(--scrollbar-track); border-radius: 3px;} |
|
|
#chat-messages::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb); border-radius: 3px; border: 1px solid var(--scrollbar-track);} |
|
|
|
|
|
.chat-message { |
|
|
margin-bottom: 15px; padding: 12px 18px; |
|
|
border-radius: var(--border-radius-lg); max-width: 85%; |
|
|
word-wrap: break-word; line-height: 1.55; |
|
|
box-shadow: 0 2px 5px rgba(0,0,0,0.06); |
|
|
opacity: 0; |
|
|
transform: translateY(10px); |
|
|
animation: fadeSlideIn 0.4s ease forwards; |
|
|
color: var(--text-primary); |
|
|
transition: background-color var(--transition-speed) var(--transition-easing), color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
@keyframes fadeSlideIn { to { opacity: 1; transform: translateY(0); } } |
|
|
|
|
|
.user-message { |
|
|
background-color: var(--bg-user-msg); margin-left: auto; |
|
|
border-bottom-right-radius: var(--border-radius-sm); text-align: right; |
|
|
} |
|
|
.bot-message { |
|
|
background-color: var(--bg-bot-msg); margin-right: auto; |
|
|
border-bottom-left-radius: var(--border-radius-sm); text-align: left; |
|
|
} |
|
|
.bot-message.thinking { |
|
|
font-style: italic; color: var(--text-muted); display: flex; align-items: center; |
|
|
} |
|
|
.bot-message.thinking::before { |
|
|
content: ''; display: inline-block; width: 16px; height: 16px; |
|
|
border: 2px solid var(--primary-color); border-top-color: transparent; |
|
|
border-radius: 50%; animation: spin 1s linear infinite; margin-right: 8px; |
|
|
} |
|
|
@keyframes spin { to { transform: rotate(360deg); } } |
|
|
body.dark-mode .chat-message { |
|
|
color: var(--dm-text-primary); |
|
|
box-shadow: 0 2px 5px rgba(0,0,0,0.2); |
|
|
} |
|
|
|
|
|
|
|
|
#chat-form { display: flex; margin-top: auto; gap: 10px; } |
|
|
#chat-input { |
|
|
flex-grow: 1; border-radius: 25px; padding: 10px 18px; |
|
|
border: 1px solid var(--border-secondary); |
|
|
background-color: var(--bg-input); |
|
|
color: var(--text-primary); |
|
|
transition: box-shadow var(--transition-speed) var(--transition-easing), border-color var(--transition-speed) var(--transition-easing), background-color var(--transition-speed) var(--transition-easing), color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
#chat-input:focus { |
|
|
box-shadow: 0 0 0 0.2rem rgba(var(--primary-color-rgb), 0.25); |
|
|
border-color: var(--primary-color); |
|
|
} |
|
|
#chat-input::placeholder { |
|
|
color: var(--text-input-placeholder); |
|
|
opacity: 1; |
|
|
} |
|
|
body.dark-mode #chat-input { |
|
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.3); |
|
|
} |
|
|
body.dark-mode #chat-input:focus { |
|
|
background-color: #4f4f4f; |
|
|
} |
|
|
|
|
|
#send-button { |
|
|
border-radius: 50%; width: 45px; height: 45px; padding: 0; |
|
|
display: flex; align-items: center; justify-content: center; flex-shrink: 0; |
|
|
transition: background-color var(--transition-speed) var(--transition-easing), transform var(--transition-speed) var(--transition-easing), border-color var(--transition-speed) var(--transition-easing), filter var(--transition-speed) var(--transition-easing); |
|
|
background-color: var(--primary-color); |
|
|
border-color: var(--primary-color); |
|
|
color: white; |
|
|
} |
|
|
#send-button:hover { |
|
|
transform: scale(1.1); |
|
|
filter: brightness(115%); |
|
|
|
|
|
background-color: var(--primary-color); |
|
|
border-color: var(--primary-color); |
|
|
} |
|
|
#send-button svg { width: 20px; height: 20px; fill: white; } |
|
|
|
|
|
|
|
|
#example-questions { |
|
|
margin-top: 12px; |
|
|
padding-top: 10px; |
|
|
border-top: 1px dashed var(--border-secondary); |
|
|
text-align: left; |
|
|
transition: border-color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
#example-questions small { |
|
|
display: block; |
|
|
margin-bottom: 8px; |
|
|
font-size: 0.8rem; |
|
|
color: var(--text-secondary); |
|
|
font-weight: 500; |
|
|
transition: color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
.suggestion-btn { |
|
|
background-color: var(--bg-suggestion); |
|
|
border: 1px solid var(--border-suggestion); |
|
|
color: var(--primary-color); |
|
|
font-size: 0.75rem; |
|
|
padding: 4px 10px; |
|
|
border-radius: 15px; |
|
|
margin-right: 6px; |
|
|
margin-bottom: 6px; |
|
|
cursor: pointer; |
|
|
transition: background-color 0.2s ease, border-color 0.2s ease, color 0.2s ease, filter 0.2s ease; |
|
|
text-decoration: none; |
|
|
display: inline-block; |
|
|
} |
|
|
.suggestion-btn:hover { |
|
|
background-color: var(--bg-suggestion-hover); |
|
|
border-color: var(--border-suggestion-hover); |
|
|
filter: brightness(110%); |
|
|
|
|
|
color: var(--primary-color); |
|
|
} |
|
|
body.dark-mode .suggestion-btn { |
|
|
color: var(--dm-text-primary); |
|
|
background-color: var(--dm-bg-suggestion); |
|
|
border-color: var(--dm-border-suggestion); |
|
|
} |
|
|
body.dark-mode .suggestion-btn:hover { |
|
|
color: var(--dm-primary-color); |
|
|
background-color: var(--dm-bg-suggestion-hover); |
|
|
border-color: var(--dm-border-suggestion-hover); |
|
|
filter: brightness(115%); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
#content-column { |
|
|
padding-left: 35px; |
|
|
max-height: calc(85vh - 110px); |
|
|
overflow-y: auto; scrollbar-width: thin; |
|
|
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); |
|
|
position: relative; |
|
|
} |
|
|
#content-column::-webkit-scrollbar { width: 6px; } |
|
|
#content-column::-webkit-scrollbar-track { background: var(--scrollbar-track); border-radius: 3px;} |
|
|
#content-column::-webkit-scrollbar-thumb { background-color: var(--scrollbar-thumb); border-radius: 3px; border: 1px solid var(--scrollbar-track);} |
|
|
|
|
|
|
|
|
#form-wrapper { |
|
|
transition: opacity var(--transition-speed) var(--transition-easing), |
|
|
transform var(--transition-speed) var(--transition-easing), |
|
|
max-height 0.6s var(--transition-easing), |
|
|
margin-bottom var(--transition-speed) var(--transition-easing), |
|
|
padding-top var(--transition-speed) var(--transition-easing), |
|
|
padding-bottom var(--transition-speed) var(--transition-easing), |
|
|
border var(--transition-speed) var(--transition-easing); |
|
|
overflow: hidden; max-height: 800px; transform: translateY(0); |
|
|
opacity: 1; margin-bottom: 30px; padding-top: 0; padding-bottom: 0; border: none; |
|
|
} |
|
|
#upload-form { |
|
|
padding: 30px; |
|
|
border: 1px solid var(--border-secondary); |
|
|
border-radius: var(--border-radius-md); |
|
|
background-color: var(--bg-content); margin-bottom: 0; |
|
|
transition: background-color var(--transition-speed) var(--transition-easing), border-color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
.form-label { |
|
|
font-weight: 500; margin-bottom: 0.6rem; |
|
|
color: var(--text-secondary); |
|
|
transition: color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
.form-control, .form-select { |
|
|
background-color: var(--bg-input); |
|
|
color: var(--text-primary); |
|
|
border: 1px solid var(--border-secondary); |
|
|
transition: background-color var(--transition-speed) var(--transition-easing), color var(--transition-speed) var(--transition-easing), border-color var(--transition-speed) var(--transition-easing), box-shadow var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
.form-control::placeholder { |
|
|
color: var(--text-input-placeholder); |
|
|
opacity: 1; |
|
|
} |
|
|
.form-control:focus, .form-select:focus { |
|
|
background-color: var(--bg-input); |
|
|
color: var(--text-primary); |
|
|
border-color: var(--primary-color); |
|
|
box-shadow: 0 0 0 0.2rem rgba(var(--primary-color-rgb), 0.25); |
|
|
} |
|
|
body.dark-mode .form-control, body.dark-mode .form-select { |
|
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.3); |
|
|
} |
|
|
body.dark-mode .form-select { |
|
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23f5f5f5' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"); |
|
|
} |
|
|
body.dark-mode .form-control:focus, body.dark-mode .form-select:focus { |
|
|
background-color: #4f4f4f; |
|
|
} |
|
|
|
|
|
.form-control-sm, .form-select-sm { |
|
|
padding: 0.45rem 0.9rem; font-size: 0.9rem; |
|
|
border-radius: var(--border-radius-sm); |
|
|
} |
|
|
.form-text { |
|
|
color: var(--text-muted); |
|
|
transition: color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
|
|
|
|
|
|
.btn-primary { |
|
|
background-color: var(--primary-color); |
|
|
border-color: var(--primary-color); |
|
|
padding: 12px 22px; |
|
|
font-size: 1rem; font-weight: 500; |
|
|
border-radius: var(--border-radius-sm); |
|
|
color: #fff; |
|
|
transition: background-color var(--transition-speed) var(--transition-easing), |
|
|
border-color var(--transition-speed) var(--transition-easing), |
|
|
box-shadow var(--transition-speed) var(--transition-easing), |
|
|
filter var(--transition-speed) var(--transition-easing), |
|
|
color var(--transition-speed) var(--transition-easing), |
|
|
opacity var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
|
|
|
|
|
|
.btn-primary:hover { |
|
|
filter: brightness(115%); |
|
|
|
|
|
background-color: var(--primary-color); |
|
|
border-color: var(--primary-color); |
|
|
color: #fff; |
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); |
|
|
} |
|
|
.btn-primary:focus, |
|
|
.btn-primary:active { |
|
|
filter: brightness(110%); |
|
|
background-color: var(--primary-color); |
|
|
border-color: var(--primary-color); |
|
|
color: #fff; |
|
|
box-shadow: 0 0 0 0.2rem rgba(var(--primary-color-rgb), 0.5), 0 4px 8px rgba(0, 0, 0, 0.1); |
|
|
} |
|
|
.btn-primary:disabled, |
|
|
.btn-primary.disabled { |
|
|
filter: none; |
|
|
background-color: var(--primary-color); |
|
|
border-color: var(--primary-color); |
|
|
opacity: 0.65; |
|
|
} |
|
|
|
|
|
|
|
|
body.dark-mode .btn-primary { |
|
|
|
|
|
background-color: var(--dm-primary-color); |
|
|
border-color: var(--dm-primary-color); |
|
|
color: #fff; |
|
|
} |
|
|
body.dark-mode .btn-primary:hover { |
|
|
filter: brightness(120%); |
|
|
background-color: var(--dm-primary-color); |
|
|
border-color: var(--dm-primary-color); |
|
|
color: #fff; |
|
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3); |
|
|
} |
|
|
body.dark-mode .btn-primary:focus, |
|
|
body.dark-mode .btn-primary:active { |
|
|
filter: brightness(110%); |
|
|
background-color: var(--dm-primary-color); |
|
|
border-color: var(--dm-primary-color); |
|
|
color: #fff; |
|
|
|
|
|
box-shadow: 0 0 0 0.2rem rgba(var(--dm-primary-color-rgb), 0.5), 0 4px 8px rgba(0, 0, 0, 0.3); |
|
|
} |
|
|
body.dark-mode .btn-primary:disabled, |
|
|
body.dark-mode .btn-primary.disabled { |
|
|
filter: none; |
|
|
background-color: var(--dm-primary-color); |
|
|
border-color: var(--dm-primary-color); |
|
|
opacity: 0.5; |
|
|
} |
|
|
|
|
|
.spinner-border { display: none; margin-right: 8px; width: 1.1rem; height: 1.1rem; color: currentColor; } |
|
|
#upload-form.processing .spinner-border { display: inline-block; } |
|
|
#upload-form.processing button[type="submit"] { cursor: not-allowed; opacity: 0.7; } |
|
|
|
|
|
|
|
|
|
|
|
body.results-loaded #form-wrapper { |
|
|
opacity: 0; transform: translateY(-20px) scale(0.95); |
|
|
max-height: 0; padding-top: 0; padding-bottom: 0; |
|
|
margin-top: 0; margin-bottom: 0; border-width: 0; |
|
|
pointer-events: none; |
|
|
} |
|
|
|
|
|
|
|
|
.result-area { |
|
|
margin-top: 0; |
|
|
opacity: 0; |
|
|
transform: translateY(20px); |
|
|
transition: opacity 0.6s ease-out 0.2s, transform 0.6s ease-out 0.2s; |
|
|
text-align: center; |
|
|
} |
|
|
body.results-loaded .result-area { |
|
|
opacity: 1; |
|
|
transform: translateY(0); |
|
|
} |
|
|
|
|
|
|
|
|
#patient-info-section { |
|
|
text-align: center; |
|
|
margin-bottom: 25px; |
|
|
padding-bottom: 15px; |
|
|
border-bottom: 1px solid var(--border-secondary); |
|
|
transition: border-color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
#patient-info-section h3 { |
|
|
font-size: 1.25rem; |
|
|
font-weight: 500; |
|
|
color: var(--text-secondary); |
|
|
margin-bottom: 10px; |
|
|
border-bottom: none; |
|
|
display: inline-block; |
|
|
transition: color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
#patient-info-data { |
|
|
font-size: 0.95rem; |
|
|
color: var(--text-primary); |
|
|
transition: color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
#patient-info-data span { |
|
|
margin: 0 8px; |
|
|
color: var(--text-muted); |
|
|
transition: color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
#patient-info-data strong { |
|
|
font-weight: 500; |
|
|
color: var(--text-secondary); |
|
|
transition: color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
|
|
|
|
|
|
.result-area .row .col-md-6 { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
margin-bottom: 20px; |
|
|
} |
|
|
.result-area .row .col-md-6 h2 { |
|
|
width: 100%; |
|
|
text-align: center; |
|
|
} |
|
|
.img-preview-container, .report-box-container { |
|
|
width: 100%; |
|
|
max-width: 500px; |
|
|
margin: 0 auto; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
#image-result-area { position: relative; width: 100%; } |
|
|
.img-preview { |
|
|
max-width: 100%; height: auto; border: 1px solid var(--border-secondary); |
|
|
border-radius: var(--border-radius-md); box-shadow: var(--shadow-soft); |
|
|
display: block; |
|
|
margin: 0 auto; |
|
|
transition: box-shadow var(--transition-speed) var(--transition-easing), border-color var(--transition-speed) var(--transition-easing); |
|
|
cursor: crosshair; |
|
|
} |
|
|
.img-preview:hover { box-shadow: var(--shadow-hover); } |
|
|
|
|
|
#zoom-preview-panel { |
|
|
position: absolute; width: 250px; height: 250px; |
|
|
background-color: var(--bg-content); |
|
|
border: 2px solid var(--primary-color); |
|
|
border-radius: var(--border-radius-md); box-shadow: var(--shadow-hover); |
|
|
background-repeat: no-repeat; background-size: 0 0; background-position: 0 0; |
|
|
opacity: 0; visibility: hidden; |
|
|
transition: opacity 0.2s ease-out, visibility 0s linear 0.2s, background-color var(--transition-speed) var(--transition-easing), border-color var(--transition-speed) var(--transition-easing); |
|
|
pointer-events: none; z-index: 100; overflow: hidden; |
|
|
top: 0; left: calc(100% + 15px); |
|
|
} |
|
|
|
|
|
@media (max-width: 1200px) and (min-width: 992px) { |
|
|
#zoom-preview-panel { left: auto; right: calc(100% + 15px); } |
|
|
} |
|
|
|
|
|
|
|
|
#zoom-controls { |
|
|
max-width: 300px; |
|
|
margin-left: auto; |
|
|
margin-right: auto; |
|
|
opacity: 0; |
|
|
transition: opacity 0.5s ease-in-out; |
|
|
display: none; |
|
|
} |
|
|
body.results-loaded #zoom-controls { |
|
|
display: block; |
|
|
opacity: 1; |
|
|
} |
|
|
#zoom-controls .form-label-sm { |
|
|
font-size: 0.8rem; |
|
|
color: var(--text-secondary); |
|
|
transition: color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
#zoom-slider { |
|
|
cursor: pointer; |
|
|
|
|
|
accent-color: var(--primary-color); |
|
|
transition: accent-color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
|
|
|
body.dark-mode #zoom-slider::-webkit-slider-runnable-track { background-color: #555; } |
|
|
body.dark-mode #zoom-slider::-moz-range-track { background-color: #555; } |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 991.98px) { |
|
|
#zoom-preview-panel, #zoom-controls { |
|
|
display: none !important; |
|
|
} |
|
|
.img-preview { cursor: default; } |
|
|
} |
|
|
|
|
|
|
|
|
.report-box { |
|
|
border: 1px solid var(--border-secondary); padding: 25px; |
|
|
background-color: var(--bg-content); border-radius: var(--border-radius-md); |
|
|
min-height: 250px; |
|
|
font-family: 'Roboto', 'Consolas', 'Courier New', monospace; |
|
|
font-size: 0.95rem; |
|
|
color: var(--text-report); white-space: pre-wrap; word-wrap: break-word; |
|
|
box-shadow: var(--shadow-soft); line-height: 1.7; |
|
|
text-align: left; |
|
|
transition: background-color var(--transition-speed) var(--transition-easing), color var(--transition-speed) var(--transition-easing), border-color var(--transition-speed) var(--transition-easing), box-shadow var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
|
|
|
|
|
|
.medical-term-finding { |
|
|
font-weight: 600; |
|
|
color: var(--highlight-finding-color); |
|
|
transition: color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
.medical-term-device { |
|
|
font-weight: 600; |
|
|
color: var(--highlight-device-color); |
|
|
transition: color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
|
|
|
|
|
|
.medical-icon { |
|
|
margin-left: 6px; |
|
|
color: var(--text-secondary); |
|
|
font-size: 0.9em; |
|
|
cursor: help; |
|
|
vertical-align: middle; |
|
|
transition: color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
|
|
|
.tooltip .tooltip-inner { |
|
|
background-color: var(--text-secondary); |
|
|
color: var(--bg-content); |
|
|
font-size: 0.8rem; |
|
|
padding: 5px 10px; |
|
|
border-radius: var(--border-radius-sm); |
|
|
transition: background-color var(--transition-speed) ease, color var(--transition-speed) ease; |
|
|
} |
|
|
.tooltip.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before, |
|
|
.tooltip.bs-tooltip-top .tooltip-arrow::before { |
|
|
border-top-color: var(--text-secondary); |
|
|
transition: border-top-color var(--transition-speed) ease; |
|
|
} |
|
|
|
|
|
|
|
|
body.dark-mode .tooltip .tooltip-inner { |
|
|
background-color: #f0f0f0; |
|
|
color: #333; |
|
|
} |
|
|
body.dark-mode .tooltip.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before, |
|
|
body.dark-mode .tooltip.bs-tooltip-top .tooltip-arrow::before { |
|
|
border-top-color: #f0f0f0; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#report-context-data { display: none; } |
|
|
|
|
|
|
|
|
#reset-button { |
|
|
display: none; |
|
|
margin-top: 30px; |
|
|
margin-bottom: 0; |
|
|
opacity: 0; transition: opacity var(--transition-speed) var(--transition-easing) 0.5s, color var(--transition-speed) var(--transition-easing), border-color var(--transition-speed) var(--transition-easing), background-color var(--transition-speed) var(--transition-easing); |
|
|
padding: 10px 20px; |
|
|
font-size: 0.95rem; border-radius: var(--border-radius-sm); |
|
|
align-items: center; justify-content: center; gap: 8px; |
|
|
color: #6c757d; |
|
|
border: 1px solid #6c757d; |
|
|
background-color: transparent; |
|
|
} |
|
|
#reset-button:hover { |
|
|
color: #fff; |
|
|
background-color: #5c636a; |
|
|
border-color: #565e64; |
|
|
} |
|
|
body.dark-mode #reset-button { |
|
|
color: var(--dm-text-secondary); |
|
|
border-color: var(--dm-border-secondary); |
|
|
} |
|
|
body.dark-mode #reset-button:hover { |
|
|
color: var(--dm-bg-primary); |
|
|
background-color: var(--dm-text-secondary); |
|
|
border-color: var(--dm-text-secondary); |
|
|
} |
|
|
body.results-loaded #reset-button { |
|
|
display: inline-flex; |
|
|
opacity: 1; |
|
|
} |
|
|
#reset-button svg { width: 1em; height: 1em; fill: currentColor; } |
|
|
|
|
|
|
|
|
#student-notes-section { |
|
|
margin-top: 40px; |
|
|
padding-top: 25px; |
|
|
border-top: 1px solid var(--border-secondary); |
|
|
text-align: left; |
|
|
opacity: 0; |
|
|
display: none; |
|
|
transition: opacity 0.6s ease-out 0.4s, border-color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
body.results-loaded #student-notes-section { |
|
|
display: block; |
|
|
opacity: 1; |
|
|
} |
|
|
.notes-input-area { |
|
|
background-color: var(--bg-content); |
|
|
padding: 20px; |
|
|
border-radius: var(--border-radius-md); |
|
|
border: 1px solid var(--border-secondary); |
|
|
box-shadow: var(--shadow-inset); |
|
|
transition: background-color var(--transition-speed) var(--transition-easing), border-color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
#student-notes-textarea, |
|
|
#student-notes-keywords { |
|
|
font-size: 0.9rem; |
|
|
line-height: 1.6; |
|
|
resize: vertical; |
|
|
background-color: var(--bg-input); |
|
|
color: var(--text-primary); |
|
|
border: 1px solid var(--border-secondary); |
|
|
transition: background-color var(--transition-speed) var(--transition-easing), color var(--transition-speed) var(--transition-easing), border-color var(--transition-speed) var(--transition-easing), box-shadow var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
#student-notes-textarea { |
|
|
min-height: 150px; |
|
|
} |
|
|
#student-notes-textarea::placeholder, |
|
|
#student-notes-keywords::placeholder { |
|
|
color: var(--text-input-placeholder); |
|
|
opacity: 1; |
|
|
} |
|
|
#student-notes-textarea:focus, |
|
|
#student-notes-keywords:focus { |
|
|
background-color: var(--bg-input); |
|
|
color: var(--text-primary); |
|
|
border-color: var(--primary-color); |
|
|
box-shadow: 0 0 0 0.2rem rgba(var(--primary-color-rgb), 0.25); |
|
|
} |
|
|
body.dark-mode #student-notes-textarea, |
|
|
body.dark-mode #student-notes-keywords { |
|
|
box-shadow: inset 0 1px 3px rgba(0,0,0,0.3); |
|
|
} |
|
|
body.dark-mode #student-notes-textarea:focus, |
|
|
body.dark-mode #student-notes-keywords:focus { |
|
|
background-color: #4f4f4f; |
|
|
} |
|
|
|
|
|
|
|
|
.notes-input-area label.form-label { |
|
|
font-size: 0.95rem; |
|
|
color: var(--text-secondary); |
|
|
font-weight: 500; |
|
|
transition: color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
#copy-notes-button { |
|
|
font-weight: 500; |
|
|
color: var(--primary-color); |
|
|
border: 1px solid var(--primary-color); |
|
|
background-color: transparent; |
|
|
transition: color var(--transition-speed) var(--transition-easing), border-color var(--transition-speed) var(--transition-easing), background-color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
#copy-notes-button:hover { |
|
|
color: #fff; |
|
|
background-color: var(--primary-color); |
|
|
border-color: var(--primary-color); |
|
|
} |
|
|
body.dark-mode #copy-notes-button { |
|
|
|
|
|
} |
|
|
body.dark-mode #copy-notes-button:hover { |
|
|
color: #fff; |
|
|
background-color: var(--dm-primary-color); |
|
|
border-color: var(--dm-primary-color); |
|
|
} |
|
|
|
|
|
#copy-notes-button:disabled { |
|
|
opacity: 0.8; |
|
|
cursor: default; |
|
|
|
|
|
background-color: #d1e7dd; |
|
|
border-color: #badbcc; |
|
|
color: #0f5132; |
|
|
} |
|
|
body.dark-mode #copy-notes-button:disabled { |
|
|
background-color: #143625; |
|
|
border-color: #1c4a32; |
|
|
color: #75b798; |
|
|
} |
|
|
|
|
|
#copy-feedback { |
|
|
font-weight: 500; |
|
|
font-size: 0.85rem; |
|
|
opacity: 0; |
|
|
transition: opacity 0.5s ease, color 0.5s ease; |
|
|
color: #198754; |
|
|
} |
|
|
body.dark-mode #copy-feedback.text-success { |
|
|
color: #61e7a9; |
|
|
} |
|
|
body.dark-mode #copy-feedback.text-danger { |
|
|
color: #ff8a80; |
|
|
} |
|
|
|
|
|
|
|
|
.alert { |
|
|
|
|
|
transition: background-color var(--transition-speed) var(--transition-easing), color var(--transition-speed) var(--transition-easing), border-color var(--transition-speed) var(--transition-easing); |
|
|
} |
|
|
|
|
|
body.dark-mode .alert-warning { |
|
|
background-color: #4d3c11; |
|
|
border-color: #664d03; |
|
|
color: #ffecb5; |
|
|
} |
|
|
|
|
|
body.dark-mode .alert-danger { |
|
|
background-color: #4c161c; |
|
|
border-color: #842029; |
|
|
color: #fcc8cb; |
|
|
} |
|
|
|
|
|
body.dark-mode .btn-close { |
|
|
filter: invert(1) grayscale(100%) brightness(200%); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
@media (max-width: 991.98px) { |
|
|
#chat-column { |
|
|
border-right: none; border-bottom: 1px solid var(--border-secondary); |
|
|
padding-right: 0; margin-bottom: 30px; |
|
|
height: 60vh; min-height: 450px; |
|
|
} |
|
|
#content-column { |
|
|
padding-left: 0; max-height: none; overflow-y: visible; |
|
|
} |
|
|
.container-fluid { padding: 25px; } |
|
|
h1 { font-size: 1.9rem; } |
|
|
h2 { font-size: 1.4rem; } |
|
|
.result-area .row > div[class*="col-md-"] { |
|
|
width: 100%; |
|
|
margin-bottom: 25px; |
|
|
} |
|
|
} |
|
|
@media (max-width: 767.98px) { |
|
|
#title-and-toggle { flex-direction: column; align-items: center; gap: 10px; } |
|
|
h1 { font-size: 1.7rem; text-align: center; } |
|
|
h2 { font-size: 1.3rem; } |
|
|
.container-fluid { padding: 20px; } |
|
|
#chat-column { height: 55vh; min-height: 400px; } |
|
|
.btn-primary, #reset-button { font-size: 0.9rem; padding: 10px 18px;} |
|
|
#send-button { width: 40px; height: 40px; } |
|
|
#send-button svg { width: 18px; height: 18px; } |
|
|
#chat-input { padding: 8px 15px; } |
|
|
.report-box { padding: 20px; font-size: 0.9rem; min-height: 200px;} |
|
|
#student-notes-textarea { min-height: 120px; } |
|
|
} |
|
|
|
|
|
</style> |
|
|
</head> |
|
|
|
|
|
<body class="{{ 'results-loaded' if report or image_data else '' }}"> |
|
|
<div class="container-fluid"> |
|
|
|
|
|
<div id="title-and-toggle" class="d-flex justify-content-between align-items-center mb-4"> |
|
|
<h1 class="flex-grow-1">Chest X-ray Report Generation</h1> |
|
|
<button id="darkModeToggle" class="btn btn-sm flex-shrink-0" aria-label="Toggle dark mode" title="Toggle dark mode"> |
|
|
<i class="fas fa-moon"></i> |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="flash-message-container"> |
|
|
{% with messages = get_flashed_messages(with_categories=true) %} |
|
|
{% if messages %} |
|
|
{% for category, message in messages %} |
|
|
<div class="alert alert-{{ category }} alert-dismissible fade show mb-3" role="alert"> |
|
|
{{ message }} |
|
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> |
|
|
</div> |
|
|
{% endfor %} |
|
|
{% endif %} |
|
|
{% endwith %} |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="row main-row"> |
|
|
|
|
|
|
|
|
<div class="col-lg-5" id="chat-column" class="{{ 'disabled' if not report or not chatbot_available }}"> |
|
|
<h2><i class="fas fa-comments me-2"></i>Chat about Report</h2> |
|
|
<small class="text-muted mb-2 d-block">Ask questions based *only* on the generated report.</small> |
|
|
|
|
|
{% if report and not chatbot_available %} |
|
|
<div class="alert alert-warning small p-2 mt-2">Chatbot (Llama 3.1) is not available or failed to load. Please check server logs.</div> |
|
|
{% endif %} |
|
|
|
|
|
<div id="chat-container"> |
|
|
<div id="chat-placeholder">Generate a report first to enable chat.</div> |
|
|
<div id="chat-messages"> |
|
|
{% if report and chatbot_available %} |
|
|
<div class="chat-message bot-message" style="opacity: 1; transform: none;">Welcome! Ask me about the findings, impression, or specific details in the report above.</div> |
|
|
{% endif %} |
|
|
</div> |
|
|
<form id="chat-form" action="javascript:void(0);"> |
|
|
<input type="text" id="chat-input" class="form-control" placeholder="Type your question..." autocomplete="off" {% if not report or not chatbot_available %}disabled{% endif %}> |
|
|
<button type="submit" id="send-button" class="btn btn-primary" {% if not report or not chatbot_available %}disabled{% endif %}> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M3.478 2.405a.75.75 0 00-.926.94l2.432 7.905H13.5a.75.75 0 010 1.5H4.984l-2.432 7.905a.75.75 0 00.926.94 60.519 60.519 0 0018.445-8.986.75.75 0 000-1.218A60.517 60.517 0 003.478 2.405z" /></svg> |
|
|
</button> |
|
|
</form> |
|
|
<div id="example-questions" {% if not report or not chatbot_available %}style="display: none;"{% endif %}> |
|
|
<small>Example Questions:</small> |
|
|
<button class="suggestion-btn">What are the main findings?</button> |
|
|
<button class="suggestion-btn">Are there any tubes or lines mentioned?</button> |
|
|
<button class="suggestion-btn">Summarize the impression.</button> |
|
|
<button class="suggestion-btn">Is the heart size normal?</button> |
|
|
<button class="suggestion-btn">Any signs of pleural effusion?</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="col-lg-7" id="content-column"> |
|
|
|
|
|
|
|
|
<div id="form-wrapper"> |
|
|
<h2><i class="fas fa-upload me-2"></i>Generate Report</h2> |
|
|
<form id="upload-form" method="post" enctype="multipart/form-data" action="{{ url_for('predict') }}"> |
|
|
<div class="mb-3"> |
|
|
<label for="imageUpload" class="form-label">1. Upload Chest X-Ray Image:</label> |
|
|
<input class="form-control form-control-sm" type="file" id="imageUpload" name="image" accept="image/png, image/jpeg, image/jpg" required> |
|
|
<div class="form-text">Allowed formats: PNG, JPG, JPEG. Filename format like '...-View-Age-Gender-Ethnicity.png' helps extract patient info.</div> |
|
|
</div> |
|
|
<div class="mb-3"> |
|
|
<label for="vlmSelect" class="form-label">2. Choose Vision-Language Model:</label> |
|
|
<select class="form-select form-select-sm" id="vlmSelect" name="vlm_choice"> |
|
|
<option value="swin_t5_chexpert" selected>Swin-T5 (CheXpert Trained)</option> |
|
|
|
|
|
</select> |
|
|
</div> |
|
|
<div class="mb-3"> |
|
|
<label for="maxLength" class="form-label">3. Max Report Length (tokens):</label> |
|
|
<input type="number" class="form-control form-control-sm" id="maxLength" name="max_length" value="100" min="10" max="512"> |
|
|
<div class="form-text">Adjusts the maximum length of the generated report (default: 100).</div> |
|
|
</div> |
|
|
<button type="submit" class="btn btn-primary w-100 mt-2"> |
|
|
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span> |
|
|
<i class="fas fa-file-medical-alt me-1"></i> Generate Report |
|
|
</button> |
|
|
</form> |
|
|
</div> |
|
|
|
|
|
|
|
|
|
|
|
{% if report or image_data %} |
|
|
<div class="result-area"> |
|
|
|
|
|
{% if patient_info %} |
|
|
<div id="patient-info-section"> |
|
|
<h3><i class="fas fa-user-md me-2"></i>Patient Information</h3> |
|
|
<p id="patient-info-data"> |
|
|
<strong>View:</strong> {{ patient_info.view | e }} |
|
|
<span>|</span> |
|
|
<strong>Age:</strong> {{ patient_info.age | e }} |
|
|
<span>|</span> |
|
|
<strong>Gender:</strong> {{ patient_info.gender | e }} |
|
|
<span>|</span> |
|
|
<strong>Ethnicity:</strong> {{ patient_info.ethnicity | e }} |
|
|
</p> |
|
|
</div> |
|
|
{% elif image_data %} |
|
|
<div id="patient-info-section"> |
|
|
<p class="text-muted small"><em>Patient information could not be parsed from the filename.</em></p> |
|
|
</div> |
|
|
{% endif %} |
|
|
|
|
|
|
|
|
<div class="row"> |
|
|
|
|
|
{% if image_data %} |
|
|
<div class="col-md-6"> |
|
|
<div class="img-preview-container" id="image-result-area"> |
|
|
<h2><i class="fas fa-image me-2"></i>Uploaded Image</h2> |
|
|
<img id="uploaded-image" src="data:image/jpeg;base64,{{ image_data }}" alt="Uploaded Chest X-ray" class="img-preview"> |
|
|
<div id="zoom-preview-panel"></div> |
|
|
<small class="d-block text-muted mt-2">Hover over image to zoom (on desktop).</small> |
|
|
<div id="zoom-controls" class="mt-3"> |
|
|
<label for="zoom-slider" class="form-label form-label-sm mb-1 d-block text-center">Zoom Level: <span id="zoom-value">2.0</span>x</label> |
|
|
<input type="range" class="form-range" id="zoom-slider" min="1" max="5" step="0.1" value="2"> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
{% endif %} |
|
|
|
|
|
|
|
|
{% if report %} |
|
|
<div class="col-md-6"> |
|
|
<div class="report-box-container"> |
|
|
<h2><i class="fas fa-file-alt me-2"></i>Generated Report</h2> |
|
|
<div class="report-box"> |
|
|
|
|
|
{{ report | safe if report.count('<') > 0 else report | e }} |
|
|
</div> |
|
|
<div id="report-context-data" data-report="{{ report|e }}"></div> |
|
|
</div> |
|
|
</div> |
|
|
{% elif image_data %} |
|
|
<div class="col-md-6"> |
|
|
<div class="report-box-container"> |
|
|
<h2><i class="fas fa-file-alt me-2"></i>Generated Report</h2> |
|
|
<div class="alert alert-danger">Report generation failed. Please check the logs or try again.</div> |
|
|
</div> |
|
|
</div> |
|
|
{% endif %} |
|
|
</div> |
|
|
|
|
|
|
|
|
<button id="reset-button" class="btn btn-outline-secondary"> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-arrow-clockwise" viewBox="0 0 16 16" aria-hidden="true"> |
|
|
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.5.5 0 0 1 .908-.417A6 6 0 1 1 8 2v1z"/> |
|
|
<path d="M8 4.466V.534a.25.25 0 0 1 .41-.192l2.36 1.966c.12.1.12.284 0 .384L8.41 4.658A.25.25 0 0 1 8 4.466z"/> |
|
|
</svg> |
|
|
<span>Start Over / New Image</span> |
|
|
</button> |
|
|
|
|
|
</div> |
|
|
{% endif %} |
|
|
|
|
|
|
|
|
|
|
|
{% if report or image_data %} {# Only show notes if results exist #} |
|
|
<div id="student-notes-section"> |
|
|
<h3><i class="fas fa-user-graduate me-2"></i>Notes for Medical Student</h3> |
|
|
<div class="notes-input-area p-3 border rounded shadow-sm"> |
|
|
<div class="mb-3"> |
|
|
<label for="student-notes-textarea" class="form-label fw-bold">Observations & Learning Points:</label> |
|
|
<textarea class="form-control form-control-sm" id="student-notes-textarea" rows="8" placeholder="Enter key observations, differential diagnoses, relevant anatomy, clinical correlations, or questions for the student..."></textarea> |
|
|
</div> |
|
|
<div class="mb-3"> |
|
|
<label for="student-notes-keywords" class="form-label fw-bold">Keywords/Tags:</label> |
|
|
<input type="text" class="form-control form-control-sm" id="student-notes-keywords" placeholder="e.g., Pneumonia, Cardiomegaly, Atelectasis, PICC Line Placement"> |
|
|
</div> |
|
|
<button id="copy-notes-button" class="btn btn-sm btn-outline-primary w-100"> |
|
|
<i class="fas fa-copy me-1"></i> Copy Notes & Keywords to Clipboard |
|
|
</button> |
|
|
<small id="copy-feedback" class="d-block text-center text-success mt-2">Copied!</small> |
|
|
</div> |
|
|
</div> |
|
|
{% endif %} |
|
|
|
|
|
|
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> |
|
|
<script> |
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
|
|
|
|
|
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); |
|
|
var tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { |
|
|
|
|
|
return new bootstrap.Tooltip(tooltipTriggerEl, { container: 'body', boundary: document.body }); |
|
|
}); |
|
|
|
|
|
|
|
|
const darkModeToggle = document.getElementById('darkModeToggle'); |
|
|
const bodyElement = document.body; |
|
|
const toggleIcon = darkModeToggle?.querySelector('i'); |
|
|
|
|
|
const applyTheme = (theme) => { |
|
|
if (!toggleIcon) return; |
|
|
const isDark = theme === 'dark'; |
|
|
|
|
|
bodyElement.classList.toggle('dark-mode', isDark); |
|
|
toggleIcon.classList.toggle('fa-moon', !isDark); |
|
|
toggleIcon.classList.toggle('fa-sun', isDark); |
|
|
localStorage.setItem('theme', theme); |
|
|
const label = isDark ? 'Switch to light mode' : 'Switch to dark mode'; |
|
|
darkModeToggle.setAttribute('aria-label', label); |
|
|
darkModeToggle.setAttribute('title', label); |
|
|
|
|
|
|
|
|
const toggleTooltipInstance = bootstrap.Tooltip.getInstance(darkModeToggle); |
|
|
if (toggleTooltipInstance) { |
|
|
toggleTooltipInstance.setContent({ '.tooltip-inner': label }); |
|
|
} |
|
|
}; |
|
|
|
|
|
if (darkModeToggle && toggleIcon) { |
|
|
const preferredTheme = localStorage.getItem('theme') || |
|
|
(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); |
|
|
applyTheme(preferredTheme); |
|
|
|
|
|
darkModeToggle.addEventListener('click', () => { |
|
|
const newTheme = bodyElement.classList.contains('dark-mode') ? 'light' : 'dark'; |
|
|
applyTheme(newTheme); |
|
|
}); |
|
|
|
|
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => { |
|
|
if (!localStorage.getItem('theme')) { |
|
|
applyTheme(event.matches ? 'dark' : 'light'); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const form = document.getElementById('upload-form'); |
|
|
const formWrapper = document.getElementById('form-wrapper'); |
|
|
const submitButton = form?.querySelector('button[type="submit"]'); |
|
|
const spinner = submitButton?.querySelector('.spinner-border'); |
|
|
|
|
|
if (form && submitButton && formWrapper) { |
|
|
form.addEventListener('submit', function(e) { |
|
|
const fileInput = document.getElementById('imageUpload'); |
|
|
if (!fileInput || fileInput.files.length === 0) { return; } |
|
|
form.classList.add('processing'); |
|
|
submitButton.disabled = true; |
|
|
if (spinner) spinner.style.display = 'inline-block'; |
|
|
submitButton.querySelector('i')?.classList.add('d-none'); |
|
|
}); |
|
|
} |
|
|
|
|
|
window.addEventListener('pageshow', function(event) { |
|
|
if (event.persisted || (window.performance && window.performance.getEntriesByType("navigation")[0].type === 'back_forward')) { |
|
|
if (form && submitButton && spinner) { |
|
|
form.classList.remove('processing'); |
|
|
submitButton.disabled = false; |
|
|
spinner.style.display = 'none'; |
|
|
submitButton.querySelector('i')?.classList.remove('d-none'); |
|
|
} |
|
|
const reportContextData = document.getElementById('report-context-data'); |
|
|
bodyElement.classList.toggle('results-loaded', !!(reportContextData && reportContextData.dataset.report)); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
const chatForm = document.getElementById('chat-form'); |
|
|
const chatInput = document.getElementById('chat-input'); |
|
|
const chatMessages = document.getElementById('chat-messages'); |
|
|
const sendButton = document.getElementById('send-button'); |
|
|
const reportContextData = document.getElementById('report-context-data'); |
|
|
const chatColumn = document.getElementById('chat-column'); |
|
|
const chatPlaceholder = document.getElementById('chat-placeholder'); |
|
|
const exampleQuestionsContainer = document.getElementById('example-questions'); |
|
|
const reportContext = reportContextData ? reportContextData.dataset.report : null; |
|
|
const isChatbotAvailable = {{ chatbot_available | tojson }}; |
|
|
const isChatEnabled = !!reportContext && isChatbotAvailable; |
|
|
|
|
|
if (chatColumn) { |
|
|
const enableChatUI = (enable) => { |
|
|
chatColumn.classList.toggle('disabled', !enable); |
|
|
if (chatInput) chatInput.disabled = !enable; |
|
|
if (sendButton) sendButton.disabled = !enable; |
|
|
if (chatPlaceholder) chatPlaceholder.style.display = enable ? 'none' : 'block'; |
|
|
if (exampleQuestionsContainer) exampleQuestionsContainer.style.display = enable ? 'block' : 'none'; |
|
|
}; |
|
|
enableChatUI(isChatEnabled); |
|
|
if (isChatEnabled && chatMessages) { |
|
|
chatMessages.scrollTop = chatMessages.scrollHeight; |
|
|
} |
|
|
} |
|
|
|
|
|
function addChatMessage(message, sender, isThinking = false) { |
|
|
if (!chatMessages) return null; |
|
|
const messageDiv = document.createElement('div'); |
|
|
messageDiv.classList.add('chat-message', sender === 'user' ? 'user-message' : 'bot-message'); |
|
|
if (isThinking) { |
|
|
messageDiv.classList.add('thinking'); |
|
|
messageDiv.innerHTML = '<span> Thinking...</span>'; |
|
|
} else { |
|
|
const sanitizedMessage = message.replace(/</g, "<").replace(/>/g, ">"); |
|
|
messageDiv.innerHTML = sanitizedMessage; |
|
|
} |
|
|
chatMessages.appendChild(messageDiv); |
|
|
chatMessages.scrollTo({ top: chatMessages.scrollHeight, behavior: 'smooth' }); |
|
|
return messageDiv; |
|
|
} |
|
|
|
|
|
if (chatForm && isChatEnabled) { |
|
|
chatForm.addEventListener('submit', async (e) => { |
|
|
e.preventDefault(); |
|
|
if (!chatInput || !reportContext) return; |
|
|
const question = chatInput.value.trim(); |
|
|
if (!question || chatInput.disabled) return; |
|
|
|
|
|
addChatMessage(question, 'user'); |
|
|
chatInput.value = ''; |
|
|
chatInput.disabled = true; |
|
|
if (sendButton) sendButton.disabled = true; |
|
|
const thinkingMessageElement = addChatMessage('', 'bot', true); |
|
|
|
|
|
try { |
|
|
const response = await fetch("{{ url_for('chat') }}", { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ question: question, report_context: reportContext }), |
|
|
}); |
|
|
if (thinkingMessageElement) thinkingMessageElement.remove(); |
|
|
|
|
|
if (!response.ok) { |
|
|
let errorMsg = `Chat Error: ${response.status || ''} ${response.statusText}`; |
|
|
try { |
|
|
const errorData = await response.json(); |
|
|
errorMsg = `Chat Error: ${errorData.error || response.statusText}`; |
|
|
} catch (e) { } |
|
|
|
|
|
addChatMessage(errorMsg, 'bot').style.color = 'var(--highlight-finding-color)'; |
|
|
} else { |
|
|
const data = await response.json(); |
|
|
addChatMessage(data.answer || "Received empty response.", 'bot'); |
|
|
} |
|
|
} catch (error) { |
|
|
console.error("Chat fetch error:", error); |
|
|
if (thinkingMessageElement) thinkingMessageElement.remove(); |
|
|
addChatMessage('Error connecting to the chat service.', 'bot').style.color = 'var(--highlight-finding-color)'; |
|
|
} finally { |
|
|
if(chatInput) chatInput.disabled = false; |
|
|
if(sendButton) sendButton.disabled = false; |
|
|
if(chatInput) chatInput.focus(); |
|
|
if(chatMessages) chatMessages.scrollTo({ top: chatMessages.scrollHeight, behavior: 'smooth' }); |
|
|
} |
|
|
}); |
|
|
} else if (chatForm) { chatForm.addEventListener('submit', (e) => e.preventDefault()); } |
|
|
|
|
|
if (exampleQuestionsContainer && chatInput && isChatEnabled) { |
|
|
exampleQuestionsContainer.addEventListener('click', (e) => { |
|
|
if (e.target?.classList.contains('suggestion-btn')) { |
|
|
e.preventDefault(); |
|
|
const questionText = e.target.textContent || e.target.innerText; |
|
|
if (questionText && !chatInput.disabled) { chatInput.value = questionText; chatInput.focus(); } |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const imagePreview = document.getElementById('uploaded-image'); |
|
|
const zoomPanel = document.getElementById('zoom-preview-panel'); |
|
|
const imageContainer = document.getElementById('image-result-area'); |
|
|
const zoomSlider = document.getElementById('zoom-slider'); |
|
|
const zoomValueDisplay = document.getElementById('zoom-value'); |
|
|
const zoomControls = document.getElementById('zoom-controls'); |
|
|
|
|
|
if (imagePreview && zoomPanel && imageContainer && zoomSlider && zoomValueDisplay && zoomControls) { |
|
|
let naturalWidth = 0, naturalHeight = 0; |
|
|
let currentZoomLevel = parseFloat(zoomSlider.value); |
|
|
let lastPercX = 0.5, lastPercY = 0.5; |
|
|
let isZoomActive = false; |
|
|
|
|
|
const updateZoomPanelBackground = () => { |
|
|
if (naturalWidth > 0 && isZoomActive) { |
|
|
zoomPanel.style.backgroundSize = `${naturalWidth * currentZoomLevel}px ${naturalHeight * currentZoomLevel}px`; |
|
|
const panelRect = zoomPanel.getBoundingClientRect(); |
|
|
const bgX = -(lastPercX * (naturalWidth * currentZoomLevel - panelRect.width)); |
|
|
const bgY = -(lastPercY * (naturalHeight * currentZoomLevel - panelRect.height)); |
|
|
zoomPanel.style.backgroundPosition = `${bgX}px ${bgY}px`; |
|
|
} |
|
|
}; |
|
|
|
|
|
const updateImageDimensions = () => { |
|
|
naturalWidth = imagePreview.naturalWidth; |
|
|
naturalHeight = imagePreview.naturalHeight; |
|
|
|
|
|
if (naturalWidth === 0 && zoomControls) { |
|
|
zoomControls.style.display = 'none'; |
|
|
} |
|
|
}; |
|
|
|
|
|
if (imagePreview.complete && imagePreview.naturalWidth > 0) { updateImageDimensions(); } |
|
|
else { imagePreview.onload = updateImageDimensions; imagePreview.onerror = updateImageDimensions; } |
|
|
|
|
|
|
|
|
imageContainer.addEventListener('mouseenter', () => { |
|
|
if (naturalWidth > 0 && window.innerWidth >= 992) { |
|
|
isZoomActive = true; |
|
|
zoomPanel.style.backgroundImage = `url('${imagePreview.src}')`; |
|
|
updateZoomPanelBackground(); |
|
|
zoomPanel.style.opacity = '1'; zoomPanel.style.visibility = 'visible'; |
|
|
zoomPanel.style.transition = 'opacity 0.2s ease-out, visibility 0s linear 0s, background-color var(--transition-speed) var(--transition-easing), border-color var(--transition-speed) var(--transition-easing)'; |
|
|
} |
|
|
}); |
|
|
|
|
|
imageContainer.addEventListener('mousemove', (e) => { |
|
|
if (!isZoomActive || naturalWidth === 0) return; |
|
|
const imgRect = imagePreview.getBoundingClientRect(); |
|
|
lastPercX = Math.max(0, Math.min(1, (e.clientX - imgRect.left) / imgRect.width)); |
|
|
lastPercY = Math.max(0, Math.min(1, (e.clientY - imgRect.top) / imgRect.height)); |
|
|
updateZoomPanelBackground(); |
|
|
|
|
|
|
|
|
const containerRect = imageContainer.getBoundingClientRect(); |
|
|
const panelRect = zoomPanel.getBoundingClientRect(); |
|
|
const gap = 15; |
|
|
let panelLeft = 'auto', panelRight = 'auto'; |
|
|
if ((window.innerWidth - containerRect.right - gap) >= panelRect.width) { |
|
|
panelLeft = `${imgRect.right - containerRect.left + gap}px`; |
|
|
} else if ((containerRect.left - gap) >= panelRect.width) { |
|
|
panelRight = `${containerRect.right - imgRect.left + gap}px`; |
|
|
} else { |
|
|
panelLeft = `${imgRect.right - containerRect.left + gap}px`; |
|
|
} |
|
|
zoomPanel.style.left = panelLeft; |
|
|
zoomPanel.style.right = panelRight; |
|
|
zoomPanel.style.top = `${imgRect.top - containerRect.top}px`; |
|
|
}); |
|
|
|
|
|
imageContainer.addEventListener('mouseleave', () => { |
|
|
isZoomActive = false; |
|
|
zoomPanel.style.opacity = '0'; zoomPanel.style.visibility = 'hidden'; |
|
|
zoomPanel.style.transition = 'opacity 0.2s ease-out, visibility 0s linear 0.2s, background-color var(--transition-speed) var(--transition-easing), border-color var(--transition-speed) var(--transition-easing)'; |
|
|
}); |
|
|
|
|
|
zoomSlider.addEventListener('input', () => { |
|
|
currentZoomLevel = parseFloat(zoomSlider.value); |
|
|
zoomValueDisplay.textContent = currentZoomLevel.toFixed(1); |
|
|
updateZoomPanelBackground(); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const notesTextArea = document.getElementById('student-notes-textarea'); |
|
|
const keywordsInput = document.getElementById('student-notes-keywords'); |
|
|
const copyNotesButton = document.getElementById('copy-notes-button'); |
|
|
const copyFeedback = document.getElementById('copy-feedback'); |
|
|
|
|
|
if (copyNotesButton && notesTextArea && keywordsInput && copyFeedback) { |
|
|
copyNotesButton.addEventListener('click', () => { |
|
|
const notes = notesTextArea.value.trim(); |
|
|
const keywords = keywordsInput.value.trim(); |
|
|
let textToCopy = "--- Medical Student Notes ---\n\n"; |
|
|
if (keywords) { textToCopy += `Keywords/Tags:\n${keywords}\n\n`; } |
|
|
if (notes) { textToCopy += `Observations & Learning Points:\n${notes}\n`; } |
|
|
else if (!keywords) { textToCopy += "(No notes or keywords entered)"; } |
|
|
|
|
|
navigator.clipboard.writeText(textToCopy).then(() => { |
|
|
copyFeedback.textContent = 'Copied!'; |
|
|
copyFeedback.classList.remove('text-danger'); |
|
|
copyFeedback.classList.add('text-success'); |
|
|
copyFeedback.style.opacity = '1'; |
|
|
copyNotesButton.disabled = true; |
|
|
copyNotesButton.innerHTML = '<i class="fas fa-check me-1"></i> Copied!'; |
|
|
setTimeout(() => { |
|
|
copyFeedback.style.opacity = '0'; |
|
|
copyNotesButton.disabled = false; |
|
|
copyNotesButton.innerHTML = '<i class="fas fa-copy me-1"></i> Copy Notes & Keywords to Clipboard'; |
|
|
}, 2000); |
|
|
}).catch(err => { |
|
|
console.error('Failed to copy notes: ', err); |
|
|
copyFeedback.textContent = 'Copy Failed!'; |
|
|
copyFeedback.classList.remove('text-success'); |
|
|
copyFeedback.classList.add('text-danger'); |
|
|
copyFeedback.style.opacity = '1'; |
|
|
|
|
|
setTimeout(() => { |
|
|
copyFeedback.style.opacity = '0'; |
|
|
|
|
|
setTimeout(() => { |
|
|
if (copyFeedback.style.opacity === '0') { |
|
|
copyFeedback.textContent = 'Copied!'; |
|
|
copyFeedback.classList.remove('text-danger'); |
|
|
copyFeedback.classList.add('text-success'); |
|
|
} |
|
|
}, 500); |
|
|
}, 2500); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
const resetButton = document.getElementById('reset-button'); |
|
|
if(resetButton) { |
|
|
resetButton.addEventListener('click', () => { window.location.href = "{{ url_for('index') }}"; }); |
|
|
} |
|
|
|
|
|
}); |
|
|
</script> |
|
|
|
|
|
</body> |
|
|
</html> |