BaoThangAI / frontend /src /ClinicalAI.jsx
TrungLC's picture
Initial commit - BaoThangAI
cb75c6a
Raw
History Blame Contribute Delete
7.93 kB
// ══════════════════════════════════════════════════════════════════
// ClinicalAI.jsx — Trang phân tích lâm sàng
// Cách tích hợp vào App.jsx hiện tại:
// 1. Copy toàn bộ component ClinicalAI bên dưới vào App.jsx
// 2. Trong state điều hướng, thêm: const [page, setPage] = useState('chat') // 'chat' | 'clinical'
// 3. Thêm nút "Clinical AI" vào Sidebar
// 4. Render: {page === 'clinical' ? <ClinicalAI /> : <ChatArea />}
// ══════════════════════════════════════════════════════════════════
import React, { useState, useRef } from 'react';
// ── Config ───────────────────────────────────────────────────────────
const API_BASE = import.meta.env.VITE_API_URL || '';
const SECTIONS = [
{ id: 's1', color: '#1D9E75', label: 'Mục 1 — Phân tích ca bệnh & xét nghiệm còn thiếu, chẩn đoán phân biệt' },
{ id: 's2', color: '#378ADD', label: 'Mục 2 — Chẩn đoán xác định & phù hợp phác đồ' },
{ id: 's3', color: '#1D9E75', label: 'Mục 3 — Khuyến cáo điều trị cụ thể' },
{ id: 's4', color: '#7F77DD', label: 'Mục 4 — Nhận xét thuốc đang dùng' },
{ id: 's5', color: '#BA7517', label: 'Mục 5 — Hướng dẫn theo dõi' },
{ id: 's6', color: '#E24B4A', label: 'Mục 6 — Cảnh báo đặc biệt' },
{ id: 's7', color: '#888780', label: 'Mục 7 — Tài liệu tham khảo nội bộ' },
];
// ── Component ────────────────────────────────────────────────────────
export default function ClinicalAI() {
const [caseText, setCaseText] = useState('');
const [loading, setLoading] = useState(false);
const [result, setResult] = useState(null);
const [error, setError] = useState('');
const [collapsed, setCollapsed] = useState({});
const textRef = useRef(null);
const toggleSection = (id) =>
setCollapsed(prev => ({ ...prev, [id]: !prev[id] }));
const analyze = async () => {
if (!caseText.trim() || caseText.trim().length < 20) {
setError('Vui lòng nhập đầy đủ thông tin ca bệnh (tối thiểu 20 ký tự).');
return;
}
setLoading(true);
setError('');
setResult(null);
try {
const res = await fetch(`${API_BASE}/api/v1/clinical-analysis`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ case_text: caseText }),
});
if (!res.ok) {
const err = await res.json().catch(() => ({}));
throw new Error(err.detail || `Lỗi server: ${res.status}`);
}
const data = await res.json();
if (data.success && data.data) {
setResult(data.data);
setCollapsed({});
} else {
throw new Error(data.error || 'Phản hồi không hợp lệ từ server.');
}
} catch (e) {
setError(e.message || 'Không thể kết nối đến server. Vui lòng thử lại.');
} finally {
setLoading(false);
}
};
const clearAll = () => {
setCaseText('');
setResult(null);
setError('');
setCollapsed({});
};
const handleKeyDown = (e) => {
if (e.ctrlKey && e.key === 'Enter') analyze();
};
return (
<div className="clinical-page">
{/* Header */}
<div className="clinical-header">
<div className="clinical-header-icon">
<i className="fa-solid fa-stethoscope" />
</div>
<div>
<h1 className="clinical-title">ClinicalAI — BaoThang</h1>
<p className="clinical-subtitle">Phân tích lâm sàng hỗ trợ ra quyết định · BVĐK KV Bảo Thắng</p>
</div>
</div>
{/* Input */}
<div className="clinical-card">
<label className="clinical-label">Thông tin ca bệnh</label>
<textarea
ref={textRef}
className="clinical-textarea"
value={caseText}
onChange={e => setCaseText(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={`Nhập thông tin bệnh nhân:\n• Tuổi, giới tính, cân nặng\n• Triệu chứng lâm sàng, thời gian khởi phát\n• Kết quả xét nghiệm, chẩn đoán hình ảnh\n• Tiền sử bệnh, dị ứng, chức năng thận/gan\n• Thuốc đang dùng (tên, liều)\n• Chẩn đoán sơ bộ (nếu có)\n\n(Ctrl+Enter để phân tích)`}
rows={10}
disabled={loading}
/>
<div className="clinical-hint">
Thông tin càng đầy đủ, phân tích càng chính xác
</div>
{error && <div className="clinical-error">{error}</div>}
<div className="clinical-actions">
<button
className="btn-clinical-primary"
onClick={analyze}
disabled={loading || !caseText.trim()}
>
{loading
? <><span className="btn-spinner" /> Đang phân tích...</>
: <><i className="fa-solid fa-brain" /> Phân tích ca bệnh</>
}
</button>
<button className="btn-clinical-secondary" onClick={clearAll} disabled={loading}>
Xoá
</button>
{result && (
<button
className="btn-clinical-secondary"
onClick={() => {
const text = SECTIONS
.map(s => `${s.label}\n${'─'.repeat(60)}\n${result[s.id] || ''}\n`)
.join('\n');
const blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `clinical_analysis_${Date.now()}.txt`;
a.click();
URL.revokeObjectURL(url);
}}
>
<i className="fa-solid fa-download" /> Tải xuống
</button>
)}
</div>
</div>
{/* Loading bar */}
{loading && (
<div className="clinical-loading-track">
<div className="clinical-loading-fill" />
</div>
)}
{/* Results */}
{result && (
<div className="clinical-results">
{SECTIONS.map(s => {
const content = result[s.id];
if (!content) return null;
const open = !collapsed[s.id];
return (
<div key={s.id} className="clinical-section">
<button
className="clinical-section-header"
onClick={() => toggleSection(s.id)}
aria-expanded={open}
>
<span className="clinical-dot" style={{ background: s.color }} />
<span className="clinical-section-title">{s.label}</span>
<i className={`fa-solid fa-chevron-down clinical-chevron ${open ? 'open' : ''}`} />
</button>
{open && (
<div className="clinical-section-body">
{content}
</div>
)}
</div>
);
})}
<p className="clinical-disclaimer">
<i className="fa-solid fa-shield-halved" /> Kết quả phân tích mang tính hỗ trợ quyết định lâm sàng.
Bác sĩ điều trị chịu trách nhiệm quyết định cuối cùng.
</p>
</div>
)}
</div>
);
}