206 lines
8.9 KiB
Markdown
206 lines
8.9 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).
|
|
|
|
## User-built form components (no kirche base needed)
|
|
|
|
A plain `CustomComponent` whose display name ends with `"Form"` is also
|
|
parsed into a web form — same template contract, no base class. To make
|
|
submitted values reach it at runtime, add an envelope input and merge it
|
|
in the output method (pattern):
|
|
|
|
```python
|
|
from lfx.io import MessageTextInput # add to imports
|
|
import json
|
|
|
|
# add to inputs:
|
|
MessageTextInput(name="form_values", display_name="Form values", value="", show=False)
|
|
|
|
# at the top of the output method:
|
|
try:
|
|
raw = self.form_values.text if hasattr(self.form_values, "text") else self.form_values
|
|
submitted = json.loads(raw or "{}")
|
|
except Exception:
|
|
submitted = {}
|
|
if isinstance(submitted.get("payload"), dict):
|
|
submitted = submitted["payload"]
|
|
|
|
|
|
def val(name, default):
|
|
return submitted.get(name, default)
|
|
```
|
|
|
|
Then use `val("rolle", self.rolle)` etc., and wire a `ChatInput` node
|
|
into `form_values` on the canvas. Branch markers (`[visible_if ...]`
|
|
in `info`) work here too — the parser reads the template only.
|
|
|
|
## Asked fields only (run envelope `fields`)
|
|
|
|
The API sends the asked field names (from the version's form schema)
|
|
in the run envelope's `fields` list. `collect()` then emits defaults
|
|
only for asked AND currently visible fields (branch conditions are
|
|
evaluated against submitted values over code defaults, so hidden
|
|
fields stay out exactly as the frontend never submits them): the
|
|
template may declare fields unknown to this code (they arrive via
|
|
envelope and merge normally), and unasked code defaults stay out of
|
|
the output. Without the list — raw dicts, Langflow playground — all
|
|
visible declared defaults apply as before.
|
|
|
|
## Variant flows (own component per flow)
|
|
|
|
A flow whose fields differ from `CustomInputForm` gets its own
|
|
standalone component (see `bauvorhaven_form.py` → "Bauvorhaben Form"
|
|
in the palette): same class pattern, own `inputs` list. Standalone
|
|
means it: no imports between form modules — the LFX runtime executes
|
|
the embedded module source without our `kirche` package, so every form
|
|
component must be self-contained (stdlib + `lfx` only). Copy the file
|
|
as the starting point for future forms.
|
|
|
|
Rule: code-declared inputs must equal template fields
|
|
(`test_embedded_code_matches_template_fields` enforces it; required
|
|
Multiselect uses the `[" "]` sentinel). Generate node + flow with
|
|
`kirche_node(..., component_cls, code_override)` and
|
|
`build_bauvorhaben()` in `backend/scripts/make_flow_fixtures.py`
|
|
rather than hand-editing JSON. Restart Langflow afterwards so the
|
|
palette picks up the new component, then build/upload the flow.
|
|
|
|
## 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 (`[" "]` for Multiselect, which rejects
|
|
a plain string at runtime). 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.
|