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.
- pylogue/core.py +804 -0
- pylogue/embeds.py +32 -0
- pylogue/integrations/__init__.py +1 -0
- pylogue/integrations/pydantic_ai.py +417 -0
- pylogue/{_modidx.py → legacy/_modidx.py} +1 -0
- pylogue/legacy/cards.py +112 -0
- pylogue/{chat.py → legacy/chat.py} +54 -27
- pylogue/{chatapp.py → legacy/chatapp.py} +70 -26
- pylogue/legacy/design_system.py +117 -0
- pylogue/legacy/renderer.py +284 -0
- pylogue/shell.py +342 -0
- pylogue/static/pylogue-core.css +372 -0
- pylogue/static/pylogue-core.js +199 -0
- pylogue/static/pylogue-markdown.js +745 -0
- {pylogue-0.2.1.dist-info → pylogue-0.3.29.dist-info}/METADATA +10 -1
- pylogue-0.3.29.dist-info/RECORD +26 -0
- {pylogue-0.2.1.dist-info → pylogue-0.3.29.dist-info}/WHEEL +1 -1
- pylogue/cards.py +0 -157
- pylogue/renderer.py +0 -128
- pylogue-0.2.1.dist-info/RECORD +0 -17
- /pylogue/{__init__.py → legacy/__init__.py} +0 -0
- /pylogue/{__pre_init__.py → legacy/__pre_init__.py} +0 -0
- /pylogue/{health.py → legacy/health.py} +0 -0
- /pylogue/{service.py → legacy/service.py} +0 -0
- /pylogue/{session.py → legacy/session.py} +0 -0
- {pylogue-0.2.1.dist-info → pylogue-0.3.29.dist-info}/entry_points.txt +0 -0
- {pylogue-0.2.1.dist-info → pylogue-0.3.29.dist-info}/licenses/AUTHORS.md +0 -0
- {pylogue-0.2.1.dist-info → pylogue-0.3.29.dist-info}/licenses/LICENSE +0 -0
- {pylogue-0.2.1.dist-info → pylogue-0.3.29.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
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
|
|
24
|
-
border-top-color:
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
-
|
|
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 =
|
|
32
|
-
header_style: str =
|
|
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
|
|
70
|
-
border-top-color:
|
|
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
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
+
)
|