Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions lumen/ai/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@
from panel.widgets import CodeEditor
from panel_gwalker import GraphicWalker
from panel_material_ui import (
Alert, Button, Checkbox, CircularProgress, Column, FileDownload,
MenuButton,
Alert, Button, Checkbox, CircularProgress, Column, FileDownload, FlexBox,
MenuButton, MenuToggle,
)

from ..base import Component
from ..config import dump_yaml, load_yaml
from ..filters import WidgetFilter
from ..pipeline import Pipeline
from ..transforms.sql import SQLLimit
from ..views.base import Panel, Table, View
Expand Down Expand Up @@ -489,6 +490,61 @@ def export(self, fmt: str) -> str | bytes:
sio.seek(0)
return sio

def _render_editor(self):
editor = super()._render_editor()
self._filters = {}
self._filter_area = FlexBox(width_policy="max")
editor.insert(0, self._filter_area)
return editor

def _add_filter(self, item):
field = item["label"]
if item["toggled"]:
if field in self._filters:
filt = self._filters[field]
else:
self._filters[field] = filt = WidgetFilter(field=field, schema=self.component.schema)
self._filter_area.append(filt.widget)
self.component.add_filter(filt)
Comment on lines +507 to +508
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a filter already exists in self._filters (line 503-504), it gets re-appended to both self._filter_area and self.component.filters (lines 507-508). This could lead to duplicate filters in the UI and the component's filter list. Consider checking if the filter is already active before re-adding it.

Suggested change
self._filter_area.append(filt.widget)
self.component.add_filter(filt)
# Only add the widget and filter if they are not already active
if filt.widget not in self._filter_area:
self._filter_area.append(filt.widget)
if filt not in self.component.filters:
self.component.add_filter(filt)

Copilot uses AI. Check for mistakes.
return
removed_filters = [filt for filt in self.component.filters if filt.field == field]
removed_widgets = [filt.widget for filt in removed_filters]
self._filter_area[:] = [w for w in self._filter_area if w not in removed_widgets]
self.component.filters = [filt for filt in self.component.filters if filt not in removed_filters]

def render_controls(self, task: Task, interface: ChatFeed):
controls = super().render_controls(task, interface)
items = []
for col in self.component.data.columns:
if col not in self.component.schema:
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a comment explaining why columns not in the schema are skipped, as this might not be immediately obvious to future maintainers. For example: # Skip columns without schema information (e.g., internal or derived columns)

Suggested change
if col not in self.component.schema:
if col not in self.component.schema:
# Skip columns without schema information (e.g., internal or derived columns)

Copilot uses AI. Check for mistakes.
continue
col_schema = self.component.schema[col]
col_type = col_schema["type"]
if col_type == "string":
if "enum" in col_schema:
icon = "format_list_bulleted"
elif col_schema.get("format") == "datetime":
icon = "calendar_month"
else:
icon = "text_fields"
elif col_type == "number":
icon = "calculate"
elif col_type == "integer":
icon = "numbers"
else:
icon = "help"
items.append({"label": col, "icon": icon})
filter_controls = MenuToggle(
items=items,
label="Add Filter",
icon="filter_list",
margin=0,
variant="text",
on_click=self._add_filter
)
controls.insert(1, filter_controls)
return controls

def render_explorer(self):
return GraphicWalker(
self.component.param.data,
Expand Down
12 changes: 7 additions & 5 deletions lumen/filters/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -327,15 +327,17 @@ def __init__(self, **params):
'type would be more sensible or raise the max_options. '
)
self.widget.options = options
self.widget.name = self.label
self.widget.visible = self.visible
self.widget.disabled = self.disabled
self.widget.param.update(
disabled=self.disabled,
label=self.label or self.field,
visible=self.visible
)
val = self.value
self.widget.link(self, bidirectional=True, value='value', visible='visible', disabled='disabled')
if val is not None:
self.widget.value = val
self.widget.value = self.widget.param.value.deserialize(val)
elif self.default is not None:
self.widget.value = self.default
self.widget.value = self.widget.param.value.deserialize(self.default)
self._setup_sync()

@classmethod
Expand Down
44 changes: 25 additions & 19 deletions lumen/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
import panel as pn
import param # type: ignore

