Files
enviPy-bayer/static/js/alpine/components/widgets.js
Tobias O d80dfb5ee3 [Feature] Dynamic additional information rendering in frontend (#282)
This implements a version of #274, relying on Pydantics built in JSON schema and JSON rendering.
Requires additional UI tagging in the ai model repo but will remove HTML tags.

Example scenario with filled information: 5882df9c-dae1-4d80-a40e-db4724271456/scenario/3a4d395a-6a6d-4154-8ce3-ced667fceec0

Reviewed-on: enviPath/enviPy#282
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2026-01-31 00:44:03 +13:00

187 lines
6.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Alpine.js Widget Components for Schema Forms
*
* Centralized widget component definitions for dynamic form rendering.
* Each widget receives explicit parameters instead of context object for better traceability.
*/
document.addEventListener('alpine:init', () => {
// Base widget factory with common functionality
const baseWidget = (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({
fieldName,
data,
schema,
uiSchema,
fieldErrors,
mode,
// Field schema access
get fieldSchema() {
return this.schema?.properties?.[this.fieldName] || {};
},
// Common metadata
get label() {
// Check uiSchema first (RJSF format)
if (this.uiSchema?.[this.fieldName]?.['ui:label']) {
return this.uiSchema[this.fieldName]['ui:label'];
}
// Fall back to schema title
if (this.fieldSchema.title) {
return this.fieldSchema.title;
}
// Default: format field name
return this.fieldName.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
},
get helpText() {
return this.fieldSchema.description || '';
},
// Field-level unit extraction from uiSchema (RJSF format)
get unit() {
return this.uiSchema?.[this.fieldName]?.['ui:unit'] || null;
},
// Error handling
get hasError() {
return !!this.fieldErrors?.[this.fieldName];
},
get errors() {
return this.fieldErrors?.[this.fieldName] || [];
},
// Mode checks
get isViewMode() { return this.mode === 'view'; },
get isEditMode() { return this.mode === 'edit'; },
});
// Text widget
Alpine.data('textWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({
...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode),
get value() { return this.data[this.fieldName] || ''; },
set value(v) { this.data[this.fieldName] = v; },
}));
// Textarea widget
Alpine.data('textareaWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({
...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode),
get value() { return this.data[this.fieldName] || ''; },
set value(v) { this.data[this.fieldName] = v; },
}));
// Number widget with unit support
Alpine.data('numberWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({
...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode),
get value() { return this.data[this.fieldName]; },
set value(v) {
this.data[this.fieldName] = v === '' || v === null ? null : parseFloat(v);
},
get hasValue() {
return this.value !== null && this.value !== undefined && this.value !== '';
},
// Format value with unit for view mode
get displayValue() {
if (!this.hasValue) return '—';
return this.unit ? `${this.value} ${this.unit}` : String(this.value);
},
}));
// Select widget
Alpine.data('selectWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({
...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode),
get value() { return this.data[this.fieldName] || ''; },
set value(v) { this.data[this.fieldName] = v; },
get options() { return this.fieldSchema.enum || []; },
}));
// Checkbox widget
Alpine.data('checkboxWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({
...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode),
get checked() { return !!this.data[this.fieldName]; },
set checked(v) { this.data[this.fieldName] = v; },
}));
// Interval widget with unit support
Alpine.data('intervalWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({
...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode),
get start() {
return this.data[this.fieldName]?.start ?? null;
},
set start(v) {
if (!this.data[this.fieldName]) this.data[this.fieldName] = {};
this.data[this.fieldName].start = v === '' || v === null ? null : parseFloat(v);
},
get end() {
return this.data[this.fieldName]?.end ?? null;
},
set end(v) {
if (!this.data[this.fieldName]) this.data[this.fieldName] = {};
this.data[this.fieldName].end = v === '' || v === null ? null : parseFloat(v);
},
// Format interval with unit for view mode
get displayValue() {
const s = this.start, e = this.end;
const unitStr = this.unit ? ` ${this.unit}` : '';
if (s !== null && e !== null) return `${s} ${e}${unitStr}`;
if (s !== null) return `${s}${unitStr}`;
if (e !== null) return `${e}${unitStr}`;
return '—';
},
get isSameValue() {
return this.start !== null && this.start === this.end;
},
// Validation: start must be <= end
get hasValidationError() {
if (this.isViewMode) return false;
const s = this.start;
const e = this.end;
// Only validate if both values are provided
if (s !== null && e !== null && typeof s === 'number' && typeof e === 'number') {
return s > e;
}
return false;
},
// Override hasError to include validation error
get hasError() {
return this.hasValidationError || !!this.fieldErrors?.[this.fieldName];
},
// Override errors to include validation error message
get errors() {
const serverErrors = this.fieldErrors?.[this.fieldName] || [];
const validationErrors = this.hasValidationError
? ['Start value must be less than or equal to end value']
: [];
return [...validationErrors, ...serverErrors];
},
}));
// PubMed link widget
Alpine.data('pubmedWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({
...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode),
get value() { return this.data[this.fieldName] || ''; },
set value(v) { this.data[this.fieldName] = v; },
get pubmedUrl() {
return this.value ? `https://pubmed.ncbi.nlm.nih.gov/${this.value}` : null;
},
}));
// Compound link widget
Alpine.data('compoundWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({
...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode),
get value() { return this.data[this.fieldName] || ''; },
set value(v) { this.data[this.fieldName] = v; },
}));
});