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.
- pylogue/core.py +804 -0
- pylogue/embeds.py +32 -0
- pylogue/integrations/__init__.py +1 -0
- pylogue/integrations/pydantic_ai.py +417 -0
- pylogue/legacy/cards.py +112 -0
- pylogue/{chat.py → legacy/chat.py} +54 -27
- pylogue/{chatapp.py → legacy/chatapp.py} +65 -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.3.dist-info → pylogue-0.3.30.dist-info}/METADATA +10 -1
- pylogue-0.3.30.dist-info/RECORD +26 -0
- {pylogue-0.3.dist-info → pylogue-0.3.30.dist-info}/WHEEL +1 -1
- pylogue/cards.py +0 -174
- pylogue/renderer.py +0 -139
- pylogue-0.3.dist-info/RECORD +0 -17
- /pylogue/{__init__.py → legacy/__init__.py} +0 -0
- /pylogue/{__pre_init__.py → legacy/__pre_init__.py} +0 -0
- /pylogue/{_modidx.py → legacy/_modidx.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.3.dist-info → pylogue-0.3.30.dist-info}/entry_points.txt +0 -0
- {pylogue-0.3.dist-info → pylogue-0.3.30.dist-info}/licenses/AUTHORS.md +0 -0
- {pylogue-0.3.dist-info → pylogue-0.3.30.dist-info}/licenses/LICENSE +0 -0
- {pylogue-0.3.dist-info → pylogue-0.3.30.dist-info}/top_level.txt +0 -0
|
@@ -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,8 +141,7 @@ class ChatApp:
|
|
|
121
141
|
# Add spinner styles
|
|
122
142
|
headers.append(Style(self.config.get_spinner_style()))
|
|
123
143
|
|
|
124
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
-
|
|
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
|
+
)
|