pylogue 0.3__py3-none-any.whl → 0.3.30__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.
@@ -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,8 +141,7 @@ class ChatApp:
121
141
  # Add spinner styles
122
142
  headers.append(Style(self.config.get_spinner_style()))
123
143
 
124
- # Add mobile responsive styles from ChatCard
125
- headers.append(Style(self.renderer.card.get_mobile_styles()))
144
+ headers.append(Style(self.renderer.get_styles()))
126
145
 
127
146
  return FastHTML(exts="ws", hdrs=tuple(headers))
128
147
 
@@ -140,20 +159,40 @@ class ChatApp:
140
159
  """Main chat interface."""
141
160
  initial_messages = self._get_initial_messages()
142
161
 
143
- container_style = self.config.container_style or (
144
- f"font-family: monospace, sans-serif; margin: 0; padding: 0; "
145
- )
146
-
147
162
  return (
148
163
  Title(self.config.page_title),
164
+ Meta(name="viewport", content="width=device-width, initial-scale=1.0"),
149
165
  Body(
150
- Div(
151
- H1(self.config.app_title, style=self.config.header_style),
152
- self.renderer.render_messages(initial_messages),
153
- self.renderer.render_form(),
154
- style=container_style,
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),
155
194
  ),
156
- style="background-color: #000",
195
+ cls="pylogue-app",
157
196
  ),
158
197
  )
159
198
 
@@ -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
+ )