Skip to content

Commit 84605f8

Browse files
authored
Add HTML export option for Reports (#1798)
1 parent 75a4a40 commit 84605f8

2 files changed

Lines changed: 75 additions & 15 deletions

File tree

lumen/ai/report.py

Lines changed: 52 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
from panel.viewable import Viewable, Viewer
2424
from panel_material_ui import (
2525
Accordion, BreakpointSwitcher, Button, Card, ChatFeed, ChatMessage,
26-
Container, Dialog, Divider, FileDownload, IconButton, Progress, Select,
27-
SpeedDial, TextAreaInput, TextInput, Typography,
26+
Container, Dialog, Divider, FileDownload, IconButton, MenuButton, Progress,
27+
Select, SpeedDial, TextAreaInput, TextInput, Typography,
2828
)
2929

3030
from ..views.base import Panel, View
@@ -633,6 +633,18 @@ def to_notebook(self):
633633
cells = make_preamble("", extensions=extensions) + cells
634634
return write_notebook(cells)
635635

636+
def to_html(self):
637+
"""
638+
Returns the HTML representation of the report.
639+
"""
640+
if len(self) and self.status != "success":
641+
raise RuntimeError(
642+
"Report has not been executed, run report before exporting to html."
643+
)
644+
buf = io.StringIO()
645+
self._view.save(buf, title=self.title or "Report")
646+
return buf.getvalue()
647+
636648
def validate(
637649
self,
638650
context: TContext | None = None,
@@ -869,13 +881,34 @@ def _init_view(self):
869881
icon="settings", on_click=self._open_settings, size="large", color="default",
870882
margin=0, description="Configure Report", visible=False
871883
)
872-
self._export = IconButton(
873-
icon="get_app", on_click=self._trigger_download, size="large", color="default",
874-
margin=0, description="Export Report to .ipynb", visible=False
884+
self._export = MenuButton(
885+
label="", icon="get_app", variant="text", color="default",
886+
margin=0, size="large", visible=False,
887+
description="Export Report",
888+
items=[
889+
{"label": "Notebook (.ipynb)", "format": "ipynb", "icon": "description"},
890+
{"label": "HTML (.html)", "format": "html", "icon": "language"},
891+
],
892+
on_click=lambda _: self._download._transfer(),
893+
icon_size="36px",
894+
sx={
895+
"& .MuiButton-endIcon": {"display": "none"},
896+
"& .MuiButton-startIcon": {"margin": 0},
897+
"& .MuiIcon-root": {"color": "var(--mui-palette-action-active)"},
898+
"minWidth": "unset",
899+
"width": "60px",
900+
"height": "60px",
901+
"borderRadius": "50%",
902+
"padding": 0,
903+
},
875904
)
876905
self._download = FileDownload(
877-
callback=self._notebook_export, filename=f"{self.title or 'Report'}.ipynb", visible=False
906+
auto=True,
907+
callback=param.bind(self._export_report, self._export),
908+
filename=f"{self.title or 'Report'}.ipynb",
909+
visible=False,
878910
)
911+
self._export.attached.append(self._download)
879912
self._dialog = Dialog(
880913
TextInput.from_param(self.param.title, margin=(10, 0, 0, 0), sizing_mode="stretch_width"),
881914
show_close_button=True,
@@ -889,14 +922,14 @@ def _init_view(self):
889922
self._collapse,
890923
self._export,
891924
self._settings,
892-
self._download,
893925
sizing_mode="stretch_width"
894926
)
895927
self._dial = SpeedDial(
896928
items=[
897929
{"label": "Execute Report", "icon": "play_arrow"},
898930
{"label": "Clear Report", "icon": "clear"},
899-
{"label": "Export Report to Notebook", "icon": "get_app"},
931+
{"label": "Export as Notebook", "icon": "description", "format": "ipynb"},
932+
{"label": "Export as HTML", "icon": "language", "format": "html"},
900933
{"label": "Configure Report", "icon": "settings"}
901934
],
902935
color="default",
@@ -934,15 +967,13 @@ async def _trigger_event(self, item: dict):
934967
await self._execute_event()
935968
elif icon == "clear":
936969
self.reset()
937-
elif icon == "get_app":
938-
self._trigger_download()
970+
elif icon in ("description", "language"):
971+
fmt = item.get("format", "ipynb")
972+
self._export.value = {"format": fmt}
973+
self._download._transfer()
939974
elif icon == "settings":
940975
self._open_settings()
941976

942-
def _trigger_download(self, event=None):
943-
self._download.filename = f"{self.title or 'Report'}.ipynb"
944-
self._download.transfer()
945-
946977
@param.depends('_current', '_tasks', watch=True)
947978
def _update_run_state(self):
948979
# Play button is always enabled - users can re-run after changing settings
@@ -972,9 +1003,15 @@ async def _execute_event(self, event=None):
9721003
await asyncio.sleep(0.01) # yield the event loop to allow button loading state to update
9731004
await self.execute()
9741005

975-
async def _notebook_export(self):
1006+
async def _export_report(self, item=None):
9761007
if len(self) and self.status != "success":
9771008
await self.execute()
1009+
fmt = item.get("format", "ipynb") if isinstance(item, dict) else "ipynb"
1010+
title = self.title or "Report"
1011+
ext = "html" if fmt == "html" else "ipynb"
1012+
self._download.filename = f"{title}.{ext}"
1013+
if fmt == "html":
1014+
return io.StringIO(self.to_html())
9781015
return io.StringIO(self.to_notebook())
9791016

9801017
def _expand_all(self, event=None):

lumen/tests/ai/test_report.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,3 +369,26 @@ async def test_report_to_notebook():
369369

370370
assert cell3['cell_type'] == 'markdown'
371371
assert cell3['source'] == ["**Hello**"]
372+
373+
374+
async def test_report_to_html():
375+
report = Report(
376+
Section(
377+
HelloAction()
378+
),
379+
title='Hello Report'
380+
)
381+
382+
with pytest.raises(RuntimeError):
383+
report.to_html()
384+
385+
await report.execute()
386+
387+
assert len(report.views) == 2
388+
389+
html_string = report.to_html()
390+
391+
assert isinstance(html_string, str)
392+
assert "<html" in html_string.lower()
393+
assert "Hello Report" in html_string
394+
assert "Hello" in html_string

0 commit comments

Comments
 (0)