vedana-backoffice 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. vedana_backoffice-0.1.0/.gitignore +6 -0
  2. vedana_backoffice-0.1.0/CHANGELOG.md +31 -0
  3. vedana_backoffice-0.1.0/PKG-INFO +10 -0
  4. vedana_backoffice-0.1.0/README.md +0 -0
  5. vedana_backoffice-0.1.0/assets/styles.css +120 -0
  6. vedana_backoffice-0.1.0/pyproject.toml +61 -0
  7. vedana_backoffice-0.1.0/rxconfig.py +9 -0
  8. vedana_backoffice-0.1.0/src/vedana_backoffice/Caddyfile +17 -0
  9. vedana_backoffice-0.1.0/src/vedana_backoffice/__init__.py +0 -0
  10. vedana_backoffice-0.1.0/src/vedana_backoffice/components/__init__.py +0 -0
  11. vedana_backoffice-0.1.0/src/vedana_backoffice/components/etl_graph.py +132 -0
  12. vedana_backoffice-0.1.0/src/vedana_backoffice/components/ui_chat.py +236 -0
  13. vedana_backoffice-0.1.0/src/vedana_backoffice/graph/__init__.py +0 -0
  14. vedana_backoffice-0.1.0/src/vedana_backoffice/graph/build.py +169 -0
  15. vedana_backoffice-0.1.0/src/vedana_backoffice/pages/__init__.py +0 -0
  16. vedana_backoffice-0.1.0/src/vedana_backoffice/pages/chat.py +204 -0
  17. vedana_backoffice-0.1.0/src/vedana_backoffice/pages/etl.py +353 -0
  18. vedana_backoffice-0.1.0/src/vedana_backoffice/pages/eval.py +1006 -0
  19. vedana_backoffice-0.1.0/src/vedana_backoffice/pages/jims_thread_list_page.py +894 -0
  20. vedana_backoffice-0.1.0/src/vedana_backoffice/pages/main_dashboard.py +483 -0
  21. vedana_backoffice-0.1.0/src/vedana_backoffice/py.typed +0 -0
  22. vedana_backoffice-0.1.0/src/vedana_backoffice/start_services.py +39 -0
  23. vedana_backoffice-0.1.0/src/vedana_backoffice/state.py +0 -0
  24. vedana_backoffice-0.1.0/src/vedana_backoffice/states/__init__.py +0 -0
  25. vedana_backoffice-0.1.0/src/vedana_backoffice/states/chat.py +368 -0
  26. vedana_backoffice-0.1.0/src/vedana_backoffice/states/common.py +66 -0
  27. vedana_backoffice-0.1.0/src/vedana_backoffice/states/etl.py +1590 -0
  28. vedana_backoffice-0.1.0/src/vedana_backoffice/states/eval.py +1940 -0
  29. vedana_backoffice-0.1.0/src/vedana_backoffice/states/jims.py +508 -0
  30. vedana_backoffice-0.1.0/src/vedana_backoffice/states/main_dashboard.py +757 -0
  31. vedana_backoffice-0.1.0/src/vedana_backoffice/ui.py +115 -0
  32. vedana_backoffice-0.1.0/src/vedana_backoffice/util.py +71 -0
  33. vedana_backoffice-0.1.0/src/vedana_backoffice/vedana_backoffice.py +23 -0
