This guide explains every way a user can extend PraisonAIUI — and which approach fits your use case.
| I want to add… | Use | Side | Effort |
|----------------|-----|------|--------|
| A new dashboard page | @aiui.page() | Server | 5 lines |
@aiui.page()
| A form that saves data | aiui.form_action() + @aiui.register_page_action() | Both | 15 lines |
aiui.form_action()
@aiui.register_page_action()
| A brand-new UI component type | window.aiui.registerComponent() + dict type | Both | 20 lines |
window.aiui.registerComponent()
| A page rendered entirely in the browser | window.aiui.registerView() | Client | 10 lines |
window.aiui.registerView()
| Ship client-side JS from Python | aiui.set_custom_js(path) | Server | 1 line |
aiui.set_custom_js(path)
| A typed contract for a component | aiui.register_component_schema(type, schema) | Server | 5 lines |
aiui.register_component_schema(type, schema)
| A custom theme | aiui.register_theme() | Server | 1 line |
aiui.register_theme()
| A new AI backend | Subclass aiui.BaseProvider | Server | ~50 lines |
aiui.BaseProvider
| A whole new feature module (routes, state) | Subclass BaseFeatureProtocol + register_feature() | Server | ~100 lines |
BaseFeatureProtocol
register_feature()
| Persistence backend | Subclass aiui.BaseDataStore | Server | ~50 lines |
aiui.BaseDataStore
Goal: Show a new page in the sidebar with custom content.
What happens:
1. The page is registered in the server's page registry.
2. It appears in the sidebar at /api/pages.
/api/pages
3. When the user clicks it, the frontend fetches /api/pages/analytics/data.
/api/pages/analytics/data
4. Your handler returns a dict; aiui.layout() wraps it in {"_components": [...]}.
aiui.layout()
{"_components": [...]}
5. dashboard.js loops over _components and calls renderComponent(comp) on each.
dashboard.js
_components
renderComponent(comp)
See Dashboard for the full @aiui.page() signature.
---
Goal: Let users submit data from the UI back to your Python code.
1. The form renders with all inputs.
2. On submit, the frontend POSTs to /api/pages/contact/action.
/api/pages/contact/action
3. The server invokes handle_contact(data) with the form values keyed by label.
handle_contact(data)
4. The returned dict is sent back to the browser as JSON.
> Note: Input labels become dict keys. Use distinct labels per input.
Goal: Add a new UI widget that doesn't exist in the 48 built-ins.
If you stop here, the component renders as formatted JSON (graceful fallback).
Put this in a plugin.js file (or paste into DevTools Console):
plugin.js
1. renderComponent(comp) checks COMPONENT_REGISTRY[comp.type] before the built-in switch.
COMPONENT_REGISTRY[comp.type]
2. Your renderer takes priority and returns a DOM element.
Use aiui.set_custom_js(path) to ship client-side extensions directly from
your app:
The script runs after the plugin loader, so window.aiui.registerView
window.aiui.registerView
and window.aiui.registerComponent are ready when it executes. No DevTools
window.aiui.registerComponent
workaround needed.
Goal: A page rendered entirely in the browser — no server round-trip on navigation.
Still register a placeholder page in Python so it appears in the sidebar:
1. registerView takes priority over the built-in + server-side rendering.
registerView
2. Navigation calls your render(container) function.
render(container)
3. When the user navigates away, your cleanup() function runs.
cleanup()
See CSS Architecture for all variables.
See Providers.
Features live in praisonaiui/features/*.py and implement BaseFeatureProtocol:
praisonaiui/features/*.py
See Protocol Architecture.
window.aiui.registerView/Component
| Gap | Resolution |
|-----|------------|
| No aiui.set_custom_js(path) | Implemented — serves the file at /custom.js and injects a <script> tag after the plugin loader. Unit tests: tests/unit/test_custom_js.py (10 tests). |
/custom.js
<script>
tests/unit/test_custom_js.py
| form_action uses input labels as keys | Implemented — every form input now accepts an optional name= kwarg that becomes the submit-key. Tests: tests/unit/test_form_input_names.py (18 tests). |
form_action
name=
tests/unit/test_form_input_names.py
| No typed Python ⇄ JS contract | Implemented — aiui.register_component_schema(type, schema) + GET /api/components/schemas. Built-in schemas are auto-derived from ui.py signatures. Tests: tests/unit/test_component_schemas.py (9 tests). |
GET /api/components/schemas
ui.py
tests/unit/test_component_schemas.py
| /api/features cold-start slow | Mitigated — info() calls are parallelized via asyncio.gather. Warm calls are now instant; cold starts are dominated by LiteLLM import (out of scope). |
/api/features
info()
asyncio.gather
| pygments 2.20 mkdocs build crash on providers.md | Fixed — docs_hooks.py patches pymdownx.BlockHtmlFormatter.__init__ to coerce filename=None → "". |
pygments 2.20
providers.md
docs_hooks.py
pymdownx.BlockHtmlFormatter.__init__
filename=None
""
aiui init --frontend
@praisonaiui/runtime
tests/unit/test_form_action.py
examples/python/
docs/features/
docs/api/
mkdocs build --strict
pytest tests/unit -v