[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

@ -0,0 +1,5 @@
<template x-if="{{ error_var }}">
<div class="alert alert-error mb-4">
<span x-text="{{ error_var }}"></span>
</div>
</template>

View File

@ -0,0 +1,5 @@
<template x-if="{{ loading_var }}">
<div class="flex justify-center items-center p-4">
<span class="loading loading-spinner loading-md"></span>
</div>
</template>

View File

@ -0,0 +1,135 @@
{% load static %}
<div>
<!-- Loading state -->
<template x-if="loading">
<div class="flex items-center justify-center p-4">
<span class="loading loading-spinner loading-md"></span>
</div>
</template>
<!-- Error state -->
<template x-if="error">
<div class="alert alert-error mb-4">
<span x-text="error"></span>
</div>
</template>
<!-- Schema form -->
<template x-if="schema && !loading">
<div class="space-y-4">
<!-- Title from schema -->
<template x-if="schema['x-title'] || schema.title">
<h4
class="text-lg font-semibold"
x-text="schema['x-title'] || schema.title"
></h4>
</template>
<!-- Render each field (ordered by ui:order) -->
<template x-for="fieldName in getFieldOrder()" :key="fieldName">
<div>
<!-- Text widget -->
<template
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'text'"
>
<div
x-data="textWidget(fieldName, data, schema, uiSchema, fieldErrors, mode)"
>
{% include "components/widgets/text_widget.html" %}
</div>
</template>
<!-- Textarea widget -->
<template
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'textarea'"
>
<div
x-data="textareaWidget(fieldName, data, schema, uiSchema, fieldErrors, mode)"
>
{% include "components/widgets/textarea_widget.html" %}
</div>
</template>
<!-- Number widget -->
<template
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'number'"
>
<div
x-data="numberWidget(fieldName, data, schema, uiSchema, fieldErrors, mode)"
>
{% include "components/widgets/number_widget.html" %}
</div>
</template>
<!-- Select widget -->
<template
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'select'"
>
<div
x-data="selectWidget(fieldName, data, schema, uiSchema, fieldErrors, mode)"
>
{% include "components/widgets/select_widget.html" %}
</div>
</template>
<!-- Checkbox widget -->
<template
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'checkbox'"
>
<div
x-data="checkboxWidget(fieldName, data, schema, uiSchema, fieldErrors, mode)"
>
{% include "components/widgets/checkbox_widget.html" %}
</div>
</template>
<!-- Interval widget -->
<template
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'interval'"
>
<div
x-data="intervalWidget(fieldName, data, schema, uiSchema, fieldErrors, mode)"
>
{% include "components/widgets/interval_widget.html" %}
</div>
</template>
<!-- PubMed link widget -->
<template
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'pubmed-link'"
>
<div
x-data="pubmedWidget(fieldName, data, schema, uiSchema, fieldErrors, mode)"
>
{% include "components/widgets/pubmed_link_widget.html" %}
</div>
</template>
<!-- Compound link widget -->
<template
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'compound-link'"
>
<div
x-data="compoundWidget(fieldName, data, schema, uiSchema, fieldErrors, mode)"
>
{% include "components/widgets/compound_link_widget.html" %}
</div>
</template>
</div>
</template>
<!-- Submit button (only in edit mode with endpoint) -->
<template x-if="mode === 'edit' && endpoint">
<div class="form-control mt-4">
<button class="btn btn-primary" @click="submit()" :disabled="loading">
<template x-if="loading">
<span class="loading loading-spinner loading-sm"></span>
</template>
<span x-text="loading ? 'Submitting...' : 'Submit'"></span>
</button>
</div>
</template>
</div>
</template>
</div>

View File

@ -0,0 +1,50 @@
{# Checkbox widget - pure HTML template #}
<div class="form-control">
<div class="flex flex-col gap-2 sm:flex-row sm:items-baseline">
<!-- Label -->
<label class="label sm:w-48 sm:shrink-0">
<span
class="label-text"
:class="{
'text-error': hasError,
'text-sm text-base-content/60': isViewMode
}"
x-text="label"
></span>
</label>
<!-- Input column -->
<div class="flex-1">
<!-- Help text -->
<template x-if="helpText">
<div class="label">
<span
class="label-text-alt text-base-content/60"
x-text="helpText"
></span>
</div>
</template>
<!-- View mode -->
<template x-if="isViewMode">
<div class="mt-1">
<span class="text-base" x-text="checked ? 'Yes' : 'No'"></span>
</div>
</template>
<!-- Edit mode -->
<template x-if="isEditMode">
<input type="checkbox" class="checkbox" x-model="checked" />
</template>
<!-- Errors -->
<template x-if="hasError">
<div class="label">
<template x-for="errMsg in errors" :key="errMsg">
<span class="label-text-alt text-error" x-text="errMsg"></span>
</template>
</div>
</template>
</div>
</div>
</div>

View File

@ -0,0 +1,66 @@
{# Compound link widget - pure HTML template #}
<div class="form-control">
<div class="flex flex-col gap-2 sm:flex-row sm:items-baseline">
<!-- Label -->
<label class="label sm:w-48 sm:shrink-0">
<span
class="label-text"
:class="{
'text-error': hasError,
'text-sm text-base-content/60': isViewMode
}"
x-text="label"
></span>
</label>
<!-- Input column -->
<div class="flex-1">
<!-- Help text -->
<template x-if="helpText">
<div class="label">
<span
class="label-text-alt text-base-content/60"
x-text="helpText"
></span>
</div>
</template>
<!-- View mode: display as link -->
<template x-if="isViewMode">
<div class="mt-1">
<template x-if="value">
<a
:href="value"
class="link link-primary break-all"
target="_blank"
x-text="value"
></a>
</template>
<template x-if="!value">
<span class="text-base-content/50"></span>
</template>
</div>
</template>
<!-- Edit mode -->
<template x-if="isEditMode">
<input
type="url"
class="input input-bordered w-full"
:class="{ 'input-error': hasError }"
placeholder="Compound URL"
x-model="value"
/>
</template>
<!-- Errors -->
<template x-if="hasError">
<div class="label">
<template x-for="errMsg in errors" :key="errMsg">
<span class="label-text-alt text-error" x-text="errMsg"></span>
</template>
</div>
</template>
</div>
</div>
</div>

View File

@ -0,0 +1,76 @@
{# Interval widget for range inputs - pure HTML template #}
<div class="form-control">
<div class="flex flex-col gap-2 sm:flex-row sm:items-baseline">
<!-- Label -->
<label class="label sm:w-48 sm:shrink-0">
<span
class="label-text"
:class="{
'text-error': hasError,
'text-sm text-base-content/60': isViewMode
}"
x-text="label"
></span>
</label>
<!-- Input column -->
<div class="flex-1">
<!-- Help text -->
<template x-if="helpText">
<div class="label">
<span
class="label-text-alt text-base-content/60"
x-text="helpText"
></span>
</div>
</template>
<!-- View mode: formatted range with unit -->
<template x-if="isViewMode">
<div class="mt-1">
<span class="text-base" x-text="start"></span>
<span class="text-base-content/60 text-xs" x-show="!isSameValue"
>to</span
>
<span class="text-base" x-text="end" x-show="!isSameValue"></span>
<template x-if="start && end && unit">
<span class="text-xs" x-text="unit"></span>
</template>
</div>
</template>
<!-- Edit mode: two inputs with shared unit badge -->
<template x-if="isEditMode">
<div class="flex gap-2 items-center">
<input
type="number"
class="input input-bordered flex-1"
:class="{ 'input-error': hasError }"
placeholder="Min"
x-model="start"
/>
<span class="text-base-content/60">to</span>
<input
type="number"
class="input input-bordered flex-1"
:class="{ 'input-error': hasError }"
placeholder="Max"
x-model="end"
/>
<template x-if="unit">
<span class="badge badge-ghost badge-lg" x-text="unit"></span>
</template>
</div>
</template>
<!-- Errors -->
<template x-if="hasError">
<div class="label">
<template x-for="errMsg in errors" :key="errMsg">
<span class="label-text-alt text-error" x-text="errMsg"></span>
</template>
</div>
</template>
</div>
</div>
</div>

View File

@ -0,0 +1,66 @@
{# Number input widget - pure HTML template #}
<div class="form-control">
<div class="flex flex-col gap-2 sm:flex-row sm:items-baseline">
<!-- Label -->
<label class="label sm:w-48 sm:shrink-0">
<span
class="label-text"
:class="{
'text-error': hasError,
'text-sm text-base-content/60': isViewMode
}"
x-text="label"
></span>
</label>
<!-- Input column -->
<div class="flex-1">
<!-- Help text -->
<template x-if="helpText">
<div class="label">
<span
class="label-text-alt text-base-content/60"
x-text="helpText"
></span>
</div>
</template>
<!-- View mode: show value with unit -->
<template x-if="isViewMode">
<div class="mt-1">
<span class="text-base" x-text="value"></span>
<template x-if="value && unit">
<span class="text-xs" x-text="unit"></span>
</template>
</div>
</template>
<!-- Edit mode: input with unit suffix -->
<template x-if="isEditMode">
<div :class="unit ? 'join w-full' : ''">
<input
type="number"
:class="unit ? 'input input-bordered join-item flex-1' : 'input input-bordered w-full'"
class:input-error="hasError"
x-model="value"
/>
<template x-if="unit">
<span
class="btn btn-ghost join-item no-animation pointer-events-none"
x-text="unit"
></span>
</template>
</div>
</template>
<!-- Errors -->
<template x-if="hasError">
<div class="label">
<template x-for="errMsg in errors" :key="errMsg">
<span class="label-text-alt text-error" x-text="errMsg"></span>
</template>
</div>
</template>
</div>
</div>
</div>

View File

@ -0,0 +1,66 @@
{# PubMed link widget - pure HTML template #}
<div class="form-control">
<div class="flex flex-col gap-2 sm:flex-row sm:items-baseline">
<!-- Label -->
<label class="label sm:w-48 sm:shrink-0">
<span
class="label-text"
:class="{
'text-error': hasError,
'text-sm text-base-content/60': isViewMode
}"
x-text="label"
></span>
</label>
<!-- Input column -->
<div class="flex-1">
<!-- Help text -->
<template x-if="helpText">
<div class="label">
<span
class="label-text-alt text-base-content/60"
x-text="helpText"
></span>
</div>
</template>
<!-- View mode: display as link -->
<template x-if="isViewMode">
<div class="mt-1">
<template x-if="value && pubmedUrl">
<a
:href="pubmedUrl"
class="link link-primary"
target="_blank"
x-text="value"
></a>
</template>
<template x-if="!value">
<span class="text-base-content/50"></span>
</template>
</div>
</template>
<!-- Edit mode -->
<template x-if="isEditMode">
<input
type="text"
class="input input-bordered w-full"
:class="{ 'input-error': hasError }"
placeholder="PubMed ID"
x-model="value"
/>
</template>
<!-- Errors -->
<template x-if="hasError">
<div class="label">
<template x-for="errMsg in errors" :key="errMsg">
<span class="label-text-alt text-error" x-text="errMsg"></span>
</template>
</div>
</template>
</div>
</div>
</div>

View File

@ -0,0 +1,68 @@
{# Select dropdown widget - pure HTML template #}
<div class="form-control">
<div class="flex flex-col gap-2 sm:flex-row sm:items-baseline">
<!-- Label -->
<label class="label sm:w-48 sm:shrink-0">
<span
class="label-text"
:class="{
'text-error': hasError,
'text-sm text-base-content/60': isViewMode
}"
x-text="label"
></span>
</label>
<!-- Input column -->
<div class="flex-1">
<!-- Help text -->
<template x-if="helpText">
<div class="label">
<span
class="label-text-alt text-base-content/60"
x-text="helpText"
></span>
</div>
</template>
<!-- View mode -->
<template x-if="isViewMode">
<div class="mt-1">
<template x-if="value">
<span class="text-base" x-text="value"></span>
</template>
<template x-if="!value">
<span class="text-base-content/50"></span>
</template>
</div>
</template>
<!-- Edit mode -->
<template x-if="isEditMode">
<select
class="select select-bordered w-full"
:class="{ 'select-error': hasError }"
x-model="value"
>
<option value="" :selected="!value">Select...</option>
<template x-for="opt in options" :key="opt">
<option
:value="opt"
:selected="value === opt"
x-text="opt"
></option>
</template>
</select>
</template>
<!-- Errors -->
<template x-if="hasError">
<div class="label">
<template x-for="errMsg in errors" :key="errMsg">
<span class="label-text-alt text-error" x-text="errMsg"></span>
</template>
</div>
</template>
</div>
</div>
</div>

View File

@ -0,0 +1,60 @@
{# Text input widget - pure HTML template #}
<div class="form-control">
<div class="flex flex-col gap-2 sm:flex-row sm:items-baseline">
<!-- Label -->
<label class="label sm:w-48 sm:shrink-0">
<span
class="label-text"
:class="{
'text-error': hasError,
'text-sm text-base-content/60': isViewMode
}"
x-text="label"
></span>
</label>
<!-- Input column -->
<div class="flex-1">
<!-- Help text -->
<template x-if="helpText">
<div class="label">
<span
class="label-text-alt text-base-content/60"
x-text="helpText"
></span>
</div>
</template>
<!-- View mode -->
<template x-if="isViewMode">
<div class="mt-1">
<template x-if="value">
<span class="text-base" x-text="value"></span>
</template>
<template x-if="!value">
<span class="text-base-content/50"></span>
</template>
</div>
</template>
<!-- Edit mode -->
<template x-if="isEditMode">
<input
type="text"
class="input input-bordered w-full"
:class="{ 'input-error': hasError }"
x-model="value"
/>
</template>
<!-- Errors -->
<template x-if="hasError">
<div class="label">
<template x-for="errMsg in errors" :key="errMsg">
<span class="label-text-alt text-error" x-text="errMsg"></span>
</template>
</div>
</template>
</div>
</div>
</div>

View File

@ -0,0 +1,59 @@
{# Textarea widget - pure HTML template #}
<div class="form-control">
<div class="flex flex-col gap-2 sm:flex-row sm:items-baseline">
<!-- Label -->
<label class="label sm:w-48 sm:shrink-0">
<span
class="label-text"
:class="{
'text-error': hasError,
'text-sm text-base-content/60': isViewMode
}"
x-text="label"
></span>
</label>
<!-- Input column -->
<div class="flex-1">
<!-- Help text -->
<template x-if="helpText">
<div class="label">
<span
class="label-text-alt text-base-content/60"
x-text="helpText"
></span>
</div>
</template>
<!-- View mode -->
<template x-if="isViewMode">
<div class="mt-1">
<template x-if="value">
<p class="text-base whitespace-pre-wrap" x-text="value"></p>
</template>
<template x-if="!value">
<span class="text-base-content/50"></span>
</template>
</div>
</template>
<!-- Edit mode -->
<template x-if="isEditMode">
<textarea
class="textarea textarea-bordered w-full"
:class="{ 'textarea-error': hasError }"
x-model="value"
></textarea>
</template>
<!-- Errors -->
<template x-if="hasError">
<div class="label">
<template x-for="errMsg in errors" :key="errMsg">
<span class="label-text-alt text-error" x-text="errMsg"></span>
</template>
</div>
</template>
</div>
</div>
</div>