from panel_material_ui import (
Checkbox, DatePicker, DateRangeSlider, DatetimeInput, DatetimePicker,
FloatInput, FloatSlider, IntInput, IntRangeSlider, IntSlider, LiteralInput,
MultiChoice, RangeSlider, Select, TextInput,
)

from .util import resolve_module_reference


Expand Down Expand Up @@ -30,33 +36,33 @@ class JSONSchema(pn.pane.PaneBase):

_unpack = True

_select_widget = pn.widgets.Select
_multi_select_widget = pn.widgets.MultiSelect
_bounded_number_widget = pn.widgets.FloatSlider
_bounded_number_range_widget = pn.widgets.RangeSlider
_unbounded_number_widget = pn.widgets.FloatInput
_list_select_widget = pn.widgets.MultiSelect
_bounded_int_widget = pn.widgets.IntSlider
_bounded_int_range_widget = pn.widgets.IntRangeSlider
_unbounded_int_widget = pn.widgets.IntInput
_string_widget = pn.widgets.TextInput
_boolean_widget = pn.widgets.Checkbox
_literal_widget = pn.widgets.LiteralInput
_unbounded_date_widget = pn.widgets.DatePicker
_date_range_widget = pn.widgets.DateRangeSlider
_select_widget = Select
_multi_select_widget = MultiChoice
_bounded_number_widget = FloatSlider
_bounded_number_range_widget = RangeSlider
_unbounded_number_widget = FloatInput
_list_select_widget = MultiChoice
_bounded_int_widget = IntSlider
_bounded_int_range_widget = IntRangeSlider
_unbounded_int_widget = IntInput
_string_widget = TextInput
_boolean_widget = Checkbox
_literal_widget = LiteralInput
_unbounded_date_widget = DatePicker
_date_range_widget = DateRangeSlider

# Not available until Panel 0.11
try:
_datetime_range_widget = pn.widgets.DatetimeRangeInput
except Exception:
_datetime_range_widget = pn.widgets.DateRangeSlider
_datetime_range_widget = DateRangeSlider

# Not available until Panel 0.12
try:
_unbounded_datetime_widget = pn.widgets.DatetimePicker
_unbounded_datetime_widget = DatetimePicker
_datetime_range_widget = pn.widgets.DatetimeRangePicker
except Exception:
_unbounded_datetime_widget = pn.widgets.DatetimeInput
_unbounded_datetime_widget = DatetimeInput

def _array_type(self, schema):
if 'items' in schema and not schema.get('additionalItems', True):
Expand Down Expand Up @@ -156,7 +162,7 @@ def _update_widgets_from_schema(self):
values = {} if self.object is None else self.object
widgets = []
for p, schema in self.schema.items():
if self.properties and p not in self.properties:
if p == '__len__' or self.properties and p not in self.properties:
Copy link

Copilot AI Jan 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition p == '__len__' or self.properties and p not in self.properties uses implicit boolean conversion which can be confusing. Consider adding parentheses to make the precedence explicit: p == '__len__' or (self.properties and p not in self.properties)

Suggested change
if p == '__len__' or self.properties and p not in self.properties:
if p == '__len__' or (self.properties and p not in self.properties):

Copilot uses AI. Check for mistakes.
continue
for prop in self._precedence:
if prop in schema:
Expand All @@ -177,7 +183,7 @@ def _update_widgets_from_schema(self):
if isinstance(wtype, str):
wtype = resolve_module_reference(wtype, pn.widgets.Widget)

if isinstance(wtype, pn.widgets.Widget):
if isinstance(wtype, pn.widgets.WidgetBase):
widget = wtype
else:
if p in values:
Expand Down
3 changes: 2 additions & 1 deletion lumen/transforms/sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,8 @@ def _build_filter_conditions(self, conditions: list) -> list:
v2_str = str(v2)

range_filters.append(column_expr.between(SQLLiteral.string(v1_str), SQLLiteral.string(v2_str)))
filters.append(or_(*range_filters))
if range_filters:
filters.append(or_(*range_filters))
else:
# Handle None values separately in lists
non_none_values = [v for v in val if v is not None]
Expand Down
Loading