Spaces:
Sleeping
Sleeping
| // ══════════════════════════════════════════════════════════════════ | |
| // 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> | |
| ); | |
| } | |