Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 44 additions & 10 deletions frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ function App() {
const [apiKey, setApiKey] = useState('');
const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(false);
const [loadingPhase, setLoadingPhase] = useState(null);
const [result, setResult] = useState(null);
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [darkMode, setDarkMode] = useState(() => {
Expand All @@ -35,16 +36,17 @@ function App() {
const handleSubmit = async (fhirData = {}) => {
if ((!query.trim() && !fhirData.has_fhir) || loading) return;

// Close the sidebar automatically on mobile when a query is sent
closeSidebar();

const userMessage = {
role: 'user',
text: query || `FHIR ${fhirData.fhir_resource_type}/${fhirData.fhir_resource_id}`
};

setMessages(prev => [...prev, userMessage]);
setQuery('');
setLoading(true);
setLoadingPhase(null);
setResult(null);

try {
Expand All @@ -55,12 +57,42 @@ function App() {
});

if (!response.ok) throw new Error('Query failed');
const data = await response.json();
console.log('full response:', data);
console.log('final_response:', data.final_response);
const final = data.final_response;
setResult(final);
setMessages(prev => [...prev, { role: 'assistant', data: final }]);

const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");

let buffer = "";

while (true) {
const { done, value } = await reader.read();
if (done) break;

buffer += decoder.decode(value, { stream: true });

const parts = buffer.split('\n\n');

buffer = parts.pop();

for (const part of parts) {
const line = part.trim();
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.substring(6));

if (data.status) {
setLoadingPhase(data.status);
}

if (data.final_response) {
setResult(data.final_response);
setMessages(prev => [...prev, { role: 'assistant', data: data.final_response }]);
}
} catch (parseError) {
console.error("JSON Parse Error on complete chunk:", line, parseError);
}
}
}
}
} catch (err) {
let errorText = 'Failed to connect to the analysis server. Ensure the backend is running.';

Expand All @@ -78,6 +110,7 @@ function App() {
}]);
} finally {
setLoading(false);
setLoadingPhase(null);
}
};

Expand All @@ -99,8 +132,8 @@ function App() {
</div>

<div>
<button onClick={toggleTheme} style={{ background: 'none', border: 'none', outline: 'none', color: 'inherit', cursor: 'pointer' }}>
{darkMode ? <Sun size={20} /> : <Moon size={20} />}
<button onClick={toggleTheme} style={{ background: 'none', border: 'none', outline: 'none', color: 'inherit' }}>
{darkMode ? <Sun /> : <Moon />}
</button>
</div>
</header>
Expand All @@ -120,10 +153,11 @@ function App() {
onQueryChange={setQuery}
onSubmit={handleSubmit}
loading={loading}
loadingPhase={loadingPhase}
/>
</div>
</div>
);
}

