kirche-einfach/backend/components/kirche/README.md

143 lines
6.2 KiB
Markdown

# Authoring web forms (CustomInputForm)
Flows that should render a web form start with a `CustomInputForm` node
(this package — the `kirche` category in the Langflow palette). The API
derives a JSON Schema + uiSchema from the node's template
(`app/flows/input_schema.py`) and the webapp renders it with RJSF.
## Adding a new form field
1. Open the Langflow authoring UI (http://localhost:7860) and load the
flow from `LANGFLOW_LOAD_FLOWS_PATH` (canonical files are preloaded).
2. Edit the `CustomInputForm` node **in its code editor**: every entry in
its `inputs` list is one web-form field. Add an `lfx.io` input, e.g.
```python
from lfx.io import DropdownInput
DropdownInput(
name="room", # JSON-Schema property key (freely renamable)
display_name="Room", # form label (title)
info="Where should it take place", # help text (description)
options=["Chapel", "Hall"], # dropdown choices (enum)
value="Chapel", # prefill (default)
required=True, # marks the field required
)
```
Supported input types → widgets: `StrInput`/`MessageTextInput` → text,
`MultilineInput` → textarea, `SecretStrInput` → password,
`IntInput` → integer, `FloatInput` → number, `BoolInput` → checkbox,
`DropdownInput` → select, `MultiselectInput` → multi-select,
`SliderInput` → slider (needs `range_spec`), `FileInput` → text stub
(real upload later).
3. Nothing else to change in code: `collect()` is dynamic — it emits
every input except the plumbing ones, so renames, additions, and
removals just work. (The inspector panel can tweak `display_name`,
`info`, `value`, and `options` of existing fields, but `name` and new
fields need the code editor.)
4. **Wire the envelope**: keep the `ChatInput → form_values` edge (hidden
input, `show=False`). Submitted web-form values arrive there as
`{"payload": {...}}` and are merged over the defaults — including keys
the node code does not declare (forward-compatible with renames).
Never make form fields required *with* an incoming edge — that's the
envelope's job.
5. Export the flow (JSON) into `backend/flows/<name>.json` and commit.
The API watcher syncs + deploys it automatically; the form schema is
regenerated per version.
## Branched questionnaires (`visible_if`)
A field can appear only when other fields have certain values — nested
to any depth. Declare it in the code editor with a marker in `info`:
```python
(
DropdownInput(
name="gebaeude",
display_name="Welches Gebäude soll verändert werden?",
options=["Kirche", "Anderes Gebäude"],
value="Kirche",
required=True,
),
)
(
MultiselectInput(
name="bestandteil",
display_name="Welcher Bestandteil der Kirche?",
options=["Glocke", "Orgel", "Innenraum", "Fenster", "Fassade"],
value=[],
info="Mehrfachauswahl möglich [visible_if gebaeude=Kirche]",
required=True,
),
)
(
DropdownInput(
name="glockenmassnahme",
display_name="Welche Maßnahme an der Glocke?",
options=["Reinigung", "Reparatur", "Neuguss"],
value="Reinigung",
info="[visible_if bestandteil=Glocke]",
),
)
```
Syntax: `[visible_if <feld>=<wert>]`, `&` for AND
(`[visible_if a=X & b=Y]`), double quotes for values with spaces
(`[visible_if gebaeude="Anderes Gebäude"]`). Trigger matching by input
type: Dropdown/Text `const`, Bool `const` (`true`/`false`), Multiselect
`contains`, Int/Float exact number. Nesting is automatic: a field whose
trigger is itself conditional renders one level deeper, without limit.
What the parser does with markers:
- The marker is stripped from the shown help text; the field leaves the
top-level schema and lives only in its `allOf`/`if`/`then` branch, so
RJSF hides it until the condition holds. `ui:order` keeps template
order, so new questions appear directly below their trigger.
- `required` on a branched field applies only while visible.
- Values are always kept: hidden fields still validate and are submitted
(stale values can keep deeper branches satisfied until changed).
- Safety is fail-open: unknown triggers (renamed?), unsupported trigger
types, self-references, and trigger cycles keep the field always
visible instead of hiding data. Renaming a trigger means updating its
markers.
See `backend/flows/bauvorhaben.json` for a full 4-level example
(Gebäude → Bestandteil → Maßnahme → Gussjahr, plus sibling branch and
bool trigger).
## Output type: Data (JSON), not Message
`collect()` returns `Data(data={...})` — a real JSON object. Consequences:
- A terminal `ChatOutput` renders it as a fenced ```json block; the run
result text is that block, not raw JSON (unwrap before `json.loads`).
- Components consuming `form_data` should read `.data` (or `get_text()`),
not `.text` like with `Message`.
## Conventions the pipeline relies on
- `required=True` on a template field: `lfx validate --level 4` demands a
value or an edge, so the canonical-flow generator
(`backend/scripts/make_flow_fixtures.py`) writes a single-space
sentinel value for such fields. The form parser treats whitespace-only
values as "no default" — don't ship real whitespace defaults.
- Field key = `name` (stable, freely renamable but must stay unique and
non-empty; collisions get a `_2`/`_3` suffix in the form schema);
label = `display_name`; help = `info`. Renames propagate to downstream
components, which read the JSON keys as-is.
- Unknown future input types degrade to a plain text field in the web
form (parser fallback), so adding exotic lfx inputs never breaks runs.
## Regenerating the bundled fixtures
After editing the component source (`form_inputs.py`), refresh the
canonical + test-fixture exports:
docker compose exec api /opt/venv/bin/python scripts/make_flow_fixtures.py
docker compose exec -T api uv run pytest tests/test_lfx_validate.py
Notes: the generator writes UTF-8/LF explicitly (Windows-safe), and
`backend/flows/*.json` may carry Langflow round-trip state (key order)
the generator does not reproduce — if a regen rewrites canonical files
beyond the component's own node, revert them and splice only the changed
node fields (code, description, output types, edge handles) instead.