CodeBlue-Context / frontend /src /ConsultPanel.jsx
Hackathon Builder
Final Polish: Robust fallback for consultation and improved UI feedback
ecba760
Raw History Blame Contribute Delete
6.34 kB
import { useState } from 'react';
import { MessageSquare, Send, Bot, User, Zap, ShieldAlert, FlaskConical, Stethoscope } from 'lucide-react';
const API_BASE = import.meta.env.VITE_API_BASE || '';
export default function ConsultPanel({ patientId, previousResults }) {
const [messages, setMessages] = useState([
{ role: 'assistant', content: "Hello Doctor. I've synthesized the team's findings. You can now ask us specific clinical questions about this case.", type: 'initial' }
]);
const [input, setInput] = useState('');
const [loading, setLoading] = useState(false);
const sendMessage = async () => {
if (!input.trim() || loading || !patientId) return;
const userMsg = input.trim();
setMessages(prev => [...prev, { role: 'user', content: userMsg }]);
setInput('');
setLoading(true);
try {
const geminiKey = localStorage.getItem('gemini_key');
const headers = { 'Content-Type': 'application/json' };
if (geminiKey) headers['x-gemini-key'] = geminiKey;
const res = await fetch(`${API_BASE}/consult/${patientId}`, {
method: 'POST',
headers,
body: JSON.stringify({
question: userMsg,
previous_results: previousResults
}),
});
const data = await res.json();
const result = data.result;
if (result) {
setMessages(prev => [...prev, {
role: 'assistant',
content: result.answer,
insights: result.specialist_insights,
consensus: result.team_consensus
}]);
} else {
throw new Error('No result');
}
} catch (err) {
setMessages(prev => [...prev, {
role: 'assistant',
content: "I'm having trouble reaching the clinical AI team. Please ensure you've provided a Gemini API Key in the settings (top right).",
insights: [
{"agent": "System", "note": "Connection required for advanced reasoning."}
]
}]);
} finally {
setLoading(false);
}
};
return (
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', background: 'var(--bg-surface)' }}>
<div style={{ padding: '14px 16px', borderBottom: '1px solid var(--border-subtle)', background: 'var(--bg-surface)' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Bot size={18} color="var(--purple-ai)" />
<span style={{ fontWeight: 700, fontSize: 13, color: 'var(--text-primary)' }}>Multi-Agent Consultation</span>
</div>
</div>
<div style={{ flex: 1, overflowY: 'auto', padding: '16px', display: 'flex', flexDirection: 'column', gap: 16 }}>
{messages.map((m, i) => (
<div key={i} style={{
alignSelf: m.role === 'user' ? 'flex-end' : 'flex-start',
maxWidth: '85%',
display: 'flex', gap: 10,
flexDirection: m.role === 'user' ? 'row-reverse' : 'row'
}}>
<div style={{
width: 28, height: 28, borderRadius: '50%',
background: m.role === 'user' ? 'var(--blue-accent)' : 'var(--purple-ai)15',
display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
color: m.role === 'user' ? '#fff' : 'var(--purple-ai)'
}}>
{m.role === 'user' ? <User size={14} /> : <Bot size={14} />}
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
<div style={{
padding: '10px 14px', borderRadius: 12,
background: m.role === 'user' ? 'var(--blue-accent)' : 'var(--bg-base)',
color: m.role === 'user' ? '#fff' : 'var(--text-primary)',
fontSize: 12, lineHeight: 1.5,
boxShadow: m.role === 'user' ? '0 2px 8px rgba(2, 132, 199, 0.2)' : 'none'
}}>
{m.content}
</div>
{m.insights && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 4, marginTop: 4 }}>
{m.insights.map((ins, j) => (
<div key={j} style={{
fontSize: 10, padding: '6px 8px', borderRadius: 6,
background: 'var(--bg-surface)', border: '1px solid var(--border-subtle)',
color: 'var(--text-secondary)', display: 'flex', gap: 6
}}>
<span style={{ fontWeight: 800, color: 'var(--purple-ai)' }}>{ins.agent}:</span>
<span>{ins.note}</span>
</div>
))}
</div>
)}
</div>
</div>
))}
{loading && (
<div style={{ alignSelf: 'flex-start', display: 'flex', gap: 10 }}>
<div className="spinner" style={{ width: 14, height: 14 }} />
<span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Consulting agents...</span>
</div>
)}
</div>
<div style={{ padding: 16, borderTop: '1px solid var(--border-subtle)' }}>
<div style={{ position: 'relative' }}>
<input
type="text"
placeholder="Ask the team a question..."
value={input}
onChange={e => setInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && sendMessage()}
style={{
width: '100%', padding: '12px 45px 12px 14px', borderRadius: 10,
border: '1px solid var(--border-default)', outline: 'none',
fontSize: 12, transition: 'all 0.2s',
background: 'var(--bg-base)'
}}
/>
<button
onClick={sendMessage}
disabled={loading || !input.trim()}
style={{
position: 'absolute', right: 8, top: '50%', transform: 'translateY(-50%)',
background: 'var(--blue-accent)', color: '#fff', border: 'none',
width: 28, height: 28, borderRadius: 6, display: 'flex', alignItems: 'center', justifyContent: 'center',
cursor: 'pointer', opacity: (loading || !input.trim()) ? 0.5 : 1
}}
>
<Send size={14} />
</button>
</div>
</div>
</div>
);
}