import { useState, useRef } from "react";
const TABS = ["Contexto Clínico", "Subir PDF", "Análisis"];
const defaultClinical = {
name: "", age: "", sex: "masculino", height: "", weight: "",
chiefComplaint: "", diagnosis: "", injuryHistory: "",
footwearCondition: "descalzo", shoeModel: "", orthoticDetails: "",
additionalNotes: ""
};
const defaultBalance = {
eo_c90area: "", eo_traceLength: "", eo_stdVelocity: "", eo_velocity: "",
eo_c90angle: "", eo_stdX: "", eo_stdY: "", eo_wdLeft: "", eo_wdRight: "",
ec_c90area: "", ec_traceLength: "", ec_stdVelocity: "", ec_velocity: "",
ec_c90angle: "", ec_stdX: "", ec_stdY: "", ec_wdLeft: "", ec_wdRight: ""
};
const defaultUnipodal = {
right_eo_c90area: "", right_eo_velocity: "", right_eo_traceLength: "", right_eo_stdX: "", right_eo_stdY: "",
right_ec_c90area: "", right_ec_velocity: "", right_ec_traceLength: "", right_ec_stdX: "", right_ec_stdY: "",
left_eo_c90area: "", left_eo_velocity: "", left_eo_traceLength: "", left_eo_stdX: "", left_eo_stdY: "",
left_ec_c90area: "", left_ec_velocity: "", left_ec_traceLength: "", left_ec_stdX: "", left_ec_stdY: ""
};
const defaultLoS = {
forward_achieved: "", forward_ref: "7.00",
rearward_achieved: "", rearward_ref: "5.00",
leftward_achieved: "", leftward_ref: "8.00",
rightward_achieved: "", rightward_ref: "8.00"
};
function Input({ label, value, onChange, type="text", placeholder="" }) {
return (
<div style={{ marginBottom:10 }}>
<label style={{ display:"block", fontSize:13, fontWeight:600, color:"#334155", marginBottom:3 }}>{label}</label>
<input type={type} value={value} onChange={e=>onChange(e.target.value)} placeholder={placeholder}
style={{ width:"100%", padding:"8px 10px", border:"1px solid #cbd5e1", borderRadius:6, fontSize:14, boxSizing:"border-box", background:"#f8fafc" }} />
</div>
);
}
function TextArea({ label, value, onChange, rows=3, placeholder="" }) {
return (
<div style={{ marginBottom:10 }}>
<label style={{ display:"block", fontSize:13, fontWeight:600, color:"#334155", marginBottom:3 }}>{label}</label>
<textarea value={value} onChange={e=>onChange(e.target.value)} rows={rows} placeholder={placeholder}
style={{ width:"100%", padding:"8px 10px", border:"1px solid #cbd5e1", borderRadius:6, fontSize:14, boxSizing:"border-box", resize:"vertical", background:"#f8fafc" }} />
</div>
);
}
function Select({ label, value, onChange, options }) {
return (
<div style={{ marginBottom:10 }}>
<label style={{ display:"block", fontSize:13, fontWeight:600, color:"#334155", marginBottom:3 }}>{label}</label>
<select value={value} onChange={e=>onChange(e.target.value)}
style={{ width:"100%", padding:"8px 10px", border:"1px solid #cbd5e1", borderRadius:6, fontSize:14, background:"#f8fafc" }}>
{options.map(o=><option key={o.value} value={o.value}>{o.label}</option>)}
</select>
</div>
);
}
function Section({ title, children }) {
return (
<div style={{ background:"#fff", border:"1px solid #e2e8f0", borderRadius:10, padding:18, marginBottom:16 }}>
<h3 style={{ margin:"0 0 12px", fontSize:15, color:"#1e40af", borderBottom:"2px solid #dbeafe", paddingBottom:8 }}>{title}</h3>
{children}
</div>
);
}
function getRiskColor(l) {
if(l==="alto") return {bg:"#fef2f2",border:"#fca5a5",text:"#991b1b",badge:"#ef4444"};
if(l==="medio") return {bg:"#fffbeb",border:"#fcd34d",text:"#92400e",badge:"#f59e0b"};
return {bg:"#f0fdf4",border:"#86efac",text:"#166534",badge:"#22c55e"};
}
function RiskBadge({level,label}){const c=getRiskColor(level);return <span style={{display:"inline-block",background:c.badge,color:"#fff",fontSize:11,fontWeight:700,padding:"2px 8px",borderRadius:10,marginLeft:6}}>{label||(level==="alto"?"ALTO":level==="medio"?"MEDIO":"BAJO")}</span>;}
function AnalysisCard({title,risk,children}){const c=getRiskColor(risk);return(<div style={{background:c.bg,border:`1px solid ${c.border}`,borderRadius:10,padding:14,marginBottom:12}}><div style={{fontWeight:700,fontSize:14,color:c.text,marginBottom:6}}>{title} <RiskBadge level={risk}/></div><div style={{fontSize:13,color:"#334155",lineHeight:1.6}}>{children}</div></div>);}
const th={padding:"6px 8px",textAlign:"left",fontSize:11,fontWeight:700};
const td={padding:"5px 8px",borderTop:"1px solid #e2e8f0"};
function DataPreview({balance,los,unipodal}){
const hasB=balance.eo_c90area||balance.ec_c90area;
const hasL=los.forward_achieved||los.rearward_achieved||los.leftward_achieved||los.rightward_achieved;
const hasU=unipodal.right_eo_c90area||unipodal.left_eo_c90area||unipodal.right_ec_c90area||unipodal.left_ec_c90area;
if(!hasB&&!hasL&&!hasU) return null;
return(
<div style={{marginTop:16}}>
{hasB&&(
<div style={{background:"#f8fafc",border:"1px solid #e2e8f0",borderRadius:8,padding:12,marginBottom:10,overflowX:"auto"}}>
<div style={{fontSize:13,fontWeight:700,color:"#1e3a5f",marginBottom:8}}>Datos Romberg Bipodal</div>
<table style={{width:"100%",fontSize:11,borderCollapse:"collapse"}}>
<thead><tr style={{background:"#e2e8f0"}}><th style={th}></th><th style={th}>C90 Area</th><th style={th}>Trace L.</th><th style={th}>STD Vel.</th><th style={th}>Vel.</th><th style={th}>STD X</th><th style={th}>STD Y</th><th style={th}>WD-L</th><th style={th}>WD-R</th></tr></thead>
<tbody>
<tr><td style={td}><strong>OA</strong></td><td style={td}>{balance.eo_c90area||"—"}</td><td style={td}>{balance.eo_traceLength||"—"}</td><td style={td}>{balance.eo_stdVelocity||"—"}</td><td style={td}>{balance.eo_velocity||"—"}</td><td style={td}>{balance.eo_stdX||"—"}</td><td style={td}>{balance.eo_stdY||"—"}</td><td style={td}>{balance.eo_wdLeft||"—"}</td><td style={td}>{balance.eo_wdRight||"—"}</td></tr>
<tr style={{background:"#f1f5f9"}}><td style={td}><strong>OC</strong></td><td style={td}>{balance.ec_c90area||"—"}</td><td style={td}>{balance.ec_traceLength||"—"}</td><td style={td}>{balance.ec_stdVelocity||"—"}</td><td style={td}>{balance.ec_velocity||"—"}</td><td style={td}>{balance.ec_stdX||"—"}</td><td style={td}>{balance.ec_stdY||"—"}</td><td style={td}>{balance.ec_wdLeft||"—"}</td><td style={td}>{balance.ec_wdRight||"—"}</td></tr>
</tbody>
</table>
</div>
)}
{hasU&&(
<div style={{background:"#f8fafc",border:"1px solid #e2e8f0",borderRadius:8,padding:12,marginBottom:10,overflowX:"auto"}}>
<div style={{fontSize:13,fontWeight:700,color:"#1e3a5f",marginBottom:8}}>Datos Unipodal</div>
<table style={{width:"100%",fontSize:11,borderCollapse:"collapse"}}>
<thead><tr style={{background:"#e2e8f0"}}><th style={th}>Apoyo</th><th style={th}>Cond.</th><th style={th}>C90 Area</th><th style={th}>Vel.</th><th style={th}>Trace L.</th><th style={th}>STD X</th><th style={th}>STD Y</th></tr></thead>
<tbody>
<tr><td style={td}><strong>Derecho</strong></td><td style={td}>OA</td><td style={td}>{unipodal.right_eo_c90area||"—"}</td><td style={td}>{unipodal.right_eo_velocity||"—"}</td><td style={td}>{unipodal.right_eo_traceLength||"—"}</td><td style={td}>{unipodal.right_eo_stdX||"—"}</td><td style={td}>{unipodal.right_eo_stdY||"—"}</td></tr>
<tr style={{background:"#f1f5f9"}}><td style={td}><strong>Derecho</strong></td><td style={td}>OC</td><td style={td}>{unipodal.right_ec_c90area||"—"}</td><td style={td}>{unipodal.right_ec_velocity||"—"}</td><td style={td}>{unipodal.right_ec_traceLength||"—"}</td><td style={td}>{unipodal.right_ec_stdX||"—"}</td><td style={td}>{unipodal.right_ec_stdY||"—"}</td></tr>
<tr><td style={td}><strong>Izquierdo</strong></td><td style={td}>OA</td><td style={td}>{unipodal.left_eo_c90area||"—"}</td><td style={td}>{unipodal.left_eo_velocity||"—"}</td><td style={td}>{unipodal.left_eo_traceLength||"—"}</td><td style={td}>{unipodal.left_eo_stdX||"—"}</td><td style={td}>{unipodal.left_eo_stdY||"—"}</td></tr>
<tr style={{background:"#f1f5f9"}}><td style={td}><strong>Izquierdo</strong></td><td style={td}>OC</td><td style={td}>{unipodal.left_ec_c90area||"—"}</td><td style={td}>{unipodal.left_ec_velocity||"—"}</td><td style={td}>{unipodal.left_ec_traceLength||"—"}</td><td style={td}>{unipodal.left_ec_stdX||"—"}</td><td style={td}>{unipodal.left_ec_stdY||"—"}</td></tr>
</tbody>
</table>
</div>
)}
{hasL&&(
<div style={{background:"#f8fafc",border:"1px solid #e2e8f0",borderRadius:8,padding:12}}>
<div style={{fontSize:13,fontWeight:700,color:"#1e3a5f",marginBottom:8}}>Datos LoS</div>
<table style={{width:"100%",fontSize:11,borderCollapse:"collapse"}}>
<thead><tr style={{background:"#e2e8f0"}}><th style={th}>Dirección</th><th style={th}>Alcanzado (°)</th><th style={th}>Referencia (°)</th></tr></thead>
<tbody>
{[{k:"forward",l:"Anterior"},{k:"rearward",l:"Posterior"},{k:"leftward",l:"Izquierda"},{k:"rightward",l:"Derecha"}].map((d,i)=>(
<tr key={d.k} style={i%2?{background:"#f1f5f9"}:{}}><td style={td}><strong>{d.l}</strong></td><td style={td}>{los[`${d.k}_achieved`]||"—"}</td><td style={td}>{los[`${d.k}_ref`]||"—"}</td></tr>
))}
</tbody>
</table>
</div>
)}
</div>
);
}
function calcAsymmetry(valA, valB) {
const a = parseFloat(valA), b = parseFloat(valB);
if (!a && !b) return null;
if (!a || !b) return null;
const max = Math.max(a, b), min = Math.min(a, b);
if (max === 0) return 0;
return ((max - min) / max * 100);
}
function calcAsymData(balance, unipodal) {
const p = v => parseFloat(v) || 0;
const res = { bipodal: {}, unipodal: {}, hasBipodal: false, hasUnipodal: false };
// Bipodal WD asymmetry
const eoL = p(balance.eo_wdLeft), eoR = p(balance.eo_wdRight);
const ecL = p(balance.ec_wdLeft), ecR = p(balance.ec_wdRight);
if (eoL && eoR) {
res.bipodal.eo = { left: eoL, right: eoR, asym: Math.abs(eoL - eoR), dominant: eoL > eoR ? "Izquierdo" : "Derecho" };
res.hasBipodal = true;
}
if (ecL && ecR) {
res.bipodal.ec = { left: ecL, right: ecR, asym: Math.abs(ecL - ecR), dominant: ecL > ecR ? "Izquierdo" : "Derecho" };
res.hasBipodal = true;
}
if (res.bipodal.eo && res.bipodal.ec) {
res.bipodal.visualDependency = Math.abs(res.bipodal.ec.asym - res.bipodal.eo.asym).toFixed(1);
res.bipodal.worsenEC = res.bipodal.ec.asym > res.bipodal.eo.asym;
}
// Unipodal asymmetry by C90 Area
const reo = p(unipodal.right_eo_c90area), leo = p(unipodal.left_eo_c90area);
const rec = p(unipodal.right_ec_c90area), lec = p(unipodal.left_ec_c90area);
if (reo && leo) {
const asym = calcAsymmetry(reo, leo);
res.unipodal.eo_area = { right: reo, left: leo, asym: asym !== null ? asym.toFixed(1) : null, better: reo < leo ? "Derecho" : "Izquierdo" };
res.hasUnipodal = true;
}
if (rec && lec) {
const asym = calcAsymmetry(rec, lec);
res.unipodal.ec_area = { right: rec, left: lec, asym: asym !== null ? asym.toFixed(1) : null, better: rec < lec ? "Derecho" : "Izquierdo" };
res.hasUnipodal = true;
}
// Unipodal visual dependency per leg
if (reo && rec) {
res.unipodal.right_rq = ((rec / reo) * 100).toFixed(0);
}
if (leo && lec) {
res.unipodal.left_rq = ((lec / leo) * 100).toFixed(0);
}
// Unipodal velocity asymmetry
const rveo = p(unipodal.right_eo_velocity), lveo = p(unipodal.left_eo_velocity);
const rvec = p(unipodal.right_ec_velocity), lvec = p(unipodal.left_ec_velocity);
if (rveo && lveo) {
const asym = calcAsymmetry(rveo, lveo);
res.unipodal.eo_vel = { right: rveo, left: lveo, asym: asym !== null ? asym.toFixed(1) : null, better: rveo < lveo ? "Derecho" : "Izquierdo" };
res.hasUnipodal = true;
}
if (rvec && lvec) {
const asym = calcAsymmetry(rvec, lvec);
res.unipodal.ec_vel = { right: rvec, left: lvec, asym: asym !== null ? asym.toFixed(1) : null, better: rvec < lvec ? "Derecho" : "Izquierdo" };
res.hasUnipodal = true;
}
return res;
}
function generateAnalysis(clinical, balance, los, unipodal) {
const results = [];
const p = v => parseFloat(v);
const hasBalance = balance.eo_c90area && balance.ec_c90area;
const hasLoS = los.forward_achieved || los.rearward_achieved;
const hasUni = unipodal.right_eo_c90area || unipodal.left_eo_c90area;
let contextNote = "";
if (clinical.footwearCondition==="descalzo") contextNote = "Evaluación realizada descalzo, valoración directa del control postural sin influencia de calzado.";
else if (clinical.footwearCondition==="zapatilla") contextNote = `Evaluación con zapatilla (${clinical.shoeModel||"N/E"}). Considerar influencia del calzado en propiocepción.`;
else if (clinical.footwearCondition==="plantillas") contextNote = `Evaluación con plantillas ortopédicas (${clinical.orthoticDetails||"N/E"}). Resultados reflejan efecto de corrección ortésica.`;
if (!hasBalance && !hasLoS && !hasUni) {
return { contextNote, results:[{title:"Sin datos",risk:"medio",text:"Sube un PDF con resultados del iBalance Premium."}], rq:null, summary:"", asymData:null };
}
let rq = null;
if (hasBalance) {
const eoA=p(balance.eo_c90area), ecA=p(balance.ec_c90area);
rq = ecA&&eoA ? Math.round((ecA/eoA)*100) : null;
let eoR=eoA>600?"alto":eoA>350?"medio":"bajo";
results.push({title:"Área C90 — Ojos Abiertos",risk:eoR,text:`${eoA} mm². ${eoR==="alto"?"Valores elevados sugieren mayor oscilación y potencial riesgo de caída.":eoR==="medio"?"Rango intermedio, se recomienda seguimiento.":"Rangos esperados para adecuado control postural estático."}`});
let ecR=ecA>800?"alto":ecA>500?"medio":"bajo";
results.push({title:"Área C90 — Ojos Cerrados",risk:ecR,text:`${ecA} mm². ${ecR==="alto"?"Dependencia importante del input visual para el control postural.":"Incremento dentro de márgenes aceptables."}`});
if (rq!==null) {
let rqR=rq>200?"alto":rq>150?"medio":"bajo";
results.push({title:"Cociente de Romberg (RQ)",risk:rqR,text:`RQ = ${rq}%. ${rqR==="alto"?"Visión preponderante, sugiere déficit propioceptivo o vestibular.":rqR==="medio"?"Influencia visual relevante pero no predominante.":"Contribución multisensorial adecuada."}`});
}
const eoV=p(balance.eo_velocity), ecV=p(balance.ec_velocity);
if (eoV||ecV) {
const mx=Math.max(eoV||0,ecV||0);
let vR=mx>25?"alto":mx>15?"medio":"bajo";
results.push({title:"Velocidad de Oscilación",risk:vR,text:`OA: ${eoV||"N/D"} mm/s | OC: ${ecV||"N/D"} mm/s. ${vR==="alto"?"Velocidades elevadas asociadas a mayor riesgo de caída (Fernie et al., 1982).":"Rangos funcionales."}`});
}
const wL=p(balance.eo_wdLeft), wR=p(balance.eo_wdRight);
if (wL&&wR) {
const as=Math.abs(wL-wR);
let wRisk=as>10?"alto":as>5?"medio":"bajo";
results.push({title:"Distribución de Peso Bipodal",risk:wRisk,text:`Izq: ${wL}% | Der: ${wR}% (Asimetría: ${as.toFixed(1)}%). ${wRisk==="alto"?"Asimetría significativa en distribución de carga.":wRisk==="medio"?"Asimetría leve-moderada.":"Distribución simétrica y funcional."}`});
}
const eY=p(balance.eo_stdY), cY=p(balance.ec_stdY);
if (eY||cY) {
const mx=Math.max(eY||0,cY||0);
let aR=mx>12?"alto":mx>8?"medio":"bajo";
results.push({title:"Oscilación AP (STD Y)",risk:aR,text:`OA: ${eY||"N/D"} mm | OC: ${cY||"N/D"} mm. ${aR==="alto"?"Dificultad en estrategia de tobillo.":"Parámetros funcionales."}`});
}
const eX=p(balance.eo_stdX), cX=p(balance.ec_stdX);
if (eX||cX) {
const mx=Math.max(eX||0,cX||0);
let mR=mx>10?"alto":mx>6?"medio":"bajo";
results.push({title:"Oscilación ML (STD X)",risk:mR,text:`OA: ${eX||"N/D"} mm | OC: ${cX||"N/D"} mm. ${mR==="alto"?"Posible debilidad de abductores o dificultad en estrategia de cadera.":"Parámetros funcionales."}`});
}
}
// Unipodal
if (hasUni) {
const reo=p(unipodal.right_eo_c90area), leo=p(unipodal.left_eo_c90area);
const rec=p(unipodal.right_ec_c90area), lec=p(unipodal.left_ec_c90area);
if (reo&&leo) {
const asym=calcAsymmetry(reo,leo);
const better=reo<leo?"derecho":"izquierdo";
let uR=(asym||0)>30?"alto":(asym||0)>15?"medio":"bajo";
results.push({title:"Unipodal OA — Asimetría C90 Area",risk:uR,text:`Der: ${reo} mm² | Izq: ${leo} mm² (Asimetría: ${asym!==null?asym.toFixed(1):"N/D"}%). Mejor control en apoyo ${better}. ${uR==="alto"?"Asimetría significativa entre apoyos con ojos abiertos.":"Asimetría dentro de rangos aceptables."}`});
}
if (rec&&lec) {
const asym=calcAsymmetry(rec,lec);
const better=rec<lec?"derecho":"izquierdo";
let uR=(asym||0)>30?"alto":(asym||0)>15?"medio":"bajo";
results.push({title:"Unipodal OC — Asimetría C90 Area",risk:uR,text:`Der: ${rec} mm² | Izq: ${lec} mm² (Asimetría: ${asym!==null?asym.toFixed(1):"N/D"}%). Mejor control en apoyo ${better}. ${uR==="alto"?"Asimetría significativa con ojos cerrados, sugiere déficit propioceptivo lateralizado.":"Asimetría aceptable."}`});
}
if (reo&&rec) {
const rRQ=((rec/reo)*100).toFixed(0);
let rR=rRQ>250?"alto":rRQ>150?"medio":"bajo";
results.push({title:"Dependencia Visual — Apoyo Derecho",risk:rR,text:`RQ unipodal derecho: ${rRQ}%. ${rR==="alto"?"Alta dependencia visual en apoyo derecho.":"Dependencia visual en rango funcional."}`});
}
if (leo&&lec) {
const lRQ=((lec/leo)*100).toFixed(0);
let lR=lRQ>250?"alto":lRQ>150?"medio":"bajo";
results.push({title:"Dependencia Visual — Apoyo Izquierdo",risk:lR,text:`RQ unipodal izquierdo: ${lRQ}%. ${lR==="alto"?"Alta dependencia visual en apoyo izquierdo.":"Dependencia visual en rango funcional."}`});
}
}
// LoS
if (hasLoS) {
const dirs=[{key:"forward",label:"Anterior",ref:p(los.forward_ref)},{key:"rearward",label:"Posterior",ref:p(los.rearward_ref)},{key:"leftward",label:"Izquierda",ref:p(los.leftward_ref)},{key:"rightward",label:"Derecha",ref:p(los.rightward_ref)}];
let weakest=null,weakestDiff=0;
const lr=[];
dirs.forEach(d=>{const a=p(los[`${d.key}_achieved`]);if(!a&&a!==0)return;const diff=d.ref-a;const pct=((a/d.ref)*100).toFixed(0);lr.push({...d,achieved:a,diff,pct,met:a>=d.ref});if(diff>weakestDiff){weakestDiff=diff;weakest=d.label;}});
if (lr.length>0) {
const unmet=lr.filter(r=>!r.met);
let lR=unmet.length>=3?"alto":unmet.length>=1?"medio":"bajo";
const det=lr.map(r=>`${r.label}: ${r.achieved}°/${r.ref}° (${r.pct}%) ${r.met?"✓":"✗"}`).join(" | ");
results.push({title:"Límites de Estabilidad (LoS)",risk:lR,text:`${det}. ${weakest?`Dirección más débil: ${weakest}.`:""}`});
}
}
const highR=results.filter(r=>r.risk==="alto").length, medR=results.filter(r=>r.risk==="medio").length;
let overallRisk=highR>=2?"alto":(highR>=1||medR>=2)?"medio":"bajo";
// Asymmetry data
const asymData = calcAsymData(balance, unipodal);
// Summary — lenguaje accesible para cualquier lector
let summary = "";
// Intro con contexto clínico
if (clinical.chiefComplaint || clinical.diagnosis) {
summary += `El paciente consultó ${clinical.chiefComplaint ? `por "${clinical.chiefComplaint}"` : ""}`;
if (clinical.diagnosis) summary += `${clinical.chiefComplaint ? ", con " : "con "}un diagnóstico de "${clinical.diagnosis}"`;
summary += ". ";
}
// Resultado general
summary += `Se realizó una evaluación del equilibrio utilizando una plataforma de fuerza, que mide cómo el cuerpo se balancea al estar de pie. `;
if (overallRisk === "alto") {
summary += `Los resultados muestran un nivel de riesgo ALTO, lo que significa que se encontraron varias alteraciones importantes en la capacidad de mantener el equilibrio. `;
} else if (overallRisk === "medio") {
summary += `Los resultados muestran un nivel de riesgo MODERADO, lo que significa que se encontraron algunas alteraciones que merecen atención, aunque no representan un riesgo severo en este momento. `;
} else {
summary += `Los resultados muestran un nivel de riesgo BAJO, lo que significa que el control del equilibrio se encuentra dentro de valores normales. `;
}
// RQ explicado
if (rq !== null) {
summary += `\n\nSe evaluó también qué tanto depende la persona de su visión para mantenerse estable. `;
if (rq > 200) {
summary += `Se encontró que al cerrar los ojos el balanceo aumenta significativamente (${rq}% respecto a ojos abiertos), lo que indica que la persona depende mucho de la vista para mantener el equilibrio. Esto puede significar que los sensores internos del cuerpo (sensibilidad en los pies, articulaciones y oído interno) no están funcionando de manera óptima. `;
} else if (rq > 150) {
summary += `Al cerrar los ojos el balanceo aumenta de forma moderada (${rq}% respecto a ojos abiertos). La visión juega un papel importante en el equilibrio, aunque los sensores internos del cuerpo aún contribuyen de manera relevante. `;
} else {
summary += `Al cerrar los ojos el balanceo no aumenta de manera preocupante (${rq}% respecto a ojos abiertos), lo que indica que el cuerpo utiliza adecuadamente tanto la vista como sus sensores internos (sensibilidad en pies, articulaciones y oído interno) para mantener el equilibrio. `;
}
}
// Bipodal asymmetry explicada
if (asymData.hasBipodal) {
summary += `\n\nRespecto al reparto de peso entre ambos pies al estar de pie: `;
if (asymData.bipodal.eo) {
const a = asymData.bipodal.eo;
if (a.asym > 10) {
summary += `Con los ojos abiertos, se observó una diferencia importante del ${a.asym.toFixed(1)}% en la distribución de peso, cargando más sobre el lado ${a.dominant.toLowerCase()}. Esto significa que la persona no reparte su peso de manera equilibrada entre ambos pies. `;
} else if (a.asym > 5) {
summary += `Con los ojos abiertos, existe una leve diferencia del ${a.asym.toFixed(1)}% en la distribución de peso, con ligero predominio hacia el lado ${a.dominant.toLowerCase()}. `;
} else {
summary += `Con los ojos abiertos, el peso se distribuye de forma equilibrada entre ambos pies (diferencia de solo ${a.asym.toFixed(1)}%). `;
}
}
if (asymData.bipodal.ec) {
const a = asymData.bipodal.ec;
if (a.asym > 10) {
summary += `Con los ojos cerrados, la diferencia en el reparto de peso es de ${a.asym.toFixed(1)}%, cargando más hacia el lado ${a.dominant.toLowerCase()}. `;
} else if (a.asym > 5) {
summary += `Con los ojos cerrados, la diferencia es de ${a.asym.toFixed(1)}% hacia el lado ${a.dominant.toLowerCase()}. `;
} else {
summary += `Con los ojos cerrados, el reparto se mantiene equilibrado (${a.asym.toFixed(1)}% de diferencia). `;
}
}
if (asymData.bipodal.worsenEC !== undefined) {
if (asymData.bipodal.worsenEC) {
summary += `Es relevante que al cerrar los ojos la asimetría empeoró, lo que sugiere que la persona usa la vista para compensar un desequilibrio en cómo reparte su peso corporal. `;
} else {
summary += `Al cerrar los ojos la asimetría no empeoró, lo que indica que el desequilibrio no depende principalmente de la vista. `;
}
}
}
// Unipodal asymmetry explicada
if (asymData.hasUnipodal) {
summary += `\n\nTambién se evaluó el equilibrio apoyándose en un solo pie. `;
if (asymData.unipodal.eo_area) {
const a = asymData.unipodal.eo_area;
if (parseFloat(a.asym) > 30) {
summary += `Con los ojos abiertos, se encontró una diferencia importante del ${a.asym}% entre el apoyo derecho e izquierdo. La persona se mantiene notablemente más estable al apoyarse sobre el pie ${a.better.toLowerCase()}. `;
} else if (parseFloat(a.asym) > 15) {
summary += `Con los ojos abiertos, existe una diferencia moderada del ${a.asym}% entre ambos apoyos, con mejor estabilidad sobre el pie ${a.better.toLowerCase()}. `;
} else {
summary += `Con los ojos abiertos, ambos apoyos muestran una estabilidad similar (diferencia del ${a.asym}%), lo que es un resultado favorable. `;
}
}
if (asymData.unipodal.ec_area) {
const a = asymData.unipodal.ec_area;
if (parseFloat(a.asym) > 30) {
summary += `Con los ojos cerrados, la diferencia entre apoyos aumenta al ${a.asym}%, con mejor control sobre el pie ${a.better.toLowerCase()}. Esto sugiere que uno de los lados tiene menor capacidad de percibir su posición sin la ayuda de la vista. `;
} else if (parseFloat(a.asym) > 15) {
summary += `Con los ojos cerrados, la diferencia es del ${a.asym}%, con mejor control sobre el pie ${a.better.toLowerCase()}. `;
} else {
summary += `Con los ojos cerrados, ambos apoyos mantienen una estabilidad comparable (${a.asym}% de diferencia). `;
}
}
if (asymData.unipodal.right_rq && asymData.unipodal.left_rq) {
const rRQ = parseFloat(asymData.unipodal.right_rq);
const lRQ = parseFloat(asymData.unipodal.left_rq);
const diffRQ = Math.abs(rRQ - lRQ);
if (diffRQ > 30) {
const worse = rRQ > lRQ ? "derecho" : "izquierdo";
const better = rRQ > lRQ ? "izquierdo" : "derecho";
summary += `Un hallazgo importante es que al cerrar los ojos, el equilibrio sobre el pie ${worse} se deteriora mucho más que sobre el pie ${better}. Esto puede indicar que la sensibilidad o la capacidad de percibir la posición del cuerpo está más disminuida en el lado ${worse}, y que ese lado depende más de la vista para compensar. `;
}
}
}
// Contexto de calzado
summary += `\n\n`;
if (clinical.footwearCondition === "descalzo") {
summary += `Esta evaluación se realizó con el paciente descalzo, lo que permite medir directamente cómo funciona su equilibrio sin la influencia del calzado.`;
} else if (clinical.footwearCondition === "zapatilla") {
summary += `Esta evaluación se realizó con zapatilla (${clinical.shoeModel || "modelo no especificado"}). Es importante considerar que el tipo de calzado puede influir en los resultados, tanto en la estabilidad como en la sensibilidad de los pies.`;
} else if (clinical.footwearCondition === "plantillas") {
summary += `Esta evaluación se realizó con plantillas ortopédicas (${clinical.orthoticDetails || "detalles no especificados"}). Los resultados reflejan cómo se comporta el equilibrio con la corrección que proporcionan las plantillas.`;
}
return { contextNote, results, rq, summary, overallRisk, asymData };
}
function exportReportAsJPEG(clinical, analysis) {
try {
const W = 900, P = 40, CW = W - P * 2;
const cv = document.createElement("canvas");
cv.width = W; cv.height = 6000;
const c = cv.getContext("2d");
c.fillStyle = "#fff"; c.fillRect(0, 0, W, 6000);
const F = "Arial, Helvetica, sans-serif";
const wrapText = (txt, mw, font) => {
c.font = font;
const out = [];
(txt || "").split("\n\n").forEach((p, i) => {
if (i > 0) out.push("");
const ws = p.replace(/\s+/g, " ").trim().split(" ");
let ln = "";
ws.forEach(w => {
const t = ln ? ln + " " + w : w;
if (c.measureText(t).width > mw && ln) { out.push(ln); ln = w; }
else ln = t;
});
if (ln) out.push(ln);
});
return out;
};
const rr = (x, y, w, h, r, bg, brd) => {
c.beginPath();
c.moveTo(x+r,y); c.arcTo(x+w,y,x+w,y+r,r); c.arcTo(x+w,y+h,x+w-r,y+h,r);
c.arcTo(x,y+h,x,y+h-r,r); c.arcTo(x,y,x+r,y,r); c.closePath();
if (bg) { c.fillStyle = bg; c.fill(); }
if (brd) { c.strokeStyle = brd; c.lineWidth = 1; c.stroke(); }
};
let y = 0;
// Header
const grd = c.createLinearGradient(0, 0, W, 0);
grd.addColorStop(0, "#1e3a5f"); grd.addColorStop(1, "#2563eb");
c.fillStyle = grd; c.fillRect(0, 0, W, 90);
c.fillStyle = "#fff"; c.font = `bold 22px ${F}`;
c.fillText("Informe de Evaluación del Equilibrio", P, 38);
c.font = `13px ${F}`; c.globalAlpha = 0.85;
c.fillText("Plataforma HUR Labs iBalance Premium", P, 60);
c.textAlign = "right";
c.fillText("Fecha: " + new Date().toLocaleDateString("es-CL"), W - P, 60);
c.textAlign = "left"; c.globalAlpha = 1;
y = 110;
// Patient
if (clinical.name || clinical.diagnosis) {
let info = clinical.name || "";
if (clinical.age) info += " | " + clinical.age + " años";
if (clinical.sex) info += " | " + clinical.sex;
if (clinical.height) info += " | " + clinical.height + " cm";
if (clinical.weight) info += " | " + clinical.weight + " kg";
const fw = clinical.footwearCondition === "descalzo" ? "Descalzo"
: clinical.footwearCondition === "zapatilla" ? "Zapatilla (" + (clinical.shoeModel || "N/E") + ")"
: "Plantillas (" + (clinical.orthoticDetails || "N/E") + ")";
const dxLines = clinical.diagnosis ? wrapText("Diagnóstico: " + clinical.diagnosis, CW - 30, `12px ${F}`) : [];
const mcLines = clinical.chiefComplaint ? wrapText("Motivo: " + clinical.chiefComplaint, CW - 30, `12px ${F}`) : [];
const bH = 52 + (info ? 20 : 0) + 20 + dxLines.length * 16 + mcLines.length * 16 + 10;
rr(P, y, CW, bH, 10, "#f1f5f9", "#e2e8f0");
c.fillStyle = "#1e3a5f"; c.font = `bold 14px ${F}`;
c.fillText("Información del Paciente", P + 15, y + 25);
let py = y + 46;
if (info) { c.fillStyle = "#334155"; c.font = `13px ${F}`; c.fillText(info, P + 15, py); py += 20; }
c.fillStyle = "#6366f1"; c.font = `bold 12px ${F}`; c.fillText("Condición: " + fw, P + 15, py); py += 20;
c.fillStyle = "#475569"; c.font = `12px ${F}`;
dxLines.forEach(l => { if (l) c.fillText(l, P + 15, py); py += 16; });
mcLines.forEach(l => { if (l) c.fillText(l, P + 15, py); py += 16; });
y += bH + 14;
}
// Risk cards
const RC = {
alto: { bg:"#fef2f2", brd:"#fca5a5", bdg:"#ef4444", txt:"#991b1b", lb:"ALTO" },
medio: { bg:"#fffbeb", brd:"#fcd34d", bdg:"#f59e0b", txt:"#92400e", lb:"MEDIO" },
bajo: { bg:"#f0fdf4", brd:"#86efac", bdg:"#22c55e", txt:"#166534", lb:"BAJO" }
};
for (const card of (analysis.results || [])) {
const rc = RC[card.risk] || RC.bajo;
const ls = wrapText(card.text, CW - 34, `12px ${F}`);
const h = 42 + ls.length * 17 + 14;
rr(P, y, CW, h, 10, rc.bg, rc.brd);
c.fillStyle = rc.txt; c.font = `bold 13px ${F}`;
c.fillText(card.title, P + 15, y + 24);
const tw = c.measureText(card.title).width;
c.font = `bold 10px ${F}`;
const bw = c.measureText(rc.lb).width + 14;
rr(P + 20 + tw, y + 12, bw, 16, 8, rc.bdg, null);
c.fillStyle = "#fff"; c.fillText(rc.lb, P + 27 + tw, y + 23);
c.fillStyle = "#334155"; c.font = `12px ${F}`;
ls.forEach((l, i) => c.fillText(l, P + 15, y + 44 + i * 17));
y += h + 8;
}
// Asymmetry section
const ad = analysis.asymData;
if (ad && (ad.hasBipodal || ad.hasUnipodal)) {
y += 6;
const drawBox = (x, bY, w, label, val, sub) => {
rr(x, bY, w, 60, 8, "#fff", "#c7d2fe");
c.fillStyle = "#6366f1"; c.font = `bold 10px ${F}`; c.fillText(label, x + 10, bY + 16);
c.fillStyle = "#1e1b4b"; c.font = `bold 20px ${F}`; c.fillText(val, x + 10, bY + 38);
c.fillStyle = "#64748b"; c.font = `10px ${F}`; c.fillText(sub, x + 10, bY + 52);
};
let ah = 50;
if (ad.hasBipodal) { ah += 100; if (ad.bipodal.worsenEC !== undefined) ah += 30; }
if (ad.hasUnipodal) { ah += 100; if (ad.unipodal.right_rq && ad.unipodal.left_rq) ah += 50; }
rr(P, y, CW, ah, 10, "#eef2ff", "#a5b4fc");
c.fillStyle = "#3730a3"; c.font = `bold 14px ${F}`;
c.fillText("Perfil de Asimetría de Control Postural", P + 15, y + 25);
let ay = y + 48;
const bxW = (CW - 50) / 2;
if (ad.hasBipodal) {
c.fillStyle = "#4338ca"; c.font = `bold 12px ${F}`;
c.fillText("Bipodal", P + 15, ay); ay += 18;
if (ad.bipodal.eo) drawBox(P + 15, ay, bxW, "OJOS ABIERTOS", ad.bipodal.eo.asym.toFixed(1) + "%", "Izq:" + ad.bipodal.eo.left + "% Der:" + ad.bipodal.eo.right + "%");
if (ad.bipodal.ec) drawBox(P + 30 + bxW, ay, bxW, "OJOS CERRADOS", ad.bipodal.ec.asym.toFixed(1) + "%", "Izq:" + ad.bipodal.ec.left + "% Der:" + ad.bipodal.ec.right + "%");
ay += 68;
if (ad.bipodal.worsenEC !== undefined) {
c.fillStyle = "#334155"; c.font = `11px ${F}`;
c.fillText(ad.bipodal.worsenEC ? "La asimetría se incrementa al cerrar los ojos (+" + ad.bipodal.visualDependency + "%)" : "La asimetría se reduce al cerrar los ojos (-" + ad.bipodal.visualDependency + "%)", P + 15, ay + 6);
ay += 22;
}
}
if (ad.hasUnipodal) {
c.fillStyle = "#4338ca"; c.font = `bold 12px ${F}`;
c.fillText("Unipodal", P + 15, ay); ay += 18;
if (ad.unipodal.eo_area) drawBox(P + 15, ay, bxW, "OJOS ABIERTOS", ad.unipodal.eo_area.asym + "%", "Mejor: " + ad.unipodal.eo_area.better);
if (ad.unipodal.ec_area) drawBox(P + 30 + bxW, ay, bxW, "OJOS CERRADOS", ad.unipodal.ec_area.asym + "%", "Mejor: " + ad.unipodal.ec_area.better);
ay += 68;
if (ad.unipodal.right_rq && ad.unipodal.left_rq) {
rr(P + 15, ay, CW - 30, 36, 8, "#fff", "#c7d2fe");
c.fillStyle = "#6366f1"; c.font = `bold 10px ${F}`;
c.fillText("DEPENDENCIA VISUAL POR APOYO", P + 25, ay + 14);
c.fillStyle = "#1e1b4b"; c.font = `bold 12px ${F}`;
c.fillText("Derecho: " + ad.unipodal.right_rq + "% | Izquierdo: " + ad.unipodal.left_rq + "%", P + 25, ay + 30);
ay += 44;
}
}
y += ah + 14;
}
// Conclusion
if (analysis.summary) {
y += 4;
const cls = wrapText(analysis.summary, CW - 44, `13px ${F}`);
const cH = 54 + cls.length * 19 + 20;
rr(P, y, CW, cH, 10, "#1e3a5f", null);
c.fillStyle = "#fff"; c.font = `bold 16px ${F}`;
c.fillText("Conclusión General", P + 20, y + 30);
const tw2 = c.measureText("Conclusión General").width;
const rl = analysis.overallRisk === "alto" ? "RIESGO ALTO" : analysis.overallRisk === "medio" ? "RIESGO MODERADO" : "RIESGO BAJO";
const rc2 = analysis.overallRisk === "alto" ? "#ef4444" : analysis.overallRisk === "medio" ? "#f59e0b" : "#22c55e";
c.font = `bold 11px ${F}`;
const rlw = c.measureText(rl).width + 16;
rr(P + 30 + tw2, y + 18, rlw, 18, 9, rc2, null);
c.fillStyle = "#fff"; c.fillText(rl, P + 38 + tw2, y + 30);
c.font = `13px ${F}`; c.globalAlpha = 0.95;
let cy2 = y + 56;
cls.forEach(l => { if (l === "") cy2 += 10; else { c.fillText(l, P + 20, cy2); cy2 += 19; } });
c.globalAlpha = 1;
y += cH + 14;
}
// Footer
c.fillStyle = "#92400e"; c.font = `italic 10px ${F}`;
c.fillText("Herramienta de apoyo clínico. Rangos orientativos — base normativa iBalance Premium (~2900 tests).", P, y + 10);
c.fillText("Ref: Kapteyn et al. 1983 | Fernie et al. 1982 | Gagey & Weber 1999.", P, y + 24);
y += 46;
// Crop and download
const out = document.createElement("canvas");
out.width = W; out.height = y;
const oc = out.getContext("2d");
oc.fillStyle = "#fff"; oc.fillRect(0, 0, W, y);
oc.drawImage(cv, 0, 0);
const dataUrl = out.toDataURL("image/jpeg", 0.95);
const link = document.createElement("a");
const safeName = (clinical.name || "paciente").replace(/\s+/g, "_").replace(/[^\w]/g, "");
link.href = dataUrl;
link.download = "informe_balance_" + safeName + "_" + new Date().toISOString().split("T")[0] + ".jpg";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
return true;
} catch (e) {
console.error("Export error:", e);
return false;
}
}
const EXTRACT_PROMPT = `You are a data extraction assistant for HUR Labs iBalance Premium PDF reports.
Extract ALL numeric values for balance test results. Return ONLY valid JSON, no markdown, no backticks, no explanation.
{
"balance": {
"eo_c90area":"","eo_traceLength":"","eo_stdVelocity":"","eo_velocity":"",
"eo_c90angle":"","eo_stdX":"","eo_stdY":"","eo_wdLeft":"","eo_wdRight":"",
"ec_c90area":"","ec_traceLength":"","ec_stdVelocity":"","ec_velocity":"",
"ec_c90angle":"","ec_stdX":"","ec_stdY":"","ec_wdLeft":"","ec_wdRight":""
},
"unipodal": {
"right_eo_c90area":"","right_eo_velocity":"","right_eo_traceLength":"","right_eo_stdX":"","right_eo_stdY":"",
"right_ec_c90area":"","right_ec_velocity":"","right_ec_traceLength":"","right_ec_stdX":"","right_ec_stdY":"",
"left_eo_c90area":"","left_eo_velocity":"","left_eo_traceLength":"","left_eo_stdX":"","left_eo_stdY":"",
"left_ec_c90area":"","left_ec_velocity":"","left_ec_traceLength":"","left_ec_stdX":"","left_ec_stdY":""
},
"los": {
"forward_achieved":"","rearward_achieved":"","leftward_achieved":"","rightward_achieved":"",
"forward_ref":"","rearward_ref":"","leftward_ref":"","rightward_ref":""
},
"patient":{"name":"","height":"","weight":""},
"romberg_quotient":"","protocol":"","test_date":""
}
Key mapping:
- Bipedal "Eyes Open"=eo, "Eyes Closed"=ec
- Single leg / one foot / unipodal: right leg = right_, left leg = left_, combined with eo/ec
- C90 Area=c90area, Trace Length=traceLength, Std Velocity=stdVelocity, Velocity=velocity, C90 Angle=c90angle
- Std X Deviation=stdX, Std Y Deviation=stdY
- Weight Distribution Left/WD-L=wdLeft, WD-R=wdRight
- LoS: Forward/Rearward/Leftward/Rightward achieved and reference
- Use decimal POINT. Convert "523,94" to "523.94".
- If the PDF has single-leg/unipodal tests, extract them into the unipodal object.
Return ONLY the JSON.`;
export default function App() {
const [tab,setTab]=useState(0);
const [clinical,setClinical]=useState(defaultClinical);
const [balance,setBalance]=useState(defaultBalance);
const [unipodal,setUnipodal]=useState(defaultUnipodal);
const [los,setLoS]=useState(defaultLoS);
const [pdfStatus,setPdfStatus]=useState("idle");
const [pdfError,setPdfError]=useState("");
const [pdfFileName,setPdfFileName]=useState("");
const [extractedMeta,setExtractedMeta]=useState(null);
const [exportStatus,setExportStatus]=useState("idle");
const fileRef=useRef(null);
const uc=k=>v=>setClinical(prev=>({...prev,[k]:v}));
const handlePdfUpload=async(e)=>{
const file=e.target.files?.[0];
if(!file)return;
if(file.type!=="application/pdf"){setPdfError("Por favor sube un archivo PDF.");return;}
if(file.size>20*1024*1024){setPdfError("Archivo demasiado grande (máx 20MB).");return;}
setPdfFileName(file.name);setPdfStatus("reading");setPdfError("");
try{
const base64=await new Promise((res,rej)=>{const r=new FileReader();r.onload=()=>res(r.result.split(",")[1]);r.onerror=()=>rej(new Error("Error al leer"));r.readAsDataURL(file);});
setPdfStatus("extracting");
const response=await fetch("https://api.anthropic.com/v1/messages",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({model:"claude-sonnet-4-20250514",max_tokens:2000,messages:[{role:"user",content:[{type:"document",source:{type:"base64",media_type:"application/pdf",data:base64}},{type:"text",text:EXTRACT_PROMPT}]}]})});
if(!response.ok) throw new Error(`Error API: ${response.status}`);
const data=await response.json();
const text=data.content?.map(c=>c.text||"").join("")||"";
const clean=text.replace(/```json|```/g,"").trim();
let parsed;
try{parsed=JSON.parse(clean);}catch{throw new Error("No se pudo interpretar la respuesta.");}
if(parsed.balance){const nb={...defaultBalance};Object.keys(nb).forEach(k=>{if(parsed.balance[k]!==undefined&&parsed.balance[k]!=="")nb[k]=String(parsed.balance[k]).replace(",",".");});setBalance(nb);}
if(parsed.unipodal){const nu={...defaultUnipodal};Object.keys(nu).forEach(k=>{if(parsed.unipodal[k]!==undefined&&parsed.unipodal[k]!=="")nu[k]=String(parsed.unipodal[k]).replace(",",".");});setUnipodal(nu);}
if(parsed.los){const nl={...defaultLoS};Object.keys(nl).forEach(k=>{if(parsed.los[k]!==undefined&&parsed.los[k]!=="")nl[k]=String(parsed.los[k]).replace(",",".");});setLoS(nl);}
if(parsed.patient){if(parsed.patient.name)setClinical(prev=>({...prev,name:parsed.patient.name}));if(parsed.patient.height)setClinical(prev=>({...prev,height:String(parsed.patient.height)}));if(parsed.patient.weight)setClinical(prev=>({...prev,weight:String(parsed.patient.weight)}));}
setExtractedMeta({protocol:parsed.protocol||"",testDate:parsed.test_date||"",rq:parsed.romberg_quotient||""});
setPdfStatus("done");
}catch(err){setPdfError(err.message||"Error desconocido");setPdfStatus("error");}
};
const resetPdf=()=>{setPdfStatus("idle");setPdfFileName("");setPdfError("");setExtractedMeta(null);setBalance(defaultBalance);setUnipodal(defaultUnipodal);setLoS(defaultLoS);if(fileRef.current)fileRef.current.value="";};
const handleExportPDF = () => {
if (!analysis) return;
setExportStatus("generating");
setTimeout(() => {
exportReportAsPDF(clinical, analysis).then(ok => {
setExportStatus(ok ? "done" : "error");
setTimeout(() => setExportStatus("idle"), 2500);
});
}, 100);
};
const analysis=tab===2?generateAnalysis(clinical,balance,los,unipodal):null;
return(
<div style={{fontFamily:"-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif",maxWidth:900,margin:"0 auto",padding:"10px 6px"}}>
<div style={{background:"linear-gradient(135deg,#1e3a5f 0%,#2563eb 100%)",borderRadius:12,padding:"18px 20px",marginBottom:16,color:"#fff"}}>
<div style={{fontSize:20,fontWeight:800,letterSpacing:-0.5}}>Analizador Clínico iBalance Premium</div>
<div style={{fontSize:12,opacity:0.85,marginTop:4}}>Evaluación posturográfica — Plataforma HUR Labs</div>
</div>
<div style={{display:"flex",gap:6,marginBottom:16}}>
{TABS.map((t,i)=>(<button key={i} onClick={()=>setTab(i)} style={{flex:1,padding:"11px 8px",border:"none",borderRadius:8,fontSize:13,fontWeight:700,cursor:"pointer",background:tab===i?"#2563eb":"#e2e8f0",color:tab===i?"#fff":"#475569",transition:"all 0.2s"}}>{t}</button>))}
</div>
{tab===0&&(
<div>
<Section title="Datos del Paciente">
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:10}}>
<Input label="Nombre" value={clinical.name} onChange={uc("name")}/>
<Input label="Edad" value={clinical.age} onChange={uc("age")} type="number"/>
<Select label="Sexo" value={clinical.sex} onChange={uc("sex")} options={[{value:"masculino",label:"Masculino"},{value:"femenino",label:"Femenino"}]}/>
<Input label="Talla (cm)" value={clinical.height} onChange={uc("height")} type="number"/>
<Input label="Peso (kg)" value={clinical.weight} onChange={uc("weight")} type="number"/>
</div>
</Section>
<Section title="Contexto Clínico">
<TextArea label="Motivo de Consulta" value={clinical.chiefComplaint} onChange={uc("chiefComplaint")} placeholder="Ej: Dolor en tobillo derecho post esguince..."/>
<TextArea label="Diagnóstico / Lesión Existente" value={clinical.diagnosis} onChange={uc("diagnosis")} placeholder="Ej: Esguince grado II LCA rodilla izquierda..."/>
<TextArea label="Antecedentes Relevantes" value={clinical.injuryHistory} onChange={uc("injuryHistory")} placeholder="Lesiones previas, cirugías, patologías de base..."/>
</Section>
<Section title="Condición de Calzado">
<Select label="Condición" value={clinical.footwearCondition} onChange={uc("footwearCondition")} options={[{value:"descalzo",label:"Descalzo"},{value:"zapatilla",label:"Con zapatilla"},{value:"plantillas",label:"Con plantillas ortopédicas"}]}/>
{clinical.footwearCondition==="zapatilla"&&<Input label="Modelo de Zapatilla" value={clinical.shoeModel} onChange={uc("shoeModel")} placeholder="Ej: Nike Pegasus 40..."/>}
{clinical.footwearCondition==="plantillas"&&<TextArea label="Detalle de Plantillas" value={clinical.orthoticDetails} onChange={uc("orthoticDetails")} rows={2} placeholder="Tipo, material, corrección..."/>}
</Section>
<Section title="Notas Adicionales">
<TextArea label="Observaciones" value={clinical.additionalNotes} onChange={uc("additionalNotes")} placeholder="Condiciones particulares, medicación, fatiga..."/>
</Section>
</div>
)}
{tab===1&&(
<div>
<Section title="Cargar Informe PDF del iBalance Premium">
<div style={{textAlign:"center",padding:"36px 20px",border:"2px dashed #93c5fd",borderRadius:14,background:"#eff6ff",cursor:"pointer",marginBottom:16}} onClick={()=>fileRef.current?.click()}>
<input ref={fileRef} type="file" accept=".pdf" onChange={handlePdfUpload} style={{display:"none"}}/>
<div style={{fontSize:44,marginBottom:10}}>📄</div>
<div style={{fontSize:16,fontWeight:700,color:"#1e40af"}}>{pdfFileName&&pdfStatus!=="idle"?pdfFileName:"Haz clic para seleccionar un PDF"}</div>
<div style={{fontSize:12,color:"#64748b",marginTop:6}}>Informe exportado desde iBalance Premium (Bipodal, Unipodal o LoS)</div>
</div>
{pdfStatus==="reading"&&<div style={{textAlign:"center",padding:24}}><div style={{fontSize:14,color:"#2563eb",fontWeight:600}}>📖 Leyendo archivo...</div></div>}
{pdfStatus==="extracting"&&(
<div style={{textAlign:"center",padding:24}}>
<div style={{fontSize:28,marginBottom:10}}>🔬</div>
<div style={{fontSize:15,color:"#2563eb",fontWeight:700}}>Extrayendo datos con IA...</div>
<div style={{fontSize:12,color:"#64748b",marginTop:6}}>Identificando parámetros bipodales, unipodales y LoS</div>
<div style={{marginTop:14,height:4,background:"#dbeafe",borderRadius:4,overflow:"hidden"}}><div style={{height:"100%",background:"#2563eb",borderRadius:4,animation:"ld 1.5s ease-in-out infinite"}}/></div>
<style>{`@keyframes ld{0%{width:10%;margin-left:0}50%{width:60%;margin-left:20%}100%{width:10%;margin-left:90%}}`}</style>
</div>
)}
{pdfStatus==="error"&&(
<div style={{background:"#fef2f2",border:"1px solid #fca5a5",borderRadius:10,padding:14}}>
<div style={{fontSize:14,color:"#991b1b",fontWeight:700}}>Error al procesar el PDF</div>
<div style={{fontSize:13,color:"#b91c1c",marginTop:4}}>{pdfError}</div>
<button onClick={resetPdf} style={{marginTop:10,padding:"8px 16px",background:"#ef4444",color:"#fff",border:"none",borderRadius:6,fontSize:13,fontWeight:600,cursor:"pointer"}}>Intentar de nuevo</button>
</div>
)}
{pdfStatus==="done"&&(
<div>
<div style={{background:"#f0fdf4",border:"1px solid #86efac",borderRadius:10,padding:16}}>
<div style={{fontSize:15,fontWeight:700,color:"#166534",marginBottom:10}}>✓ Datos extraídos correctamente</div>
{extractedMeta&&(
<div style={{fontSize:13,color:"#334155",marginBottom:10,lineHeight:1.7}}>
{extractedMeta.protocol&&<span style={{display:"inline-block",background:"#dbeafe",padding:"2px 8px",borderRadius:6,fontSize:12,fontWeight:600,color:"#1e40af",marginRight:8}}>{extractedMeta.protocol}</span>}
{extractedMeta.testDate&&<span style={{fontSize:12,color:"#64748b"}}>Fecha: {extractedMeta.testDate}</span>}
{extractedMeta.rq&&<span style={{display:"block",marginTop:4,fontSize:13}}><strong>Cociente Romberg:</strong> {extractedMeta.rq}</span>}
</div>
)}
<div style={{fontSize:13,color:"#166534"}}>Revisa los datos y ve a <strong>"Análisis"</strong> para la conclusión completa.</div>
<button onClick={resetPdf} style={{marginTop:12,padding:"8px 16px",background:"#2563eb",color:"#fff",border:"none",borderRadius:6,fontSize:13,fontWeight:600,cursor:"pointer"}}>Subir otro PDF</button>
</div>
<DataPreview balance={balance} los={los} unipodal={unipodal}/>
</div>
)}
</Section>
{pdfStatus==="idle"&&<div style={{background:"#f1f5f9",border:"1px solid #e2e8f0",borderRadius:8,padding:14,fontSize:13,color:"#475569",lineHeight:1.6}}><strong>¿Qué PDF subir?</strong> Exporta desde iBalance Premium el informe Summary, Posturogram o LoS. El sistema extrae datos bipodales, unipodales y de límites de estabilidad automáticamente.</div>}
</div>
)}
{tab===2&&analysis&&(
<div>
{/* Export Button */}
{analysis.results.length > 0 && analysis.results[0].title !== "Sin datos" && (
<div style={{marginBottom:14}}>
<button
onClick={handleExportPDF}
disabled={exportStatus === "generating"}
style={{
width:"100%",
padding:"12px 16px",
background: exportStatus === "done" ? "#22c55e" : exportStatus === "error" ? "#ef4444" : "#2563eb",
color:"#fff", border:"none", borderRadius:10, fontSize:14, fontWeight:700,
cursor: exportStatus === "generating" ? "wait" : "pointer",
display:"flex", alignItems:"center", justifyContent:"center", gap:8,
transition:"all 0.2s", opacity: exportStatus === "generating" ? 0.7 : 1
}}>
{exportStatus === "generating" && <><span>⏳</span> Generando PDF...</>}
{exportStatus === "done" && <><span>✓</span> PDF descargado</>}
{exportStatus === "error" && <><span>✗</span> Error al generar</>}
{exportStatus === "idle" && <><span>📥</span> Descargar informe en PDF (2 páginas)</>}
</button>
</div>
)}
{clinical.name&&(
<div style={{background:"#f1f5f9",borderRadius:10,padding:14,marginBottom:14,fontSize:13}}>
<strong>Paciente:</strong> {clinical.name} {clinical.age?`| ${clinical.age} años`:""} {clinical.sex?`| ${clinical.sex}`:""}
{clinical.height?` | ${clinical.height} cm`:""}{clinical.weight?` | ${clinical.weight} kg`:""}
{clinical.footwearCondition&&<span style={{display:"block",marginTop:4,color:"#6366f1",fontWeight:600}}>Condición: {clinical.footwearCondition==="descalzo"?"Descalzo":clinical.footwearCondition==="zapatilla"?`Zapatilla (${clinical.shoeModel||"N/E"})`:`Plantillas (${clinical.orthoticDetails||"N/E"})`}</span>}
{clinical.diagnosis&&<span style={{display:"block",marginTop:2,fontSize:12,color:"#475569"}}>Dx: {clinical.diagnosis}</span>}
</div>
)}
{analysis.results.map((r,i)=>(<AnalysisCard key={i} title={r.title} risk={r.risk}>{r.text}</AnalysisCard>))}
{/* Asymmetry Summary Card */}
{analysis.asymData && (analysis.asymData.hasBipodal || analysis.asymData.hasUnipodal) && (
<div style={{background:"#eef2ff",border:"1px solid #a5b4fc",borderRadius:10,padding:16,marginBottom:12}}>
<div style={{fontWeight:700,fontSize:15,color:"#3730a3",marginBottom:10}}>Perfil de Asimetría de Control Postural</div>
{analysis.asymData.hasBipodal && (
<div style={{marginBottom:12}}>
<div style={{fontSize:13,fontWeight:700,color:"#4338ca",marginBottom:6}}>Bipodal</div>
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:8}}>
{analysis.asymData.bipodal.eo && (
<div style={{background:"#fff",borderRadius:8,padding:10,border:"1px solid #c7d2fe"}}>
<div style={{fontSize:11,color:"#6366f1",fontWeight:600}}>OJOS ABIERTOS</div>
<div style={{fontSize:22,fontWeight:800,color:"#1e1b4b"}}>{analysis.asymData.bipodal.eo.asym.toFixed(1)}%</div>
<div style={{fontSize:11,color:"#64748b"}}>Izq: {analysis.asymData.bipodal.eo.left}% | Der: {analysis.asymData.bipodal.eo.right}%</div>
<div style={{fontSize:11,color:"#4338ca",fontWeight:600}}>Predominio: {analysis.asymData.bipodal.eo.dominant}</div>
</div>
)}
{analysis.asymData.bipodal.ec && (
<div style={{background:"#fff",borderRadius:8,padding:10,border:"1px solid #c7d2fe"}}>
<div style={{fontSize:11,color:"#6366f1",fontWeight:600}}>OJOS CERRADOS</div>
<div style={{fontSize:22,fontWeight:800,color:"#1e1b4b"}}>{analysis.asymData.bipodal.ec.asym.toFixed(1)}%</div>
<div style={{fontSize:11,color:"#64748b"}}>Izq: {analysis.asymData.bipodal.ec.left}% | Der: {analysis.asymData.bipodal.ec.right}%</div>
<div style={{fontSize:11,color:"#4338ca",fontWeight:600}}>Predominio: {analysis.asymData.bipodal.ec.dominant}</div>
</div>
)}
</div>
{analysis.asymData.bipodal.worsenEC !== undefined && (
<div style={{fontSize:12,color:"#334155",marginTop:8,lineHeight:1.5}}>
{analysis.asymData.bipodal.worsenEC
? `⚠️ La asimetría se incrementa al cerrar los ojos (+${analysis.asymData.bipodal.visualDependency}%), sugiriendo compensación visual parcial del desequilibrio.`
: `✓ La asimetría se reduce al cerrar los ojos (−${analysis.asymData.bipodal.visualDependency}%), el input visual no es el factor principal.`}
</div>
)}
</div>
)}
{analysis.asymData.hasUnipodal && (
<div>
<div style={{fontSize:13,fontWeight:700,color:"#4338ca",marginBottom:6}}>Unipodal (C90 Area)</div>
<div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:8,marginBottom:8}}>
{analysis.asymData.unipodal.eo_area && (
<div style={{background:"#fff",borderRadius:8,padding:10,border:"1px solid #c7d2fe"}}>
<div style={{fontSize:11,color:"#6366f1",fontWeight:600}}>OJOS ABIERTOS</div>
<div style={{fontSize:22,fontWeight:800,color:"#1e1b4b"}}>{analysis.asymData.unipodal.eo_area.asym}%</div>
<div style={{fontSize:11,color:"#64748b"}}>Der: {analysis.asymData.unipodal.eo_area.right} mm² | Izq: {analysis.asymData.unipodal.eo_area.left} mm²</div>
<div style={{fontSize:11,color:"#4338ca",fontWeight:600}}>Mejor control: {analysis.asymData.unipodal.eo_area.better}</div>
</div>
)}
{analysis.asymData.unipodal.ec_area && (
<div style={{background:"#fff",borderRadius:8,padding:10,border:"1px solid #c7d2fe"}}>
<div style={{fontSize:11,color:"#6366f1",fontWeight:600}}>OJOS CERRADOS</div>
<div style={{fontSize:22,fontWeight:800,color:"#1e1b4b"}}>{analysis.asymData.unipodal.ec_area.asym}%</div>
<div style={{fontSize:11,color:"#64748b"}}>Der: {analysis.asymData.unipodal.ec_area.right} mm² | Izq: {analysis.asymData.unipodal.ec_area.left} mm²</div>
<div style={{fontSize:11,color:"#4338ca",fontWeight:600}}>Mejor control: {analysis.asymData.unipodal.ec_area.better}</div>
</div>
)}
</div>
{(analysis.asymData.unipodal.right_rq || analysis.asymData.unipodal.left_rq) && (
<div style={{background:"#fff",borderRadius:8,padding:10,border:"1px solid #c7d2fe",marginBottom:8}}>
<div style={{fontSize:11,color:"#6366f1",fontWeight:600,marginBottom:4}}>DEPENDENCIA VISUAL POR APOYO (RQ Unipodal)</div>
<div style={{display:"flex",gap:16,fontSize:13}}>
{analysis.asymData.unipodal.right_rq && <div><strong>Derecho:</strong> {analysis.asymData.unipodal.right_rq}%</div>}
{analysis.asymData.unipodal.left_rq && <div><strong>Izquierdo:</strong> {analysis.asymData.unipodal.left_rq}%</div>}
</div>
</div>
)}
</div>
)}
</div>
)}
{analysis.summary&&(
<div style={{background:"#1e3a5f",borderRadius:10,padding:18,color:"#fff",marginTop:4}}>
<div style={{fontSize:15,fontWeight:700,marginBottom:8}}>Conclusión General{analysis.overallRisk&&<RiskBadge level={analysis.overallRisk} label={analysis.overallRisk==="alto"?"RIESGO ALTO":analysis.overallRisk==="medio"?"RIESGO MODERADO":"RIESGO BAJO"}/>}</div>
<div style={{fontSize:13,lineHeight:1.7,opacity:0.95}}>{analysis.summary}</div>
</div>
)}
<div style={{background:"#fefce8",border:"1px solid #fde68a",borderRadius:8,padding:12,marginTop:14,fontSize:11,color:"#92400e"}}>
<strong>Nota:</strong> Herramienta de apoyo clínico. Rangos orientativos basados en base normativa iBalance Premium (~2900 tests). Ref: Kapteyn et al. 1983; Fernie et al. 1982; Gagey & Weber 1999.
</div>
</div>
)}
</div>
);
}