@@ -0,0 +1,6 @@
1
+ *.py[cod]
2
+ __pycache__/
3
+ assets/external/
4
+ .states
5
+ .web
6
+ *.db
@@ -0,0 +1,31 @@
1
+ # 0.4.0
2
+
3
+ * Splitting ETL pipelines into tabs by new pipeline label
4
+ * Improve data views: server-side pagination, custom styling
5
+ * Add stats to ETL step / data table cards - last run time, row changes, row counts
6
+ * Improve ETL Graph rendering logic
7
+ * Refactor backoffice state.py - split into smaller states per-page
8
+ * Improve pipeline details view in assistant messages
9
+ * Add pipeline logs to assistant message details on Chat page
10
+ * New pipeline tests/evaluation page: view golden dataset, run tests, compare results
11
+ * add LLM model selection to chat page - integration via openrouter
12
+
13
+ # 0.3.0 - 2025.11.20
14
+
15
+ * ETL - added monitoring dashboard on the main page
16
+ * General - UI improvements: layouts, tags filtering in JIMS thread viewer
17
+ * Chat - added total chat cost counter
18
+ * ETL - added row count and last run changes in DataTable view
19
+
20
+ * Add link to Telegram bot if `TELEGRAM_BOT_TOKEN` env is provided
21
+
22
+ # 0.2.0 - 2025.10.29
23
+
24
+ * JIMS thread viewer - add feedback processing flow, UI updates
25
+
26
+ # 0.1.0
27
+
28
+ Initial commit:
29
+ * ETL page - view and select steps / data tables and run Datapipe pipelines
30
+ * Chat page - basic chat UI
31
+ * JIMS thread viewer - view previous conversations, add feedback
@@ -0,0 +1,10 @@
1
+ Metadata-Version: 2.4
2
+ Name: vedana-backoffice
3
+ Version: 0.1.0
4
+ Summary: Reflex-based admin/backoffice UI for Vedana
5
+ Author-email: Andrey Tatarinov <a@tatarinov.co>, Timur Sheydaev <tsheyd@epoch8.co>
6
+ Requires-Python: >=3.12
7
+ Requires-Dist: orjson>=3.11.3
8
+ Requires-Dist: reflex<0.9.0,>=0.8.25
9
+ Requires-Dist: vedana-core
10
+ Requires-Dist: vedana-etl
File without changes
@@ -0,0 +1,120 @@
1
+ .datatable-surface {
2
+ /* Width is controlled by inline styles or parent container */
3
+ display: block;
4
+ }
5
+
6
+ .datatable-surface .gridjs-container {
7
+ width: fit-content;
8
+ min-width: 100%;
9
+ background-color: var(--gray-2);
10
+ border: 1px solid var(--gray-5);
11
+ border-radius: 12px;
12
+ padding: 0.75rem;
13
+ color: var(--gray-12);
14
+ box-shadow: 0 8px 24px rgba(15, 23, 42, 0.12);
15
+ }
16
+
17
+ .datatable-surface .gridjs-wrapper {
18
+ border-radius: 8px;
19
+ overflow: visible;
20
+ }
21
+
22
+ .datatable-surface .gridjs-table {
23
+ width: 100%;
24
+ border-collapse: collapse;
25
+ background-color: transparent;
26
+ }
27
+
28
+ .datatable-surface .gridjs-th,
29
+ .datatable-surface .gridjs-td {
30
+ border-color: var(--gray-5);
31
+ color: var(--gray-12);
32
+ }
33
+
34
+ .datatable-surface .gridjs-th {
35
+ background-color: var(--gray-3);
36
+ font-weight: 600;
37
+ }
38
+
39
+ .datatable-surface .gridjs-tr:nth-child(odd) .gridjs-td {
40
+ background-color: var(--gray-1);
41
+ }
42
+
43
+ .datatable-surface .gridjs-tr:nth-child(even) .gridjs-td {
44
+ background-color: var(--gray-2);
45
+ }
46
+
47
+ .datatable-surface .gridjs-tr:hover .gridjs-td {
48
+ background-color: var(--accent-3);
49
+ transition: background-color 0.2s ease-in-out;
50
+ }
51
+
52
+ .datatable-surface .gridjs-search-input,
53
+ .datatable-surface .gridjs-pagination button {
54
+ background-color: var(--gray-1);
55
+ color: var(--gray-12);
56
+ border: 1px solid var(--gray-5);
57
+ border-radius: 6px;
58
+ padding: 0.35rem 0.65rem;
59
+ }
60
+
61
+ .datatable-surface .gridjs-pagination .gridjs-currentPage {
62
+ background-color: var(--accent-4);
63
+ color: var(--accent-12);
64
+ border-color: transparent;
65
+ }
66
+
67
+ .datatable-surface .gridjs-footer {
68
+ background-color: var(--gray-2);
69
+ border-top: 1px solid var(--gray-5);
70
+ padding: 0.5rem 0.75rem;
71
+ color: var(--gray-12);
72
+ border-radius: 0 0 8px 8px;
73
+ }
74
+
75
+ .datatable-surface .gridjs-footer .gridjs-pages {
76
+ gap: 0.25rem;
77
+ }
78
+
79
+ .datatable-surface .gridjs-footer .gridjs-pagination button {
80
+ background-color: var(--gray-1);
81
+ border-color: var(--gray-5);
82
+ color: var(--gray-12);
83
+ }
84
+
85
+ .datatable-surface .gridjs-footer .gridjs-pagination button:disabled {
86
+ background-color: var(--gray-4);
87
+ color: var(--gray-9);
88
+ border-color: var(--gray-6);
89
+ }
90
+
91
+ .datatable-surface .gridjs-footer .gridjs-pagination .gridjs-currentPage {
92
+ background-color: var(--accent-4);
93
+ color: var(--accent-12);
94
+ border-color: transparent;
95
+ }
96
+
97
+ .datatable-surface .gridjs-th:hover {
98
+ background-color: var(--gray-4);
99
+ color: var(--gray-12);
100
+ }
101
+
102
+ .datatable-surface .gridjs-pagination button:disabled {
103
+ opacity: 0.6;
104
+ }
105
+
106
+ .datatable-surface .gridjs-search-input::placeholder {
107
+ color: var(--gray-10);
108
+ }
109
+
110
+ .datatable-surface .gridjs-pagination .gridjs-pages button.gridjs-spread {
111
+ background-color: var(--gray-1) !important;
112
+ color: var(--gray-12) !important;
113
+ border-color: var(--gray-5) !important;
114
+ cursor: default;
115
+ }
116
+
117
+ .datatable-surface .gridjs-pagination .gridjs-pages button.gridjs-spread:hover {
118
+ background-color: var(--gray-3) !important;
119
+ border-color: var(--gray-6) !important;
120
+ }
@@ -0,0 +1,61 @@
1
+ [project]
2
+ name = "vedana-backoffice"
3
+ dynamic = ["version"]
4
+ description = "Reflex-based admin/backoffice UI for Vedana"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ authors = [
8
+ { name = "Andrey Tatarinov", email = "a@tatarinov.co" },
9
+ { name = "Timur Sheydaev", email = "tsheyd@epoch8.co" },
10
+ ]
11
+
12
+ dependencies = [
13
+ "reflex>=0.8.25,<0.9.0",
14
+ "orjson>=3.11.3",
15
+ "vedana-core",
16
+ "vedana-etl",
17
+ ]
18
+
19
+ [project.scripts]
20
+ vedana-backoffice-with-caddy = "vedana_backoffice.start_services:main"
21
+
22
+ [tool.uv.build.wheel]
23
+ packages = ["vedana_backoffice"]
24
+
25
+ include = [
26
+ { path = "vedana_backoffice/Caddyfile" }
27
+ ]
28
+
29
+ [build-system]
30
+ requires = ["hatchling", "uv-dynamic-versioning"]
31
+ build-backend = "hatchling.build"
32
+
33
+ [tool.hatch.version]
34
+ source = "uv-dynamic-versioning"
35
+
36
+ [tool.uv-dynamic-versioning]
37
+ enable = true
38
+ vcs = "git"
39
+ pattern = "default-unprefixed"
40
+ pattern-prefix = "vedana-"
41
+
42
+ [tool.mypy]
43
+ plugins = ['pydantic.mypy']
44
+
45
+ [dependency-groups]
46
+ dev = [
47
+ "ruff>=0.14.10",
48
+ "mypy>=1.19.0",
49
+ "pytest>=8.4.0",
50
+ "pandas-stubs==2.3.2.250926",
51
+ "types-requests>=2.32.4.20250913",
52
+ ]
53
+
54
+ [tool.ruff]
55
+ line-length = 120
56
+
57
+ [tool.uv-workspace-codegen]
58
+ generate = true
59
+ template_type = ["lib", "publish"]
60
+ generate_standard_pytest_step = false
61
+ typechecker = "mypy"
@@ -0,0 +1,9 @@
1
+ import reflex
2
+
3
+ config = reflex.Config( # type: ignore
4
+ app_name="vedana_backoffice",
5
+ plugins=[
6
+ reflex.plugins.TailwindV3Plugin(),
7
+ reflex.plugins.sitemap.SitemapPlugin(),
8
+ ],
9
+ )
@@ -0,0 +1,17 @@
1
+ :9000
2
+
3
+ encode gzip
4
+
5
+ @backend_routes path /_event/* /ping /_upload /_upload/*
6
+ handle @backend_routes {
7
+ reverse_proxy localhost:8000 {
8
+ header_up Upgrade {http.request.header.upgrade}
9
+ header_up Connection {http.request.header.connection}
10
+ }
11
+ }
12
+
13
+ root * /srv
14
+ route {
15
+ try_files {path} {path}/ /404.html
16
+ file_server
17
+ }
@@ -0,0 +1,132 @@
1
+ import reflex as rx
2
+
3
+ from vedana_backoffice.states.etl import EtlState
4
+
5
+
6
+ def _node_card(node: dict) -> rx.Component:
7
+ is_table = node.get("node_type") == "table" # step for transform, table for table
8
+ return rx.card(
9
+ rx.vstack(
10
+ rx.hstack(
11
+ rx.hstack(
12
+ rx.cond(
13
+ is_table,
14
+ rx.box(),
15
+ rx.badge(node.get("step_type", ""), color_scheme="indigo", variant="soft"),
16
+ ),
17
+ spacing="2",
18
+ ),
19
+ rx.text(node.get("name", ""), weight="medium"),
20
+ justify="between",
21
+ width="100%",
22
+ ),
23
+ rx.text(node.get("labels_str", ""), size="1", color="gray"),
24
+ rx.cond(
25
+ is_table,
26
+ rx.hstack(
27
+ rx.tooltip(rx.text(node.get("last_run", "—"), size="1", color="gray"), content="last update time"),
28
+ rx.hstack(
29
+ rx.tooltip(
30
+ rx.text(node.get("row_count", "—"), size="1", color="gray", weight="bold"),
31
+ content="rows total",
32
+ ),
33
+ rx.tooltip(
34
+ rx.text(node.get("last_add", "—"), size="1", color="green"), content="added during last run"
35
+ ),
36
+ rx.text("/", size="1", color="gray"),
37
+ rx.tooltip(
38
+ rx.text(node.get("last_upd", "—"), size="1", color="gray"),
39
+ content="updated during last run",
40
+ ),
41
+ rx.text("/", size="1", color="gray"),
42
+ rx.tooltip(
43
+ rx.text(node.get("last_rm", "—"), size="1", color="red"), content="deleted during last run"
44
+ ),
45
+ spacing="1",
46
+ ),
47
+ width="100%",
48
+ justify="between",
49
+ ),
50
+ # Step view: show last run time and rows processed
51
+ rx.cond(
52
+ node.get("step_type") != "BatchGenerate", # BatchGenerate has no meta table
53
+ rx.hstack(
54
+ rx.tooltip(
55
+ rx.text(node.get("last_run", "—"), size="1", color="gray"),
56
+ content="last run time (that produced changes)",
57
+ ),
58
+ rx.hstack(
59
+ rx.tooltip(
60
+ rx.text(node.get("rows_processed", 0), size="1", color="gray"),
61
+ content="rows processed in last run",
62
+ ),
63
+ rx.text("/", size="1", color="gray"),
64
+ rx.tooltip(
65
+ rx.text(node.get("total_success", 0), size="1", color="gray", weight="bold"),
66
+ content="rows processed total",
67
+ ),
68
+ rx.cond(
69
+ node.get("has_total_failed", False),
70
+ rx.tooltip(
71
+ rx.text(node.get("total_failed_str", ""), size="1", color="red"),
72
+ content="total failed rows (all time)",
73
+ ),
74
+ rx.box(),
75
+ ),
76
+ spacing="1",
77
+ ),
78
+ width="100%",
79
+ justify="between",
80
+ ),
81
+ rx.box(),
82
+ ),
83
+ ),
84
+ spacing="2",
85
+ width="100%",
86
+ ),
87
+ padding="0.75em",
88
+ style={
89
+ "position": "absolute",
90
+ "left": node.get("left", "0px"),
91
+ "top": node.get("top", "0px"),
92
+ "width": node.get("width", "420px"),
93
+ "height": "auto",
94
+ "border": node.get("border_css", "1px solid #e5e7eb"),
95
+ "overflow": "visible",
96
+ "boxSizing": "border-box",
97
+ },
98
+ variant="surface",
99
+ on_click=rx.cond(
100
+ is_table,
101
+ # no direct return values here and that's ok, handled in state
102
+ EtlState.preview_table(table_name=node.get("name", "")), # type: ignore
103
+ EtlState.toggle_node_selection(index=node.get("index_value")), # type: ignore
104
+ ),
105
+ )
106
+
107
+
108
+ def etl_graph() -> rx.Component:
109
+ svg = rx.box(
110
+ rx.html(EtlState.graph_svg),
111
+ style={
112
+ "position": "absolute",
113
+ "left": 0,
114
+ "top": 0,
115
+ "pointerEvents": "none",
116
+ "width": "100%",
117
+ "height": "100%",
118
+ },
119
+ )
120
+
121
+ nodes_layer = rx.box(
122
+ rx.foreach(EtlState.graph_nodes, _node_card),
123
+ style={
124
+ "position": "absolute",
125
+ "left": 0,
126
+ "top": 0,
127
+ "width": "100%",
128
+ "height": "100%",
129
+ },
130
+ )
131
+
132
+ return rx.box(svg, nodes_layer, style={"position": "relative", "width": "100%", "height": "100%"})
@@ -0,0 +1,236 @@
1
+ import reflex as rx
2
+
3
+
4
+ def render_message_bubble(
5
+ msg: dict,
6
+ on_toggle_details,
7
+ extras: rx.Component | None = None,
8
+ corner_tags_component: rx.Component | None = None,
9
+ ) -> rx.Component: # type: ignore[valid-type]
10
+ """Render a chat-style message bubble.
11
+
12
+ Expects msg dict with keys:
13
+ - content, is_assistant (bool-like), created_at_fmt or created_at
14
+ - has_tech, has_logs, show_details
15
+ - has_models, has_vts, has_cypher, models_str, vts_str, cypher_str (optional)
16
+ - logs_str (optional)
17
+ """
18
+
19
+ tech_block = rx.cond(
20
+ msg.get("has_tech"),
21
+ rx.card(
22
+ rx.vstack(
23
+ rx.cond(
24
+ msg.get("has_models"),
25
+ rx.vstack(
26
+ rx.text("Models", weight="medium"),
27
+ rx.code_block(
28
+ msg.get("models_str", ""),
29
+ font_size="11px",
30
+ language="json",
31
+ # (bug in reflex?) code_block does not pass some custom styling (wordBreak, whiteSpace)
32
+ # https://github.com/reflex-dev/reflex/issues/6051
33
+ code_tag_props={"style": {"whiteSpace": "pre-wrap"}},
34
+ style={
35
+ "display": "block",
36
+ "maxWidth": "100%",
37
+ "boxSizing": "border-box",
38
+ },
39
+ ),
40
+ spacing="1",
41
+ width="100%",
42
+ ),
43
+ ),
44
+ rx.cond(
45
+ msg.get("has_vts"),
46
+ rx.vstack(
47
+ rx.text("VTS Queries", weight="medium"),
48
+ rx.code_block(
49
+ msg.get("vts_str", ""),
50
+ font_size="11px",
51
+ language="python",
52
+ code_tag_props={"style": {"whiteSpace": "pre-wrap"}},
53
+ style={
54
+ "display": "block",
55
+ "maxWidth": "100%",
56
+ "boxSizing": "border-box",
57
+ },
58
+ ),
59
+ spacing="1",
60
+ width="100%",
61
+ ),
62
+ ),
63
+ rx.cond(
64
+ msg.get("has_cypher"),
65
+ rx.vstack(
66
+ rx.text("Cypher Queries", weight="medium"),
67
+ rx.code_block(
68
+ msg.get("cypher_str", ""),
69
+ font_size="11px",
70
+ language="cypher",
71
+ code_tag_props={"style": {"whiteSpace": "pre-wrap"}},
72
+ style={
73
+ "display": "block",
74
+ "maxWidth": "100%",
75
+ "boxSizing": "border-box",
76
+ },
77
+ ),
78
+ spacing="1",
79
+ width="100%",
80
+ ),
81
+ ),
82
+ spacing="2",
83
+ width="100%",
84
+ ),
85
+ padding="0.75em",
86
+ variant="surface",
87
+ ),
88
+ rx.box(),
89
+ )
90
+
91
+ generic_details_block = rx.cond(
92
+ msg.get("generic_meta"),
93
+ rx.code_block(
94
+ msg.get("event_data_str", ""),
95
+ font_size="11px",
96
+ language="json",
97
+ code_tag_props={"style": {"whiteSpace": "pre-wrap"}},
98
+ style={
99
+ "display": "block",
100
+ "maxWidth": "100%",
101
+ "boxSizing": "border-box",
102
+ },
103
+ ),
104
+ rx.box(),
105
+ )
106
+
107
+ logs_block = rx.cond(
108
+ msg.get("has_logs"),
109
+ rx.card(
110
+ rx.vstack(
111
+ rx.text("Logs", weight="medium"),
112
+ rx.scroll_area(
113
+ rx.code_block(
114
+ msg.get("logs_str", ""),
115
+ font_size="11px",
116
+ language="log",
117
+ wrap_long_lines=True,
118
+ style={
119
+ "display": "block",
120
+ "maxWidth": "100%",
121
+ "boxSizing": "border-box",
122
+ },
123
+ code_tag_props={"style": {"whiteSpace": "pre-wrap"}}, # styling workaround
124
+ ),
125
+ type="always",
126
+ scrollbars="vertical",
127
+ style={
128
+ "maxHeight": "25vh",
129
+ "width": "100%",
130
+ },
131
+ ),
132
+ spacing="1",
133
+ width="100%",
134
+ ),
135
+ padding="0.75em",
136
+ width="100%",
137
+ variant="surface",
138
+ ),
139
+ rx.box(),
140
+ )
141
+
142
+ details_block = rx.vstack(
143
+ tech_block,
144
+ generic_details_block,
145
+ logs_block,
146
+ spacing="2",
147
+ width="100%",
148
+ )
149
+
150
+ # Tag badges for feedback
151
+ tags_box = corner_tags_component or rx.box()
152
+
153
+ header_left = rx.hstack(
154
+ # event type label at the very left
155
+ rx.cond(
156
+ msg.get("tag_label", "") != "",
157
+ rx.badge(msg.get("tag_label", ""), variant="soft", size="1", color_scheme="gray"),
158
+ rx.box(),
159
+ ),
160
+ rx.cond(
161
+ msg.get("has_tech") | msg.get("has_logs") | msg.get("generic_meta"), # type: ignore[operator]
162
+ rx.button(
163
+ "Details",
164
+ variant="ghost",
165
+ color_scheme="gray",
166
+ size="1",
167
+ on_click=on_toggle_details, # type: ignore[arg-type]
168
+ ),
169
+ ),
170
+ rx.text(msg.get("created_at", ""), size="1", color="gray"),
171
+ spacing="2",
172
+ align="center",
173
+ )
174
+
175
+ body = rx.vstack(
176
+ rx.hstack(header_left, tags_box, justify="between", width="100%", align="center"), # header
177
+ rx.text(
178
+ msg.get("content", ""),
179
+ style={
180
+ "whiteSpace": "pre-wrap",
181
+ "wordBreak": "break-word",
182
+ },
183
+ ),
184
+ rx.cond(msg.get("show_details"), details_block),
185
+ rx.cond(extras is not None, extras or rx.box()),
186
+ spacing="2",
187
+ width="100%",
188
+ style={
189
+ "maxWidth": "100%",
190
+ },
191
+ )
192
+
193
+ assistant_bubble = rx.card(
194
+ body,
195
+ padding="0.75em",
196
+ style={
197
+ "maxWidth": "70%", # 35 vw is 70% of 50% parent card width in vw terms
198
+ "backgroundColor": "#11182714",
199
+ "border": "1px solid #e5e7eb",
200
+ "borderRadius": "12px",
201
+ "wordBreak": "break-word",
202
+ "overflowX": "hidden",
203
+ },
204
+ )
205
+ user_bubble = rx.card(
206
+ body,
207
+ padding="0.75em",
208
+ style={
209
+ "maxWidth": "70%",
210
+ "backgroundColor": "#3b82f614",
211
+ "border": "1px solid #e5e7eb",
212
+ "borderRadius": "12px",
213
+ "wordBreak": "break-word",
214
+ "overflowX": "hidden",
215
+ },
216
+ )
217
+
218
+ return rx.cond(
219
+ msg.get("is_assistant"),
220
+ rx.hstack(
221
+ rx.avatar(fallback="A", size="2", radius="full"),
222
+ assistant_bubble,
223
+ spacing="2",
224
+ width="100%",
225
+ justify="start",
226
+ align="start",
227
+ ),
228
+ rx.hstack(
229
+ user_bubble,
230
+ rx.avatar(fallback="U", size="2", radius="full"),
231
+ spacing="2",
232
+ width="100%",
233
+ justify="end",
234
+ align="start",
235
+ ),
236
+ )