/**
* DynamicClinicalForm.jsx
* =======================
* The core Schema-Driven Dynamic Form Engine.
*
* Fetches the JSON Schema for the selected disease via useDiseaseForm hook,
* renders categorized accordion cards with dynamic field components,
* and provides submit/reset controls.
*
* Features:
* - Schema fetching with loading skeleton
* - Error boundary + retry
* - Categorized collapsible cards (Vitals, Lifestyle, etc.)
* - Toggle rows rendered inline in compact grids
* - React Hook Form integration with Zod validation
* - Submit button that triggers parent callback
*/
import { useState } from 'react';
import {
Play,
RotateCcw,
Loader2,
ChevronDown,
Activity,
Heart,
User,
Stethoscope,
Pill,
Wind,
ClipboardList,
AlertCircle,
Shuffle,
BookOpen,
} from 'lucide-react';
import { useDisease } from '../context/DiseaseContext';
import { useDiseaseForm } from '../hooks/useDiseaseForm';
import { SchemaErrorBoundary } from './ErrorBoundary';
import SchemaFieldFactory from './SchemaFieldFactory';
import FormSkeleton from './FormSkeleton';
import { getCategoryIcon } from '../utils/featureCategorizer';
import VariableScalesModal from './VariableScalesModal';
// ── Icon resolver for categories ──
const CATEGORY_ICONS = {
Activity, Heart, User, Stethoscope, Pill, Wind, ClipboardList,
};
function resolveIcon(iconName) {
return CATEGORY_ICONS[iconName] || ClipboardList;
}
/**
* Collapsible category card — styled accordion for field groups.
*/
function CategoryCard({ category, fields, control, errors, defaultOpen }) {
const [isOpen, setIsOpen] = useState(defaultOpen);
const Icon = resolveIcon(getCategoryIcon(category));
// Separate toggle fields from others for compact layout
const toggleFields = fields.filter((f) => f.component === 'toggle');
const otherFields = fields.filter((f) => f.component !== 'toggle');
return (
{isOpen && (
{/* Toggle fields — compact inline grid */}
{toggleFields.length > 0 && (
{toggleFields.map((field) => (
))}
)}
{/* Other fields — standard grid */}
{otherFields.length > 0 && (
{otherFields.map((field) => (
))}
)}
)}
);
}
/**
* DynamicClinicalForm — the main form component.
*
* @param {{
* diseaseName: string,
* onRunInference: (formData: object) => Promise,
* loading?: boolean,
* defaultOpenCategories?: string[],
* }} props
*/
export default function DynamicClinicalForm({
diseaseName,
onRunInference,
loading = false,
defaultOpenCategories = ['Vitals & Signs', 'Medical History'],
}) {
const [hasRun, setHasRun] = useState(false);
const [scalesModalOpen, setScalesModalOpen] = useState(false);
// ── Randomize all fields with valid data ──
const randomizeFields = () => {
const randomized = {};
fields.forEach((f) => {
if (f.component === 'toggle' || f.component === 'segmented') {
randomized[f.name] = Math.random() > 0.5 ? 1 : 0;
} else if (f.component === 'select' && f.validation.enum?.length) {
const opts = f.validation.enum;
randomized[f.name] = opts[Math.floor(Math.random() * opts.length)];
} else if (f.type === 'number' || f.type === 'integer') {
const min = f.validation.minimum ?? 0;
const max = f.validation.maximum ?? 100;
const step = f.validation.step ?? (f.type === 'number' ? 0.1 : 1);
const val = min + Math.random() * (max - min);
// Align to step increments, then round to avoid floating jitter
const stepped = Math.round(val / step) * step;
randomized[f.name] = step < 1
? parseFloat(stepped.toFixed(10))
: Math.round(stepped);
} else {
randomized[f.name] = f.default ?? '';
}
});
form.reset(randomized);
};
// ── Form hook ──
const {
form,
categorizedFields,
fields,
schemaLoading,
schemaError,
refetchSchema,
submitForm,
resetForm,
hasErrors,
} = useDiseaseForm(diseaseName, {
onSubmit: async (data) => {
setHasRun(true);
if (onRunInference) {
await onRunInference(data);
}
},
});
const { control, formState: { errors } } = form;
// ── Loading skeleton ──
if (schemaLoading) {
return (
);
}
// ── Schema error ──
if (schemaError) {
return (
Failed to load form schema
{schemaError}
);
}
// ── Empty schema ──
if (!fields || fields.length === 0) {
return (
No form fields available for this disease.
);
}
// ── Render form ──
return (
<>
setScalesModalOpen(false)}
fields={fields}
diseaseName={diseaseName}
categorizedFields={categorizedFields}
/>
>
);
}