export default App;
export default App;
51 changes: 34 additions & 17 deletions frontend/src/components/ChatWindow.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,33 @@ const FHIR_RESOURCE_TYPES = ['Condition', 'MedicationRequest', 'DiagnosticReport

const SAMPLE_PLACEHOLDERS = [
'e.g. What are the evidence-based treatments for heart failure?',
'e.g. Are there any contraindications for prescribing Tylenol?',
'e.g. Are there any contraindications for prescribing Metoprolol?',
'e.g. Summarize the latest clinical guidelines for managing Type 2 Diabetes.',
'e.g. What is the NNT for statins in primary prevention?'
'e.g. What is the primary reason to prescribe statins?'
];

function ChatWindow({ messages, query, onQueryChange, onSubmit, loading }) {
const NODE_MESSAGES = {
fhir_input: "Connecting to HAPI FHIR server...",
preprocess_query: "Analyzing query intent...",
pubmed_retrieval: "Querying PubMed abstracts...",
detect_medications: "Scanning for medication entities...",
fda_enrichment: "Retrieving openFDA medication data...",
llm_generation: "Drafting initial clinical response...",
parse_claims: "Extracting testable claims...",
nli_scoring: "Running NLI verification model...",
confidence_scoring: "Calculating overall reliability...",
assembly: "Finalizing evidence-based response..."
};

function ChatWindow({ messages, query, onQueryChange, onSubmit, loading, loadingPhase }) {
const bottomRef = useRef(null);
const textareaRef = useRef(null);
const [showFhir, setShowFhir] = useState(false);
const [fhirType, setFhirType] = useState('Condition');
const [fhirId, setFhirId] = useState('');

// Track which placeholder is currently active
const [placeholderIndex, setPlaceholderIndex] = useState(0);

// Swap the placeholder every 5 seconds
useEffect(() => {
// Stop the carousel if a message has already been sent
if (messages.length > 0) return;

const timer = setInterval(() => {
Expand Down Expand Up @@ -71,7 +80,6 @@ function ChatWindow({ messages, query, onQueryChange, onSubmit, loading }) {

const canSubmit = query.trim() || (showFhir && fhirId.trim());

// Determine what the current placeholder should be
const currentPlaceholder = showFhir
? 'Optional: add a specific question about this resource…'
: messages.length > 0
Expand Down Expand Up @@ -137,10 +145,21 @@ function ChatWindow({ messages, query, onQueryChange, onSubmit, loading }) {

{loading && (
<div className="message-assistant">
<div className="typing-indicator">
<div className="typing-dot" />
<div className="typing-dot" />
<div className="typing-dot" />
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<div className="typing-indicator">
<div className="typing-dot" />
<div className="typing-dot" />
<div className="typing-dot" />
</div>
<div style={{
fontFamily: 'DM Mono, monospace',
fontSize: 11,
color: 'var(--text-3)',
paddingLeft: 4,
animation: 'fadeIn 0.5s ease-in'
}}>
{NODE_MESSAGES[loadingPhase] || "Initializing analysis server..."}
</div>
</div>
</div>
)}
Expand All @@ -159,8 +178,6 @@ function ChatWindow({ messages, query, onQueryChange, onSubmit, loading }) {
· HAPI R4 Server
</span>
</div>

{/* Added flexWrap: 'wrap' here so the inputs stack cleanly on small mobile screens */}
<div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
<select
value={fhirType}
Expand All @@ -175,8 +192,8 @@ function ChatWindow({ messages, query, onQueryChange, onSubmit, loading }) {
padding: '6px 10px',
outline: 'none',
cursor: 'pointer',
flex: '1 1 auto', /* Allows the select to shrink/grow gracefully */
minWidth: '140px', /* Ensures it doesn't get too small before wrapping */
flex: '1 1 auto',
minWidth: '140px',
}}
>
{FHIR_RESOURCE_TYPES.map(t => (
Expand All @@ -189,7 +206,7 @@ function ChatWindow({ messages, query, onQueryChange, onSubmit, loading }) {
value={fhirId}
onChange={(e) => setFhirId(e.target.value)}
style={{
flex: '2 1 auto', /* Allows input to take up remaining space */
flex: '2 1 auto',
minWidth: '150px',
background: 'var(--bg)',
border: '1px solid var(--border)',
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/components/Sidebar.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';

function Sidebar({ apiKey, onApiKeyChange, result }) {
const score = result ? Math.round(result.confidence_score * 100) : null;
const score = result?.confidence_score ? Math.round(result.confidence_score * 100) : 0;

const getScoreColor = (pct) => {
if (pct >= 70) return '#16a34a';
Expand All @@ -15,9 +15,9 @@ function Sidebar({ apiKey, onApiKeyChange, result }) {
return 'Low reliability — review carefully';
};

const supported = result ? result.scored_claims.filter(c => c.label === 'Supported').length : 0;
const contradicted = result ? result.scored_claims.filter(c => c.label === 'Contradicted').length : 0;
const unverifiable = result ? result.scored_claims.filter(c => c.label === 'Unverifiable').length : 0;
const supported = (result?.scored_claims || []).filter(c => c.label === 'Supported').length;
const contradicted = (result?.scored_claims || []).filter(c => c.label === 'Contradicted').length;
const unverifiable = (result?.scored_claims || []).filter(c => c.label === 'Unverifiable').length;

return (
<aside className="sidebar">
Expand Down Expand Up @@ -79,7 +79,7 @@ function Sidebar({ apiKey, onApiKeyChange, result }) {

<div className="sidebar-section">
<div className="sidebar-label">Sources</div>
{result.abstracts.slice(0, 5).map((a, i) => (
{(result?.abstracts || []).slice(0, 5).map((a, i) => (
<div key={i} style={{ marginBottom: 10 }}>
<div style={{ fontSize: 12, color: 'var(--text)', lineHeight: 1.4, marginBottom: 2 }}>
{a.title}
Expand All @@ -105,4 +105,4 @@ function Sidebar({ apiKey, onApiKeyChange, result }) {
);
}

export default Sidebar;
export default Sidebar;
Loading
Loading