[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>
This commit is contained in:
2026-01-31 00:44:03 +13:00
committed by jebus
parent 9f63a9d4de
commit d80dfb5ee3
42 changed files with 3732 additions and 609 deletions

View File

@ -3,10 +3,99 @@
<dialog
id="update_scenario_additional_information_modal"
class="modal"
x-data="modalForm()"
x-data="{
isSubmitting: false,
items: [],
schemas: {},
loading: false,
error: null,
originalItems: [], // Store original data to detect changes
modifiedUuids: new Set(), // Track which items were modified
async init() {
try {
this.loading = true;
const scenarioUuid = '{{ scenario.uuid }}';
const { items, schemas } =
await window.AdditionalInformationApi.loadSchemasAndItems(scenarioUuid);
this.items = items;
this.schemas = schemas;
// Store deep copy of original items for comparison
this.originalItems = JSON.parse(JSON.stringify(items));
} catch (err) {
this.error = err.message;
} finally {
this.loading = false;
}
},
reset() {
this.isSubmitting = false;
this.error = null;
this.modifiedUuids.clear();
},
updateItemData(uuid, data) {
// Update the item's data in the items array
const item = this.items.find(i => i.uuid === uuid);
if (item) {
item.data = data;
// Mark this item as modified
this.modifiedUuids.add(uuid);
}
},
async submit() {
if (this.items.length === 0) {
this.error = 'No data to update';
return;
}
// Filter to only items that were actually modified
const modifiedItems = this.items.filter(item => this.modifiedUuids.has(item.uuid));
if (modifiedItems.length === 0) {
this.error = 'No changes to save';
return;
}
this.isSubmitting = true;
this.error = null;
try {
const scenarioUuid = '{{ scenario.uuid }}';
// Use the unified API client for sequential, safe updates - only modified items
await window.AdditionalInformationApi.updateItems(scenarioUuid, modifiedItems);
// Close modal and reload page
document.getElementById('update_scenario_additional_information_modal').close();
window.location.reload();
} catch (err) {
// Handle validation errors with field-level details
if (err.isValidationError && err.fieldErrors) {
this.error = err.message;
// Dispatch event to set field errors in the specific form
if (err.itemUuid) {
window.dispatchEvent(new CustomEvent('set-field-errors-for-item', {
detail: {
uuid: err.itemUuid,
fieldErrors: err.fieldErrors
}
}));
}
} else {
this.error = err.message;
}
} finally {
this.isSubmitting = false;
}
}
}"
@close="reset()"
@update-item-data.window="updateItemData($event.detail.uuid, $event.detail.data)"
>
<div class="modal-box">
<div class="modal-box max-h-[90vh] max-w-4xl overflow-y-auto">
<!-- Header -->
<h3 class="text-lg font-bold">Update Additional Information</h3>
@ -22,18 +111,39 @@
<!-- Body -->
<div class="py-4">
<form
id="edit-scenario-additional-information-modal-form"
accept-charset="UTF-8"
action=""
method="post"
>
{% csrf_token %}
{% for widget in update_widgets %}
{{ widget|safe }}
{% endfor %}
<input type="hidden" name="hidden" value="set-additional-information" />
</form>
<!-- Loading state -->
{% include "components/modals/loading_state.html" with loading_var="loading" %}
<!-- Error state -->
{% include "components/modals/error_state.html" with error_var="error" %}
<!-- Items list -->
<template x-if="!loading">
<div class="space-y-4">
<template x-if="items.length === 0">
<p class="text-base-content/60">
No additional information to update.
</p>
</template>
<template x-for="(item, index) in items" :key="item.uuid">
<div class="card bg-base-200 shadow-sm">
<div class="card-body p-4">
<div
x-data="schemaRenderer({
rjsf: schemas[item.type.toLowerCase()],
data: item.data,
mode: 'edit'
})"
x-init="await init(); $watch('data', (value) => { $dispatch('update-item-data', { uuid: item.uuid, data: value }) }, { deep: true })"
>
{% include "components/schema_form.html" %}
</div>
</div>
</div>
</template>
</div>
</template>
</div>
<!-- Footer -->
@ -49,10 +159,17 @@
<button
type="button"
class="btn btn-primary"
@click="submit('edit-scenario-additional-information-modal-form')"
:disabled="isSubmitting"
@click="submit()"
:disabled="isSubmitting || loading || items.length === 0 || modifiedUuids.size === 0"
>
<span x-show="!isSubmitting">Update</span>
<span x-show="!isSubmitting">
<template x-if="modifiedUuids.size > 0">
<span x-text="`Update (${modifiedUuids.size})`"></span>
</template>
<template x-if="modifiedUuids.size === 0">
<span>No Changes</span>
</template>
</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"