pylogue 0.2.1__py3-none-any.whl → 0.3.29__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.
@@ -5,7 +5,12 @@ __all__ = ['spinner_style', 'user_messages', 'echo_responder', 'get_initial_mess
5
5
 
6
6
  # %% ../../nbs/1-Chat.ipynb 1
7
7
  from fasthtml.common import *
8
- from .cards import ChatCard, render_chat_list, mk_inp
8
+ import asyncio
9
+ import inspect
10
+ from monsterui.all import Theme, Container, ContainerT, Card, CardT, TextPresets, Button, ButtonT
11
+ from .cards import render_chat_list, mk_inp
12
+ from .design_system import get_color
13
+ from .renderer import ChatRenderer
9
14
 
10
15
  # %% ../../nbs/1-Chat.ipynb 2
11
16
  async def echo_responder(text: str) -> str:
@@ -15,20 +20,20 @@ async def echo_responder(text: str) -> str:
15
20
 
16
21
 
17
22
  spinner_style = Style(
18
- """
19
- .spinner {
23
+ f"""
24
+ .spinner {{
20
25
  display: inline-block;
21
26
  width: 20px;
22
27
  height: 20px;
23
- border: 3px solid rgba(0, 0, 0, 0.1);
24
- border-top-color: #333;
28
+ border: 3px solid {get_color("spinner_light")};
29
+ border-top-color: {get_color("light_text")};
25
30
  border-radius: 50%;
26
31
  animation: spin 1s linear infinite;
27
- }
28
-
29
- @keyframes spin {
30
- to { transform: rotate(360deg); }
31
- }
32
+ }}
33
+
34
+ @keyframes spin {{
35
+ to {{ transform: rotate(360deg); }}
36
+ }}
32
37
  """
33
38
  )
34
39
 
@@ -57,34 +62,56 @@ def on_disconn(ws):
57
62
 
58
63
 
59
64
  def create_chat_app(rt, responder=None):
60
- app = FastHTML(
61
- exts="ws",
62
- hdrs=(
65
+ renderer = ChatRenderer()
66
+ headers = list(Theme.blue.headers())
67
+ headers.extend(
68
+ [
69
+ Link(rel="preconnect", href="https://fonts.googleapis.com"),
70
+ Link(rel="preconnect", href="https://fonts.gstatic.com", crossorigin="anonymous"),
71
+ Link(
72
+ rel="stylesheet",
73
+ href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap",
74
+ ),
75
+ ]
76
+ )
77
+ headers.extend(
78
+ [
63
79
  MarkdownJS(),
64
80
  HighlightJS(langs=["python", "javascript", "html", "css"]),
65
81
  spinner_style,
66
- confirm_script,
67
- ),
82
+ Style(renderer.get_styles()),
83
+ ]
68
84
  )
85
+ app = FastHTML(exts="ws", hdrs=tuple(headers))
69
86
  rt = app.route
70
87
  responder = echo_responder if responder is None else responder
71
-
72
88
  @rt("/")
73
89
  def home():
74
90
  return (
75
91
  Title("Supply Chain Analyst Chat"),
76
- Div(
77
- H1("Supply Chain RCA", style="text-align: center; padding: 1em;"),
78
- render_chat_list(get_initial_messages()),
79
- Form(
80
- mk_inp(),
81
- id="form",
82
- ws_send=True,
83
- style="display: flex; justify-content: center; margin-top: 20px; padding: 20px;",
92
+ Meta(name="viewport", content="width=device-width, initial-scale=1.0"),
93
+ Body(
94
+ Container(
95
+ Card(
96
+ Div(render_chat_list(get_initial_messages()), cls="chat-scroll"),
97
+ footer=Form(
98
+ mk_inp(),
99
+ Button("Send", cls=("chat-send", ButtonT.primary), type="submit"),
100
+ id="form",
101
+ ws_send=True,
102
+ cls="chat-form",
103
+ ),
104
+ header=Div(
105
+ H2("Supply Chain RCA", cls="text-2xl font-semibold"),
106
+ P("Streaming analysis with a smarter interface.", cls=TextPresets.muted_sm),
107
+ cls="space-y-2",
108
+ ),
109
+ body_cls="space-y-4",
110
+ cls=("chat-shell", CardT.default),
111
+ ),
112
+ cls=("pylogue-container", "mt-10", ContainerT.xl),
84
113
  ),
85
- hx_ext="ws",
86
- ws_connect="/ws",
87
- style=f"font-family: monospace, sans-serif; margin: 0; padding: 0; background: {bg_color}; min-height: 100vh;",
114
+ cls="pylogue-app",
88
115
  ),
89
116
  )
90
117
 
@@ -7,6 +7,7 @@ __all__ = ['ChatAppConfig', 'ChatApp', 'create_default_chat_app', 'example_respo
7
7
  from typing import Optional, Callable, List, Dict, Any
8
8
  from dataclasses import dataclass, field
9
9
  from fasthtml.common import *
10
+ from monsterui.all import Theme, Container, ContainerT, Card, CardT, TextPresets
10
11
  import asyncio
11
12
  import inspect
12
13
 
@@ -14,6 +15,7 @@ from .session import SessionManager, InMemorySessionManager, ChatSession, Messag
14
15
  from .service import ChatService, Responder, ErrorHandler
15
16
  from .renderer import ChatRenderer
16
17
  from .cards import ChatCard
18
+ from .design_system import get_color, get_spacing, get_typography
17
19
 
18
20
  # %% ../../nbs/4-ChatApp.ipynb 3
19
21
  @dataclass
@@ -27,11 +29,18 @@ class ChatAppConfig:
27
29
  # Initial messages
28
30
  initial_messages_factory: Optional[Callable[[], List[Message]]] = None
29
31
 
30
- # Styling
31
- bg_color: str = "#1a1a1a"
32
- header_style: str = "text-align: center; padding: 1em; color: white;"
32
+ # Styling (using design system defaults)
33
+ bg_color: str = None
34
+ header_style: str = None
33
35
  container_style: Optional[str] = None
34
36
 
37
+ def __post_init__(self):
38
+ """Initialize with design system defaults if not provided."""
39
+ if self.bg_color is None:
40
+ self.bg_color = get_color("dark_bg")
41
+ if self.header_style is None:
42
+ self.header_style = f"text-align: center; padding: {get_spacing('md')}; color: {get_color('light_text')};"
43
+
35
44
  # WebSocket settings
36
45
  ws_endpoint: str = "/ws"
37
46
  chat_endpoint: str = "/chat"
@@ -57,24 +66,24 @@ class ChatAppConfig:
57
66
  ]
58
67
 
59
68
  def get_spinner_style(self) -> str:
60
- """Get spinner CSS styles."""
69
+ """Get spinner CSS styles (using design system)."""
61
70
  if self.spinner_css:
62
71
  return self.spinner_css
63
72
 
64
- return """
65
- .spinner {
73
+ return f"""
74
+ .spinner {{
66
75
  display: inline-block;
67
76
  width: 20px;
68
77
  height: 20px;
69
- border: 3px solid rgba(255, 255, 255, 0.3);
70
- border-top-color: #fff;
78
+ border: 3px solid {get_color("spinner_light")};
79
+ border-top-color: {get_color("light_text")};
71
80
  border-radius: 50%;
72
81
  animation: spin 1s linear infinite;
73
- }
74
-
75
- @keyframes spin {
76
- to { transform: rotate(360deg); }
77
- }
82
+ }}
83
+
84
+ @keyframes spin {{
85
+ to {{ transform: rotate(360deg); }}
86
+ }}
78
87
  """
79
88
 
80
89
  # %% ../../nbs/4-ChatApp.ipynb 4
@@ -108,7 +117,18 @@ class ChatApp:
108
117
 
109
118
  def _create_fasthtml_app(self) -> FastHTML:
110
119
  """Create and configure FastHTML application."""
111
- headers = []
120
+ headers = list(Theme.blue.headers())
121
+
122
+ headers.extend(
123
+ [
124
+ Link(rel="preconnect", href="https://fonts.googleapis.com"),
125
+ Link(rel="preconnect", href="https://fonts.gstatic.com", crossorigin="anonymous"),
126
+ Link(
127
+ rel="stylesheet",
128
+ href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap",
129
+ ),
130
+ ]
131
+ )
112
132
 
113
133
  # Add markdown support
114
134
  if self.config.markdown_enabled:
@@ -121,6 +141,8 @@ class ChatApp:
121
141
  # Add spinner styles
122
142
  headers.append(Style(self.config.get_spinner_style()))
123
143
 
144
+ headers.append(Style(self.renderer.get_styles()))
145
+
124
146
  return FastHTML(exts="ws", hdrs=tuple(headers))
125
147
 
126
148
  def _get_initial_messages(self) -> List[Message]:
@@ -133,22 +155,44 @@ class ChatApp:
133
155
  """Register HTTP and WebSocket routes."""
134
156
 
135
157
  @self.app.route(self.config.chat_endpoint)
136
- def home():
158
+ def chat():
137
159
  """Main chat interface."""
138
160
  initial_messages = self._get_initial_messages()
139
161
 
140
- container_style = self.config.container_style or (
141
- f"font-family: monospace, sans-serif; margin: 0; padding: 0; "
142
- f"background: {self.config.bg_color}; min-height: 100vh;"
143
- )
144
-
145
162
  return (
146
163
  Title(self.config.page_title),
147
- Div(
148
- H1(self.config.app_title, style=self.config.header_style),
149
- self.renderer.render_messages(initial_messages),
150
- self.renderer.render_form(),
151
- style=container_style,
164
+ Meta(name="viewport", content="width=device-width, initial-scale=1.0"),
165
+ Body(
166
+ Container(
167
+ Div(
168
+ Div(
169
+ P("PYLOGUE", cls="tracking-widest text-xs uppercase text-emerald-200/70"),
170
+ H1(self.config.app_title, cls="text-4xl md:text-5xl font-semibold tracking-tight"),
171
+ P(
172
+ "Streaming chat UI powered by MonsterUI + FastHTML.",
173
+ cls=TextPresets.muted_sm,
174
+ ),
175
+ cls="space-y-3 mb-10",
176
+ ),
177
+ Card(
178
+ Div(
179
+ self.renderer.render_messages(initial_messages),
180
+ cls="chat-scroll",
181
+ ),
182
+ footer=self.renderer.render_form(),
183
+ header=Div(
184
+ H2("Live Conversation", cls="text-2xl font-semibold"),
185
+ P("Messages update in real time as the agent streams.", cls=TextPresets.muted_sm),
186
+ cls="space-y-2",
187
+ ),
188
+ body_cls="space-y-4",
189
+ cls=("chat-shell", CardT.default),
190
+ ),
191
+ cls="pylogue-layout",
192
+ ),
193
+ cls=("pylogue-container", "mt-10", ContainerT.xl),
194
+ ),
195
+ cls="pylogue-app",
152
196
  ),
153
197
  )
154
198
 
@@ -211,7 +255,7 @@ class ChatApp:
211
255
  assistant_msg.id, content=full_response, pending=False
212
256
  )
213
257
  # Send updated message list to UI
214
- print(f"📤 Sending chunk #{chunk_count}: {repr(chunk)}") # Debug
258
+ # print(f"📤 Sending chunk #{chunk_count}: {repr(chunk)}") # Debug
215
259
  await send(self.renderer.render_messages(session.get_messages()))
216
260
 
217
261
  except Exception as e:
@@ -0,0 +1,117 @@
1
+ """
2
+ Design System for Pylogue
3
+ Centralized design tokens for colors, spacing, typography, and breakpoints.
4
+ """
5
+
6
+ from dataclasses import dataclass
7
+ from typing import Dict
8
+
9
+
10
+ @dataclass
11
+ class DesignSystem:
12
+ """Centralized design system with consistent tokens across the application."""
13
+
14
+ # Color palette
15
+ COLORS: Dict[str, str] = None
16
+
17
+ # Spacing scale (using em for better scalability)
18
+ SPACING: Dict[str, str] = None
19
+
20
+ # Typography scale
21
+ TYPOGRAPHY: Dict[str, str] = None
22
+
23
+ # Border radius values
24
+ BORDER_RADIUS: Dict[str, str] = None
25
+
26
+ # Breakpoints for responsive design
27
+ BREAKPOINTS: Dict[str, str] = None
28
+
29
+ def __post_init__(self):
30
+ """Initialize default design tokens."""
31
+ if self.COLORS is None:
32
+ self.COLORS = {
33
+ # Background colors
34
+ "dark_bg": "#1a1a1a",
35
+
36
+ # Message role colors
37
+ "user_msg": "#1C0069",
38
+ "assistant_msg": "#004539",
39
+
40
+ # Text colors (calculated via WCAG for contrast)
41
+ "light_text": "#FFFFFF",
42
+ "dark_text": "#000000",
43
+
44
+ # Spinner colors
45
+ "spinner_light": "rgba(255, 255, 255, 0.3)",
46
+ "spinner_border": "rgba(255, 255, 255, 0.1)",
47
+ }
48
+
49
+ if self.SPACING is None:
50
+ self.SPACING = {
51
+ "xs": "0.5em", # 8px at base 16px
52
+ "sm": "0.75em", # 12px at base 16px
53
+ "md": "1em", # 16px at base 16px
54
+ "lg": "1.25em", # 20px at base 16px
55
+ "xl": "1.5em", # 24px at base 16px
56
+ "2xl": "2em", # 32px at base 16px
57
+ "3xl": "3em", # 48px at base 16px
58
+ }
59
+
60
+ if self.TYPOGRAPHY is None:
61
+ self.TYPOGRAPHY = {
62
+ "base": "1em",
63
+ "sm": "0.875em", # 14px at base 16px
64
+ "md": "1em", # 16px
65
+ "lg": "1.1em", # 17.6px
66
+ "xl": "1.25em", # 20px
67
+ "2xl": "1.5em", # 24px
68
+ "weight_normal": "normal",
69
+ "weight_bold": "bold",
70
+ }
71
+
72
+ if self.BORDER_RADIUS is None:
73
+ self.BORDER_RADIUS = {
74
+ "sm": "0.5em",
75
+ "md": "1em",
76
+ "lg": "1.5em",
77
+ }
78
+
79
+ if self.BREAKPOINTS is None:
80
+ self.BREAKPOINTS = {
81
+ "mobile": "768px",
82
+ "tablet": "1024px",
83
+ }
84
+
85
+
86
+ # Global default instance
87
+ default_design_system = DesignSystem()
88
+
89
+
90
+ def get_color(name: str) -> str:
91
+ """Get a color value from the design system."""
92
+ return default_design_system.COLORS.get(name, "")
93
+
94
+
95
+ def get_spacing(name: str) -> str:
96
+ """Get a spacing value from the design system."""
97
+ return default_design_system.SPACING.get(name, "")
98
+
99
+
100
+ def get_typography(name: str) -> str:
101
+ """Get a typography value from the design system."""
102
+ return default_design_system.TYPOGRAPHY.get(name, "")
103
+
104
+
105
+ def get_border_radius(name: str) -> str:
106
+ """Get a border radius value from the design system."""
107
+ return default_design_system.BORDER_RADIUS.get(name, "")
108
+
109
+
110
+ def get_breakpoint(name: str) -> str:
111
+ """Get a breakpoint value from the design system."""
112
+ return default_design_system.BREAKPOINTS.get(name, "")
113
+
114
+
115
+ def get_mobile_media_query() -> str:
116
+ """Get the standard mobile media query."""
117
+ return f"@media (max-width: {get_breakpoint('mobile')})"
@@ -0,0 +1,284 @@
1
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/3-Renderer.ipynb.
2
+
3
+ # %% auto 0
4
+ __all__ = ['ChatRenderer']
5
+
6
+ # %% ../../nbs/3-Renderer.ipynb 1
7
+ from typing import List, Dict, Any, Optional
8
+ from fasthtml.common import *
9
+ from monsterui.all import Button, ButtonT
10
+ from .cards import ChatCard
11
+ from .session import Message
12
+
13
+ # %% ../../nbs/3-Renderer.ipynb 3
14
+ class ChatRenderer:
15
+ """Renders chat components with customizable styling."""
16
+
17
+ CHAT_DIV_ID = "chat-cards"
18
+
19
+ def __init__(
20
+ self,
21
+ card: Optional[ChatCard] = None,
22
+ input_placeholder: str = "Type a message...",
23
+ input_style: Optional[str] = None,
24
+ input_cls: Optional[str] = None,
25
+ chat_container_style: Optional[str] = None,
26
+ chat_container_cls: Optional[str] = None,
27
+ form_cls: Optional[str] = None,
28
+ submit_text: str = "Send",
29
+ ws_endpoint: str = "/ws",
30
+ ):
31
+ """
32
+ Initialize ChatRenderer.
33
+
34
+ Args:
35
+ card: ChatCard instance for rendering messages
36
+ input_placeholder: Placeholder text for input field
37
+ input_style: Custom CSS style for input field
38
+ chat_container_style: Custom CSS style for chat container
39
+ ws_endpoint: WebSocket endpoint path
40
+ """
41
+ self.card = card or ChatCard()
42
+ self.input_placeholder = input_placeholder
43
+ self.ws_endpoint = ws_endpoint
44
+ self.input_style = input_style
45
+ self.input_cls = input_cls or "uk-input chat-input-field"
46
+ self.chat_container_style = chat_container_style
47
+ self.chat_container_cls = chat_container_cls or "chat-stream"
48
+ self.form_cls = form_cls or "chat-form"
49
+ self.submit_text = submit_text
50
+
51
+ def get_styles(self) -> str:
52
+ """Shared stylesheet for the chat experience."""
53
+ return """
54
+ :root {
55
+ --pylogue-bg: #0b0f1a;
56
+ --pylogue-panel: #0f172a;
57
+ --pylogue-panel-border: #1e293b;
58
+ --pylogue-accent: #22c55e;
59
+ --pylogue-accent-2: #38bdf8;
60
+ --pylogue-text: #e2e8f0;
61
+ }
62
+
63
+ body {
64
+ font-family: "Space Grotesk", "Helvetica Neue", sans-serif;
65
+ background: var(--pylogue-bg);
66
+ color: var(--pylogue-text);
67
+ }
68
+
69
+ .pylogue-app {
70
+ min-height: 100vh;
71
+ background:
72
+ radial-gradient(1200px 600px at 80% -10%, rgba(34, 197, 94, 0.18), transparent 60%),
73
+ radial-gradient(900px 500px at 10% 0%, rgba(56, 189, 248, 0.15), transparent 55%),
74
+ linear-gradient(180deg, #0b0f1a 0%, #0a0f1d 100%);
75
+ }
76
+
77
+ .pylogue-container {
78
+ padding-bottom: 3rem;
79
+ }
80
+
81
+ .pylogue-layout {
82
+ display: flex;
83
+ flex-direction: column;
84
+ gap: 2.5rem;
85
+ }
86
+
87
+ .chat-shell {
88
+ background: rgba(15, 23, 42, 0.75);
89
+ border: 1px solid var(--pylogue-panel-border);
90
+ box-shadow: 0 20px 60px rgba(2, 6, 23, 0.45);
91
+ backdrop-filter: blur(16px);
92
+ }
93
+
94
+ .chat-header {
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: space-between;
98
+ gap: 1rem;
99
+ }
100
+
101
+ .chat-stream {
102
+ display: flex;
103
+ flex-direction: column;
104
+ gap: 1rem;
105
+ padding: 1.5rem 0;
106
+ }
107
+
108
+ .chat-scroll {
109
+ max-height: 70vh;
110
+ overflow-y: auto;
111
+ padding-right: 0.5rem;
112
+ }
113
+
114
+ .chat-scroll::-webkit-scrollbar {
115
+ width: 6px;
116
+ }
117
+
118
+ .chat-scroll::-webkit-scrollbar-thumb {
119
+ background: rgba(148, 163, 184, 0.4);
120
+ border-radius: 999px;
121
+ }
122
+
123
+ .chat-row {
124
+ display: flex;
125
+ }
126
+
127
+ .chat-row--user {
128
+ justify-content: flex-end;
129
+ }
130
+
131
+ .chat-row--assistant {
132
+ justify-content: flex-start;
133
+ }
134
+
135
+ .chat-bubble {
136
+ max-width: min(70ch, 90%);
137
+ border-radius: 1.25rem;
138
+ padding: 0.9rem 1.1rem;
139
+ position: relative;
140
+ }
141
+
142
+ .chat-bubble--user {
143
+ background: linear-gradient(135deg, rgba(34, 197, 94, 0.95), rgba(56, 189, 248, 0.85));
144
+ color: #0b1220;
145
+ box-shadow: 0 12px 30px rgba(34, 197, 94, 0.25);
146
+ }
147
+
148
+ .chat-bubble--assistant {
149
+ background: rgba(15, 23, 42, 0.9);
150
+ border: 1px solid rgba(148, 163, 184, 0.2);
151
+ color: var(--pylogue-text);
152
+ }
153
+
154
+ .chat-role {
155
+ display: block;
156
+ text-transform: uppercase;
157
+ letter-spacing: 0.08em;
158
+ font-weight: 600;
159
+ margin-bottom: 0.5rem;
160
+ }
161
+
162
+ .chat-text {
163
+ line-height: 1.6;
164
+ }
165
+
166
+ .chat-form {
167
+ display: flex;
168
+ gap: 0.75rem;
169
+ align-items: center;
170
+ }
171
+
172
+ .chat-input-field {
173
+ flex: 1;
174
+ background: rgba(15, 23, 42, 0.9);
175
+ border-color: rgba(148, 163, 184, 0.3);
176
+ }
177
+
178
+ .chat-input-field:focus {
179
+ border-color: var(--pylogue-accent);
180
+ box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
181
+ }
182
+
183
+ .chat-send {
184
+ min-width: 6.5rem;
185
+ }
186
+
187
+ @media (max-width: 768px) {
188
+ .chat-shell {
189
+ margin: 0 0.75rem;
190
+ }
191
+ .chat-stream {
192
+ padding: 1rem 0;
193
+ }
194
+ .chat-form {
195
+ flex-direction: column;
196
+ align-items: stretch;
197
+ }
198
+ .chat-send {
199
+ width: 100%;
200
+ }
201
+ }
202
+ """
203
+
204
+ def render_message(self, message: Message) -> Any:
205
+ """Render a single message."""
206
+ return self.card(message.to_dict())
207
+
208
+ def render_messages(self, messages: List[Message]) -> Any:
209
+ """
210
+ Render a list of messages in a container.
211
+
212
+ Args:
213
+ messages: List of Message objects to render
214
+
215
+ Returns:
216
+ FastHTML Div containing all rendered messages
217
+ """
218
+ return Div(
219
+ *[self.render_message(msg) for msg in messages],
220
+ id=self.CHAT_DIV_ID,
221
+ cls=self.chat_container_cls,
222
+ style=self.chat_container_style,
223
+ )
224
+
225
+ def render_messages_from_dicts(self, message_dicts: List[Dict[str, Any]]) -> Any:
226
+ """
227
+ Render messages from dictionary representations.
228
+
229
+ Args:
230
+ message_dicts: List of message dictionaries
231
+
232
+ Returns:
233
+ FastHTML Div containing all rendered messages
234
+ """
235
+ messages = [Message.from_dict(d) for d in message_dicts]
236
+ return self.render_messages(messages)
237
+
238
+ def render_input(self, input_id: str = "msg", autofocus: bool = True) -> Any:
239
+ """
240
+ Render the message input field.
241
+
242
+ Args:
243
+ input_id: HTML ID for the input element
244
+ autofocus: Whether to autofocus the input
245
+
246
+ Returns:
247
+ FastHTML Input element
248
+ """
249
+ return Input(
250
+ id=input_id,
251
+ name="msg",
252
+ placeholder=self.input_placeholder,
253
+ autofocus=autofocus,
254
+ cls=self.input_cls,
255
+ style=self.input_style,
256
+ )
257
+
258
+ def render_form(
259
+ self,
260
+ form_id: str = "form",
261
+ form_style: Optional[str] = None,
262
+ ws_send: bool = True,
263
+ ) -> Any:
264
+ """
265
+ Render the input form with WebSocket connection.
266
+
267
+ Args:
268
+ form_id: HTML ID for the form
269
+ form_style: Custom CSS style for form
270
+ ws_send: Whether form sends via WebSocket
271
+
272
+ Returns:
273
+ FastHTML Form element
274
+ """
275
+ return Form(
276
+ self.render_input(),
277
+ Button(self.submit_text, cls=(ButtonT.primary, "chat-send"), type="submit"),
278
+ id=form_id,
279
+ hx_ext="ws",
280
+ ws_connect=self.ws_endpoint,
281
+ ws_send=ws_send,
282
+ cls=self.form_cls,
283
+ style=form_style,
284
+ )