violit 0.0.1__py3-none-any.whl
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.
- violit/__init__.py +3 -0
- violit/app.py +1984 -0
- violit/broadcast.py +690 -0
- violit/broadcast_primitives.py +197 -0
- violit/component.py +38 -0
- violit/context.py +10 -0
- violit/engine.py +33 -0
- violit/state.py +76 -0
- violit/theme.py +749 -0
- violit/widgets/__init__.py +30 -0
- violit/widgets/card_widgets.py +595 -0
- violit/widgets/chart_widgets.py +253 -0
- violit/widgets/chat_widgets.py +221 -0
- violit/widgets/data_widgets.py +529 -0
- violit/widgets/form_widgets.py +421 -0
- violit/widgets/input_widgets.py +745 -0
- violit/widgets/layout_widgets.py +419 -0
- violit/widgets/list_widgets.py +107 -0
- violit/widgets/media_widgets.py +173 -0
- violit/widgets/status_widgets.py +255 -0
- violit/widgets/text_widgets.py +413 -0
- violit-0.0.1.dist-info/METADATA +504 -0
- violit-0.0.1.dist-info/RECORD +26 -0
- violit-0.0.1.dist-info/WHEEL +5 -0
- violit-0.0.1.dist-info/licenses/LICENSE +21 -0
- violit-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Chart Widgets Mixin for Violit"""
|
|
2
|
+
|
|
3
|
+
from typing import Union, Optional, Any, Callable
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import plotly.graph_objects as go
|
|
6
|
+
import plotly.io as pio
|
|
7
|
+
from ..component import Component
|
|
8
|
+
from ..context import rendering_ctx
|
|
9
|
+
from ..state import State
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ChartWidgetsMixin:
|
|
13
|
+
"""Chart widgets (line, bar, area, scatter, plotly, pyplot, etc.)"""
|
|
14
|
+
|
|
15
|
+
def plotly_chart(self, fig: Union[go.Figure, Callable, State], use_container_width=True, render_mode="svg", **props):
|
|
16
|
+
"""Display Plotly chart with Signal/Lambda support"""
|
|
17
|
+
cid = self._get_next_cid("plot")
|
|
18
|
+
|
|
19
|
+
def builder():
|
|
20
|
+
# Handle Signal/Lambda/Direct Figure
|
|
21
|
+
current_fig = fig
|
|
22
|
+
if isinstance(fig, State):
|
|
23
|
+
token = rendering_ctx.set(cid)
|
|
24
|
+
current_fig = fig.value
|
|
25
|
+
rendering_ctx.reset(token)
|
|
26
|
+
elif callable(fig):
|
|
27
|
+
token = rendering_ctx.set(cid)
|
|
28
|
+
current_fig = fig()
|
|
29
|
+
rendering_ctx.reset(token)
|
|
30
|
+
|
|
31
|
+
if current_fig is None:
|
|
32
|
+
return Component("div", id=f"{cid}_wrapper", content="No data")
|
|
33
|
+
|
|
34
|
+
# Force render_mode if requested (default: svg)
|
|
35
|
+
# Convert scattergl to scatter for SVG rendering
|
|
36
|
+
if render_mode == "svg" and hasattr(current_fig, "data"):
|
|
37
|
+
has_scattergl = any(trace.type == 'scattergl' for trace in current_fig.data)
|
|
38
|
+
if has_scattergl:
|
|
39
|
+
# Create new figure with converted traces
|
|
40
|
+
new_traces = []
|
|
41
|
+
for trace in current_fig.data:
|
|
42
|
+
if trace.type == 'scattergl':
|
|
43
|
+
trace_dict = trace.to_plotly_json()
|
|
44
|
+
trace_dict['type'] = 'scatter'
|
|
45
|
+
new_traces.append(go.Scatter(trace_dict))
|
|
46
|
+
else:
|
|
47
|
+
new_traces.append(trace)
|
|
48
|
+
# Recreate figure with new traces and original layout
|
|
49
|
+
current_fig = go.Figure(data=new_traces, layout=current_fig.layout)
|
|
50
|
+
|
|
51
|
+
fj = pio.to_json(current_fig)
|
|
52
|
+
width_style = "width: 100%;" if use_container_width else ""
|
|
53
|
+
html = f'''
|
|
54
|
+
<div id="{cid}" style="{width_style} height: 500px;"></div>
|
|
55
|
+
<script>(function(){{
|
|
56
|
+
const d = {fj};
|
|
57
|
+
if (window.Plotly) {{
|
|
58
|
+
Plotly.newPlot('{cid}', d.data, d.layout, {{responsive: true}});
|
|
59
|
+
}} else {{
|
|
60
|
+
console.error("Plotly not found");
|
|
61
|
+
}}
|
|
62
|
+
}})();</script>
|
|
63
|
+
'''
|
|
64
|
+
return Component("div", id=f"{cid}_wrapper", content=html)
|
|
65
|
+
|
|
66
|
+
self._register_component(cid, builder)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def pyplot(self, fig=None, use_container_width=True, **props):
|
|
70
|
+
"""Display Matplotlib figure"""
|
|
71
|
+
import matplotlib
|
|
72
|
+
matplotlib.use('Agg')
|
|
73
|
+
import matplotlib.pyplot as plt
|
|
74
|
+
import io
|
|
75
|
+
import base64
|
|
76
|
+
|
|
77
|
+
cid = self._get_next_cid("pyplot")
|
|
78
|
+
|
|
79
|
+
def builder():
|
|
80
|
+
current_fig = fig if fig is not None else plt.gcf()
|
|
81
|
+
|
|
82
|
+
buf = io.BytesIO()
|
|
83
|
+
current_fig.savefig(buf, format='png', bbox_inches='tight', dpi=100)
|
|
84
|
+
buf.seek(0)
|
|
85
|
+
img_base64 = base64.b64encode(buf.read()).decode('utf-8')
|
|
86
|
+
buf.close()
|
|
87
|
+
|
|
88
|
+
width_style = "width: 100%;" if use_container_width else ""
|
|
89
|
+
html = f'<img src="data:image/png;base64,{img_base64}" style="{width_style} height: auto;" />'
|
|
90
|
+
return Component("div", id=cid, content=html, class_="pyplot-container")
|
|
91
|
+
|
|
92
|
+
self._register_component(cid, builder)
|
|
93
|
+
|
|
94
|
+
def line_chart(self, data, x=None, y=None, width=None, height=400, use_container_width=True, render_mode="svg", **props):
|
|
95
|
+
"""Display simple line chart"""
|
|
96
|
+
cid = self._get_next_cid("line_chart")
|
|
97
|
+
|
|
98
|
+
def builder():
|
|
99
|
+
x_data, y_data, trace_name = self._parse_chart_data(data, x, y)
|
|
100
|
+
|
|
101
|
+
fig = go.Figure()
|
|
102
|
+
cls = go.Scattergl if render_mode == "webgl" else go.Scatter
|
|
103
|
+
fig.add_trace(cls(x=x_data, y=y_data, mode='lines+markers', name=trace_name))
|
|
104
|
+
fig.update_layout(
|
|
105
|
+
height=height,
|
|
106
|
+
margin=dict(l=0, r=0, t=30, b=0),
|
|
107
|
+
template='plotly_white'
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
fj = pio.to_json(fig)
|
|
111
|
+
container_width = "width: 100%;" if use_container_width else f"width: {width}px;" if width else "width: 100%;"
|
|
112
|
+
html = f'''
|
|
113
|
+
<div id="{cid}" style="{container_width} height: {height}px;"></div>
|
|
114
|
+
<script>(function(){{
|
|
115
|
+
const d = {fj};
|
|
116
|
+
Plotly.newPlot('{cid}', d.data, d.layout, {{responsive: true}});
|
|
117
|
+
}})();</script>
|
|
118
|
+
'''
|
|
119
|
+
return Component("div", id=f"{cid}_wrapper", content=html)
|
|
120
|
+
|
|
121
|
+
self._register_component(cid, builder)
|
|
122
|
+
|
|
123
|
+
def bar_chart(self, data, x=None, y=None, width=None, height=400, use_container_width=True, render_mode="svg", **props):
|
|
124
|
+
"""Display simple bar chart"""
|
|
125
|
+
cid = self._get_next_cid("bar_chart")
|
|
126
|
+
|
|
127
|
+
def builder():
|
|
128
|
+
x_data, y_data, trace_name = self._parse_chart_data(data, x, y)
|
|
129
|
+
|
|
130
|
+
fig = go.Figure()
|
|
131
|
+
fig.add_trace(go.Bar(x=x_data, y=y_data, name=trace_name))
|
|
132
|
+
fig.update_layout(
|
|
133
|
+
height=height,
|
|
134
|
+
margin=dict(l=0, r=0, t=30, b=0),
|
|
135
|
+
template='plotly_white'
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
fj = pio.to_json(fig)
|
|
139
|
+
container_width = "width: 100%;" if use_container_width else f"width: {width}px;" if width else "width: 100%;"
|
|
140
|
+
html = f'''
|
|
141
|
+
<div id="{cid}" style="{container_width} height: {height}px;"></div>
|
|
142
|
+
<script>(function(){{
|
|
143
|
+
const d = {fj};
|
|
144
|
+
Plotly.newPlot('{cid}', d.data, d.layout, {{responsive: true}});
|
|
145
|
+
}})();</script>
|
|
146
|
+
'''
|
|
147
|
+
return Component("div", id=f"{cid}_wrapper", content=html)
|
|
148
|
+
|
|
149
|
+
self._register_component(cid, builder)
|
|
150
|
+
|
|
151
|
+
def area_chart(self, data, x=None, y=None, width=None, height=400, use_container_width=True, render_mode="svg", **props):
|
|
152
|
+
"""Display area chart"""
|
|
153
|
+
cid = self._get_next_cid("area_chart")
|
|
154
|
+
|
|
155
|
+
def builder():
|
|
156
|
+
x_data, y_data, trace_name = self._parse_chart_data(data, x, y)
|
|
157
|
+
|
|
158
|
+
fig = go.Figure()
|
|
159
|
+
cls = go.Scattergl if render_mode == "webgl" else go.Scatter
|
|
160
|
+
fig.add_trace(cls(x=x_data, y=y_data, fill='tozeroy', name=trace_name))
|
|
161
|
+
fig.update_layout(
|
|
162
|
+
height=height,
|
|
163
|
+
margin=dict(l=0, r=0, t=30, b=0),
|
|
164
|
+
template='plotly_white'
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
fj = pio.to_json(fig)
|
|
168
|
+
container_width = "width: 100%;" if use_container_width else f"width: {width}px;" if width else "width: 100%;"
|
|
169
|
+
html = f'''
|
|
170
|
+
<div id="{cid}" style="{container_width} height: {height}px;"></div>
|
|
171
|
+
<script>(function(){{
|
|
172
|
+
const d = {fj};
|
|
173
|
+
Plotly.newPlot('{cid}', d.data, d.layout, {{responsive: true}});
|
|
174
|
+
}})();</script>
|
|
175
|
+
'''
|
|
176
|
+
return Component("div", id=f"{cid}_wrapper", content=html)
|
|
177
|
+
|
|
178
|
+
self._register_component(cid, builder)
|
|
179
|
+
|
|
180
|
+
def scatter_chart(self, data, x=None, y=None, width=None, height=400, use_container_width=True, render_mode="svg", **props):
|
|
181
|
+
"""Display scatter chart"""
|
|
182
|
+
cid = self._get_next_cid("scatter_chart")
|
|
183
|
+
|
|
184
|
+
def builder():
|
|
185
|
+
x_data, y_data, trace_name = self._parse_chart_data(data, x, y)
|
|
186
|
+
|
|
187
|
+
fig = go.Figure()
|
|
188
|
+
cls = go.Scattergl if render_mode == "webgl" else go.Scatter
|
|
189
|
+
fig.add_trace(cls(x=x_data, y=y_data, mode='markers', name=trace_name))
|
|
190
|
+
fig.update_layout(
|
|
191
|
+
height=height,
|
|
192
|
+
margin=dict(l=0, r=0, t=30, b=0),
|
|
193
|
+
template='plotly_white'
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
fj = pio.to_json(fig)
|
|
197
|
+
container_width = "width: 100%;" if use_container_width else f"width: {width}px;" if width else "width: 100%;"
|
|
198
|
+
html = f'''
|
|
199
|
+
<div id="{cid}" style="{container_width} height: {height}px;"></div>
|
|
200
|
+
<script>(function(){{
|
|
201
|
+
const d = {fj};
|
|
202
|
+
Plotly.newPlot('{cid}', d.data, d.layout, {{responsive: true}});
|
|
203
|
+
}})();</script>
|
|
204
|
+
'''
|
|
205
|
+
return Component("div", id=f"{cid}_wrapper", content=html)
|
|
206
|
+
|
|
207
|
+
self._register_component(cid, builder)
|
|
208
|
+
|
|
209
|
+
def bokeh_chart(self, figure, use_container_width=True, **props):
|
|
210
|
+
"""Display Bokeh chart"""
|
|
211
|
+
from bokeh.embed import components
|
|
212
|
+
|
|
213
|
+
cid = self._get_next_cid("bokeh_chart")
|
|
214
|
+
|
|
215
|
+
def builder():
|
|
216
|
+
script, div = components(figure)
|
|
217
|
+
width_style = "width: 100%;" if use_container_width else ""
|
|
218
|
+
html = f'''
|
|
219
|
+
<div style="{width_style}">
|
|
220
|
+
{div}
|
|
221
|
+
{script}
|
|
222
|
+
</div>
|
|
223
|
+
'''
|
|
224
|
+
return Component("div", id=cid, content=html)
|
|
225
|
+
|
|
226
|
+
self._register_component(cid, builder)
|
|
227
|
+
|
|
228
|
+
def _parse_chart_data(self, data, x, y):
|
|
229
|
+
"""Parse chart data into x, y, and trace name"""
|
|
230
|
+
if isinstance(data, pd.DataFrame):
|
|
231
|
+
if x and y:
|
|
232
|
+
x_data = data[x].tolist()
|
|
233
|
+
y_data = data[y].tolist()
|
|
234
|
+
trace_name = y
|
|
235
|
+
elif y:
|
|
236
|
+
x_data = list(range(len(data)))
|
|
237
|
+
y_data = data[y].tolist()
|
|
238
|
+
trace_name = y
|
|
239
|
+
else:
|
|
240
|
+
cols = data.columns.tolist()
|
|
241
|
+
x_data = data[cols[0]].tolist()
|
|
242
|
+
y_data = data[cols[1]].tolist() if len(cols) > 1 else list(range(len(data)))
|
|
243
|
+
trace_name = cols[1] if len(cols) > 1 else 'Value'
|
|
244
|
+
elif isinstance(data, (list, tuple)):
|
|
245
|
+
x_data = list(range(len(data)))
|
|
246
|
+
y_data = list(data)
|
|
247
|
+
trace_name = 'Value'
|
|
248
|
+
else:
|
|
249
|
+
x_data = []
|
|
250
|
+
y_data = []
|
|
251
|
+
trace_name = 'Value'
|
|
252
|
+
|
|
253
|
+
return x_data, y_data, trace_name
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
from typing import Optional, Union, Callable
|
|
2
|
+
from ..component import Component
|
|
3
|
+
from ..context import fragment_ctx
|
|
4
|
+
from ..state import get_session_store
|
|
5
|
+
|
|
6
|
+
class ChatWidgetsMixin:
|
|
7
|
+
"""Chat-related widgets"""
|
|
8
|
+
|
|
9
|
+
def chat_message(self, name: str, avatar: Optional[str] = None):
|
|
10
|
+
"""
|
|
11
|
+
Insert a chat message container.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
name (str): The name of the author (e.g. "user", "assistant").
|
|
15
|
+
avatar (str, optional): The avatar image or emoji.
|
|
16
|
+
"""
|
|
17
|
+
cid = self._get_next_cid("chat_message")
|
|
18
|
+
|
|
19
|
+
class ChatMessageContext:
|
|
20
|
+
def __init__(self, app, message_id, name, avatar):
|
|
21
|
+
self.app = app
|
|
22
|
+
self.message_id = message_id
|
|
23
|
+
self.name = name
|
|
24
|
+
self.avatar = avatar
|
|
25
|
+
self.token = None
|
|
26
|
+
|
|
27
|
+
def __enter__(self):
|
|
28
|
+
# Register builder
|
|
29
|
+
def builder():
|
|
30
|
+
store = get_session_store()
|
|
31
|
+
|
|
32
|
+
# Collected content
|
|
33
|
+
htmls = []
|
|
34
|
+
# Check static
|
|
35
|
+
for cid_child, b in self.app.static_fragment_components.get(self.message_id, []):
|
|
36
|
+
htmls.append(b().render())
|
|
37
|
+
# Check session
|
|
38
|
+
for cid_child, b in store['fragment_components'].get(self.message_id, []):
|
|
39
|
+
htmls.append(b().render())
|
|
40
|
+
|
|
41
|
+
inner_html = "".join(htmls)
|
|
42
|
+
|
|
43
|
+
# determine avatar and background
|
|
44
|
+
bg_color = "transparent"
|
|
45
|
+
avatar_content = ""
|
|
46
|
+
|
|
47
|
+
# Icons handling
|
|
48
|
+
if self.avatar:
|
|
49
|
+
if self.avatar.startswith("http") or self.avatar.startswith("data:"):
|
|
50
|
+
avatar_content = f'<img src="{self.avatar}" style="width:32px;height:32px;border-radius:4px;object-fit:cover;">'
|
|
51
|
+
else:
|
|
52
|
+
avatar_content = f'<div style="width:32px;height:32px;border-radius:4px;background:#eee;display:flex;align-items:center;justify-content:center;font-size:20px;">{self.avatar}</div>'
|
|
53
|
+
else:
|
|
54
|
+
if self.name == "user":
|
|
55
|
+
avatar_content = f'<div style="width:32px;height:32px;border-radius:4px;background:#7C4DFF;color:white;display:flex;align-items:center;justify-content:center;"><sl-icon name="person-fill"></sl-icon></div>'
|
|
56
|
+
bg_color = "rgba(124, 77, 255, 0.05)"
|
|
57
|
+
elif self.name == "assistant":
|
|
58
|
+
avatar_content = f'<div style="width:32px;height:32px;border-radius:4px;background:#FF5252;color:white;display:flex;align-items:center;justify-content:center;"><sl-icon name="robot"></sl-icon></div>'
|
|
59
|
+
bg_color = "rgba(255, 82, 82, 0.05)"
|
|
60
|
+
else:
|
|
61
|
+
initial = self.name[0].upper() if self.name else "?"
|
|
62
|
+
avatar_content = f'<div style="width:32px;height:32px;border-radius:4px;background:#9CA3AF;color:white;display:flex;align-items:center;justify-content:center;font-weight:bold;">{initial}</div>'
|
|
63
|
+
bg_color = "rgba(0,0,0,0.02)"
|
|
64
|
+
|
|
65
|
+
html = f'''
|
|
66
|
+
<div class="chat-message" style="display:flex; gap:16px; margin-bottom:16px; padding:16px; border-radius:8px; background:{bg_color};">
|
|
67
|
+
<div class="chat-avatar" style="flex-shrink:0;">
|
|
68
|
+
{avatar_content}
|
|
69
|
+
</div>
|
|
70
|
+
<div class="chat-content" style="flex:1; min-width:0; overflow-wrap:break-word;">
|
|
71
|
+
{inner_html}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
'''
|
|
75
|
+
return Component("div", id=self.message_id, content=html)
|
|
76
|
+
|
|
77
|
+
self.app._register_component(self.message_id, builder)
|
|
78
|
+
|
|
79
|
+
self.token = fragment_ctx.set(self.message_id)
|
|
80
|
+
return self
|
|
81
|
+
|
|
82
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
83
|
+
if self.token:
|
|
84
|
+
fragment_ctx.reset(self.token)
|
|
85
|
+
|
|
86
|
+
def __getattr__(self, name):
|
|
87
|
+
return getattr(self.app, name)
|
|
88
|
+
|
|
89
|
+
return ChatMessageContext(self, cid, name, avatar)
|
|
90
|
+
|
|
91
|
+
def chat_input(self, placeholder: str = "Your message", on_submit: Optional[Callable[[str], None]] = None, auto_scroll: bool = True):
|
|
92
|
+
"""
|
|
93
|
+
Display a chat input widget at the bottom of the page.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
placeholder (str): Placeholder text.
|
|
97
|
+
on_submit (Callable[[str], None]): Callback function to run when message is sent.
|
|
98
|
+
auto_scroll (bool): If True, automatically scroll to bottom after rendering.
|
|
99
|
+
"""
|
|
100
|
+
cid = self._get_next_cid("chat_input")
|
|
101
|
+
store = get_session_store()
|
|
102
|
+
|
|
103
|
+
# Register action handler
|
|
104
|
+
def handler(val):
|
|
105
|
+
if on_submit:
|
|
106
|
+
on_submit(val)
|
|
107
|
+
|
|
108
|
+
self.static_actions[cid] = handler
|
|
109
|
+
|
|
110
|
+
def builder():
|
|
111
|
+
# Fixed bottom input
|
|
112
|
+
# We use window.lastActiveChatInput to restore focus after re-render/replacement
|
|
113
|
+
scroll_script = "window.scrollTo(0, document.body.scrollHeight);" if auto_scroll else ""
|
|
114
|
+
|
|
115
|
+
html = f'''
|
|
116
|
+
<div class="chat-input-container" style="
|
|
117
|
+
position: fixed;
|
|
118
|
+
bottom: 0;
|
|
119
|
+
left: 0;
|
|
120
|
+
right: 0;
|
|
121
|
+
padding: 20px;
|
|
122
|
+
background: linear-gradient(to top, var(--sl-bg) 80%, transparent);
|
|
123
|
+
z-index: 1000;
|
|
124
|
+
display: flex;
|
|
125
|
+
justify-content: center;
|
|
126
|
+
pointer-events: none;
|
|
127
|
+
">
|
|
128
|
+
<div style="
|
|
129
|
+
width: 100%;
|
|
130
|
+
max-width: 800px;
|
|
131
|
+
background: var(--sl-bg-card);
|
|
132
|
+
border: 1px solid var(--sl-border);
|
|
133
|
+
border-radius: 8px;
|
|
134
|
+
padding: 8px;
|
|
135
|
+
box-shadow: 0 -4px 10px rgba(0,0,0,0.05);
|
|
136
|
+
display: flex;
|
|
137
|
+
gap: 8px;
|
|
138
|
+
pointer-events: auto;
|
|
139
|
+
">
|
|
140
|
+
<input type="text" id="input_{cid}" class="chat-input-box" placeholder="{placeholder}"
|
|
141
|
+
style="
|
|
142
|
+
flex: 1;
|
|
143
|
+
border: none;
|
|
144
|
+
background: transparent;
|
|
145
|
+
padding: 8px;
|
|
146
|
+
font-size: 1rem;
|
|
147
|
+
color: var(--sl-text);
|
|
148
|
+
outline: none;
|
|
149
|
+
"
|
|
150
|
+
onkeydown="if(event.key==='Enter'){{
|
|
151
|
+
window.chatInputWasActive = true;
|
|
152
|
+
{f"sendAction('{cid}', this.value);" if self.mode == 'ws' else f"htmx.ajax('POST', '/action/{cid}', {{values: {{value: this.value}}, swap: 'none'}});"}
|
|
153
|
+
this.value = '';
|
|
154
|
+
}}"
|
|
155
|
+
>
|
|
156
|
+
<sl-button size="small" variant="primary" circle onclick="
|
|
157
|
+
const el = document.getElementById('input_{cid}');
|
|
158
|
+
window.chatInputWasActive = true;
|
|
159
|
+
{f"sendAction('{cid}', el.value);" if self.mode == 'ws' else f"htmx.ajax('POST', '/action/{cid}', {{values: {{value: el.value}}, swap: 'none'}});"}
|
|
160
|
+
el.value = '';
|
|
161
|
+
">
|
|
162
|
+
<sl-icon name="send" label="Send"></sl-icon>
|
|
163
|
+
</sl-button>
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
<!-- Spacer -->
|
|
167
|
+
<div style="height: 100px;"></div>
|
|
168
|
+
<script>
|
|
169
|
+
// Auto-scroll if enabled
|
|
170
|
+
if ("{auto_scroll}" === "True") {{
|
|
171
|
+
setTimeout(() => {{
|
|
172
|
+
window.scrollTo({{
|
|
173
|
+
top: document.documentElement.scrollHeight,
|
|
174
|
+
behavior: 'smooth'
|
|
175
|
+
}});
|
|
176
|
+
}}, 100);
|
|
177
|
+
}}
|
|
178
|
+
|
|
179
|
+
// Restore focus if a chat input was just used
|
|
180
|
+
if (window.chatInputWasActive) {{
|
|
181
|
+
setTimeout(() => {{
|
|
182
|
+
// Find ANY chat input box
|
|
183
|
+
const el = document.querySelector('.chat-input-box');
|
|
184
|
+
if (el) {{
|
|
185
|
+
el.focus();
|
|
186
|
+
}}
|
|
187
|
+
window.chatInputWasActive = false;
|
|
188
|
+
}}, 150);
|
|
189
|
+
}}
|
|
190
|
+
</script>
|
|
191
|
+
'''
|
|
192
|
+
return Component("div", id=cid, content=html)
|
|
193
|
+
|
|
194
|
+
self._register_component(cid, builder)
|
|
195
|
+
|
|
196
|
+
# Return the value just submitted, or None
|
|
197
|
+
# We need to check if this specific component triggered the action in this cycle
|
|
198
|
+
# This is tricky without a dedicated 'current_action_trigger' context.
|
|
199
|
+
# In `App.action`, it calls the handler.
|
|
200
|
+
# If we use `actions` dict in store, it persists.
|
|
201
|
+
# We want `chat_input` to return the value ONLY when it was just submitted.
|
|
202
|
+
|
|
203
|
+
# Hack: Check if this cid matches the latest action if we had that info.
|
|
204
|
+
# Alternative: The user code uses `if prompt := app.chat_input():`.
|
|
205
|
+
# This implies standard rerun logic.
|
|
206
|
+
# If the frontend sent an action for `cid`, `store['actions'][cid]` will be set.
|
|
207
|
+
# We should probably clear it after reading to behave like a one-time event?
|
|
208
|
+
# But if we clear it here, and the script reruns multiple times or checks it multiple times?
|
|
209
|
+
# Usually it's read once per run.
|
|
210
|
+
|
|
211
|
+
val = store['actions'].get(cid)
|
|
212
|
+
|
|
213
|
+
# To prevent stale values on subsequent non-related runs (e.g. other buttons),
|
|
214
|
+
# we ideally need to know 'who' triggered the run.
|
|
215
|
+
# But for now, returning what's in store is the best approximation.
|
|
216
|
+
# If another button is clicked, `store['actions']` might still have this cid's old value
|
|
217
|
+
# if we don't clear it.
|
|
218
|
+
# However, `store['actions']` is persistent in the current `app.py` logic?
|
|
219
|
+
# Let's check app.py action handler.
|
|
220
|
+
|
|
221
|
+
return val
|