arionxiv 1.0.32__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.
- arionxiv/__init__.py +40 -0
- arionxiv/__main__.py +10 -0
- arionxiv/arxiv_operations/__init__.py +0 -0
- arionxiv/arxiv_operations/client.py +225 -0
- arionxiv/arxiv_operations/fetcher.py +173 -0
- arionxiv/arxiv_operations/searcher.py +122 -0
- arionxiv/arxiv_operations/utils.py +293 -0
- arionxiv/cli/__init__.py +4 -0
- arionxiv/cli/commands/__init__.py +1 -0
- arionxiv/cli/commands/analyze.py +587 -0
- arionxiv/cli/commands/auth.py +365 -0
- arionxiv/cli/commands/chat.py +714 -0
- arionxiv/cli/commands/daily.py +482 -0
- arionxiv/cli/commands/fetch.py +217 -0
- arionxiv/cli/commands/library.py +295 -0
- arionxiv/cli/commands/preferences.py +426 -0
- arionxiv/cli/commands/search.py +254 -0
- arionxiv/cli/commands/settings_unified.py +1407 -0
- arionxiv/cli/commands/trending.py +41 -0
- arionxiv/cli/commands/welcome.py +168 -0
- arionxiv/cli/main.py +407 -0
- arionxiv/cli/ui/__init__.py +1 -0
- arionxiv/cli/ui/global_theme_manager.py +173 -0
- arionxiv/cli/ui/logo.py +127 -0
- arionxiv/cli/ui/splash.py +89 -0
- arionxiv/cli/ui/theme.py +32 -0
- arionxiv/cli/ui/theme_system.py +391 -0
- arionxiv/cli/utils/__init__.py +54 -0
- arionxiv/cli/utils/animations.py +522 -0
- arionxiv/cli/utils/api_client.py +583 -0
- arionxiv/cli/utils/api_config.py +505 -0
- arionxiv/cli/utils/command_suggestions.py +147 -0
- arionxiv/cli/utils/db_config_manager.py +254 -0
- arionxiv/github_actions_runner.py +206 -0
- arionxiv/main.py +23 -0
- arionxiv/prompts/__init__.py +9 -0
- arionxiv/prompts/prompts.py +247 -0
- arionxiv/rag_techniques/__init__.py +8 -0
- arionxiv/rag_techniques/basic_rag.py +1531 -0
- arionxiv/scheduler_daemon.py +139 -0
- arionxiv/server.py +1000 -0
- arionxiv/server_main.py +24 -0
- arionxiv/services/__init__.py +73 -0
- arionxiv/services/llm_client.py +30 -0
- arionxiv/services/llm_inference/__init__.py +58 -0
- arionxiv/services/llm_inference/groq_client.py +469 -0
- arionxiv/services/llm_inference/llm_utils.py +250 -0
- arionxiv/services/llm_inference/openrouter_client.py +564 -0
- arionxiv/services/unified_analysis_service.py +872 -0
- arionxiv/services/unified_auth_service.py +457 -0
- arionxiv/services/unified_config_service.py +456 -0
- arionxiv/services/unified_daily_dose_service.py +823 -0
- arionxiv/services/unified_database_service.py +1633 -0
- arionxiv/services/unified_llm_service.py +366 -0
- arionxiv/services/unified_paper_service.py +604 -0
- arionxiv/services/unified_pdf_service.py +522 -0
- arionxiv/services/unified_prompt_service.py +344 -0
- arionxiv/services/unified_scheduler_service.py +589 -0
- arionxiv/services/unified_user_service.py +954 -0
- arionxiv/utils/__init__.py +51 -0
- arionxiv/utils/api_helpers.py +200 -0
- arionxiv/utils/file_cleanup.py +150 -0
- arionxiv/utils/ip_helper.py +96 -0
- arionxiv-1.0.32.dist-info/METADATA +336 -0
- arionxiv-1.0.32.dist-info/RECORD +69 -0
- arionxiv-1.0.32.dist-info/WHEEL +5 -0
- arionxiv-1.0.32.dist-info/entry_points.txt +4 -0
- arionxiv-1.0.32.dist-info/licenses/LICENSE +21 -0
- arionxiv-1.0.32.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Animation utilities for ArionXiv CLI
|
|
3
|
+
Provides shake, reveal, slam, and other visual effects for terminal UI
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
from io import StringIO
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from rich.columns import Columns
|
|
11
|
+
from rich.text import Text
|
|
12
|
+
from rich.live import Live
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def slam_content(console_instance: Console, content: str, style: str = "", duration: float = 0.6):
|
|
16
|
+
"""
|
|
17
|
+
Slam content onto screen with dramatic zoom-in and impact effect
|
|
18
|
+
Content appears to fly in from distance and slam with a shake on impact
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
console_instance: Rich console instance
|
|
22
|
+
content: Content to slam (can be multi-line)
|
|
23
|
+
style: Rich style to apply
|
|
24
|
+
duration: Duration of effect in seconds
|
|
25
|
+
"""
|
|
26
|
+
lines = content.split('\n')
|
|
27
|
+
num_lines = len(lines)
|
|
28
|
+
|
|
29
|
+
# Phase 1: Zoom in (content grows from nothing) - 60% of duration
|
|
30
|
+
# Phase 2: Impact shake - 40% of duration
|
|
31
|
+
zoom_duration = duration * 0.5
|
|
32
|
+
shake_duration = duration * 0.5
|
|
33
|
+
|
|
34
|
+
zoom_frames = int(zoom_duration * 40)
|
|
35
|
+
shake_frames = int(shake_duration * 30)
|
|
36
|
+
|
|
37
|
+
with Live(console=console_instance, refresh_per_second=40, transient=True) as live:
|
|
38
|
+
# Phase 1: Zoom in - show progressively more lines from center
|
|
39
|
+
for i in range(zoom_frames):
|
|
40
|
+
progress = (i + 1) / zoom_frames
|
|
41
|
+
# Ease-out curve for dramatic deceleration
|
|
42
|
+
eased = 1 - (1 - progress) ** 3
|
|
43
|
+
|
|
44
|
+
# Calculate how many lines to show (from center outward)
|
|
45
|
+
lines_to_show = max(1, int(num_lines * eased))
|
|
46
|
+
|
|
47
|
+
# Center the visible lines
|
|
48
|
+
start_idx = (num_lines - lines_to_show) // 2
|
|
49
|
+
end_idx = start_idx + lines_to_show
|
|
50
|
+
|
|
51
|
+
visible_lines = lines[start_idx:end_idx]
|
|
52
|
+
|
|
53
|
+
# Add padding from top to simulate coming from distance
|
|
54
|
+
top_padding = int((1 - eased) * 5)
|
|
55
|
+
padded_content = '\n' * top_padding + '\n'.join(visible_lines)
|
|
56
|
+
|
|
57
|
+
if style:
|
|
58
|
+
live.update(Text(padded_content, style=style))
|
|
59
|
+
else:
|
|
60
|
+
live.update(Text(padded_content))
|
|
61
|
+
|
|
62
|
+
time.sleep(zoom_duration / zoom_frames)
|
|
63
|
+
|
|
64
|
+
# Phase 2: Impact shake - violent shake that settles
|
|
65
|
+
for i in range(shake_frames):
|
|
66
|
+
progress = i / shake_frames
|
|
67
|
+
# Start intense, decay quickly
|
|
68
|
+
intensity = int(10 * (1 - progress) ** 2)
|
|
69
|
+
|
|
70
|
+
# Rapid oscillation
|
|
71
|
+
if i % 2 == 0:
|
|
72
|
+
offset = intensity
|
|
73
|
+
else:
|
|
74
|
+
offset = -intensity // 2
|
|
75
|
+
|
|
76
|
+
shifted_lines = []
|
|
77
|
+
for line in lines:
|
|
78
|
+
if offset > 0:
|
|
79
|
+
shifted_lines.append(" " * offset + line)
|
|
80
|
+
else:
|
|
81
|
+
shifted_lines.append(line)
|
|
82
|
+
|
|
83
|
+
shifted_content = '\n'.join(shifted_lines)
|
|
84
|
+
|
|
85
|
+
if style:
|
|
86
|
+
live.update(Text(shifted_content, style=style))
|
|
87
|
+
else:
|
|
88
|
+
live.update(Text(shifted_content))
|
|
89
|
+
|
|
90
|
+
time.sleep(shake_duration / shake_frames)
|
|
91
|
+
|
|
92
|
+
# Print final stable content
|
|
93
|
+
if style:
|
|
94
|
+
console_instance.print(Text(content, style=style))
|
|
95
|
+
else:
|
|
96
|
+
console_instance.print(content)
|
|
97
|
+
|
|
98
|
+
def slam_columns(console_instance: Console, columns: Columns, duration: float = 0.6):
|
|
99
|
+
"""
|
|
100
|
+
Slam Rich Columns onto screen with dramatic zoom-in and impact effect
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
console_instance: Rich console instance
|
|
104
|
+
columns: Rich Columns object to slam
|
|
105
|
+
duration: Duration of effect in seconds
|
|
106
|
+
"""
|
|
107
|
+
# Capture columns as string
|
|
108
|
+
temp_console = Console(file=StringIO(), force_terminal=True, width=console_instance.width)
|
|
109
|
+
temp_console.print(columns)
|
|
110
|
+
columns_str = temp_console.file.getvalue()
|
|
111
|
+
|
|
112
|
+
lines = columns_str.split('\n')
|
|
113
|
+
num_lines = len(lines)
|
|
114
|
+
|
|
115
|
+
zoom_duration = duration * 0.5
|
|
116
|
+
shake_duration = duration * 0.5
|
|
117
|
+
|
|
118
|
+
zoom_frames = int(zoom_duration * 40)
|
|
119
|
+
shake_frames = int(shake_duration * 30)
|
|
120
|
+
|
|
121
|
+
with Live(console=console_instance, refresh_per_second=40, transient=True) as live:
|
|
122
|
+
# Phase 1: Zoom in from center
|
|
123
|
+
for i in range(zoom_frames):
|
|
124
|
+
progress = (i + 1) / zoom_frames
|
|
125
|
+
eased = 1 - (1 - progress) ** 3
|
|
126
|
+
|
|
127
|
+
lines_to_show = max(1, int(num_lines * eased))
|
|
128
|
+
start_idx = (num_lines - lines_to_show) // 2
|
|
129
|
+
end_idx = start_idx + lines_to_show
|
|
130
|
+
|
|
131
|
+
visible_lines = lines[start_idx:end_idx]
|
|
132
|
+
|
|
133
|
+
# Horizontal squeeze effect - indent from sides
|
|
134
|
+
h_squeeze = int((1 - eased) * 15)
|
|
135
|
+
squeezed_lines = [" " * h_squeeze + line for line in visible_lines]
|
|
136
|
+
|
|
137
|
+
live.update(Text.from_ansi('\n'.join(squeezed_lines)))
|
|
138
|
+
time.sleep(zoom_duration / zoom_frames)
|
|
139
|
+
|
|
140
|
+
# Phase 2: Impact shake
|
|
141
|
+
for i in range(shake_frames):
|
|
142
|
+
progress = i / shake_frames
|
|
143
|
+
intensity = int(5 * (1 - progress) ** 2)
|
|
144
|
+
|
|
145
|
+
if i % 2 == 0:
|
|
146
|
+
offset = intensity
|
|
147
|
+
else:
|
|
148
|
+
offset = -intensity // 2
|
|
149
|
+
|
|
150
|
+
shifted_lines = []
|
|
151
|
+
for line in lines:
|
|
152
|
+
if offset > 0:
|
|
153
|
+
shifted_lines.append(" " * offset + line)
|
|
154
|
+
else:
|
|
155
|
+
shifted_lines.append(line)
|
|
156
|
+
|
|
157
|
+
live.update(Text.from_ansi('\n'.join(shifted_lines)))
|
|
158
|
+
time.sleep(shake_duration / shake_frames)
|
|
159
|
+
|
|
160
|
+
console_instance.print(columns)
|
|
161
|
+
|
|
162
|
+
def shake_content(console_instance: Console, content: str, style: str = "", duration: float = 1.0, intensity: int = 4):
|
|
163
|
+
"""
|
|
164
|
+
Shake content with earthquake effect for specified duration
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
console_instance: Rich console instance
|
|
168
|
+
content: Content to shake (can be multi-line)
|
|
169
|
+
style: Rich style to apply
|
|
170
|
+
duration: Duration of shake in seconds
|
|
171
|
+
intensity: Max spaces to shift left/right
|
|
172
|
+
"""
|
|
173
|
+
frames = int(duration * 30) # 30 fps
|
|
174
|
+
|
|
175
|
+
with Live(console=console_instance, refresh_per_second=30, transient=True) as live:
|
|
176
|
+
for i in range(frames):
|
|
177
|
+
progress = i / frames
|
|
178
|
+
current_intensity = int(intensity * (1 - progress * 0.5))
|
|
179
|
+
offset = int(current_intensity * (1 if (i % 4) < 2 else -1) * (1 if (i % 2) == 0 else 0.5))
|
|
180
|
+
|
|
181
|
+
lines = content.split('\n')
|
|
182
|
+
shifted_lines = []
|
|
183
|
+
for line in lines:
|
|
184
|
+
if offset > 0:
|
|
185
|
+
shifted_lines.append(" " * offset + line)
|
|
186
|
+
else:
|
|
187
|
+
shifted_lines.append(line)
|
|
188
|
+
|
|
189
|
+
shifted_content = '\n'.join(shifted_lines)
|
|
190
|
+
|
|
191
|
+
if style:
|
|
192
|
+
live.update(Text(shifted_content, style=style))
|
|
193
|
+
else:
|
|
194
|
+
live.update(Text(shifted_content))
|
|
195
|
+
|
|
196
|
+
time.sleep(duration / frames)
|
|
197
|
+
|
|
198
|
+
if style:
|
|
199
|
+
console_instance.print(Text(content, style=style))
|
|
200
|
+
else:
|
|
201
|
+
console_instance.print(content)
|
|
202
|
+
|
|
203
|
+
def shake_text(console_instance: Console, message: str, style: str = "", duration: float = 0.5, intensity: int = 3):
|
|
204
|
+
"""
|
|
205
|
+
Shake a single line of text with earthquake effect
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
console_instance: Rich console instance
|
|
209
|
+
message: The message to display with shake effect
|
|
210
|
+
style: Rich style to apply
|
|
211
|
+
duration: Duration of shake in seconds
|
|
212
|
+
intensity: Max spaces to shift left/right
|
|
213
|
+
"""
|
|
214
|
+
shake_content(console_instance, message, style=style, duration=duration, intensity=intensity)
|
|
215
|
+
|
|
216
|
+
def shake_columns(console_instance: Console, columns: Columns, duration: float = 1.0, intensity: int = 3):
|
|
217
|
+
"""
|
|
218
|
+
Shake Rich Columns with earthquake effect
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
console_instance: Rich console instance
|
|
222
|
+
columns: Rich Columns object to shake
|
|
223
|
+
duration: Duration of shake in seconds
|
|
224
|
+
intensity: Max spaces to shift
|
|
225
|
+
"""
|
|
226
|
+
frames = int(duration * 25)
|
|
227
|
+
|
|
228
|
+
temp_console = Console(file=StringIO(), force_terminal=True, width=console_instance.width)
|
|
229
|
+
temp_console.print(columns)
|
|
230
|
+
columns_str = temp_console.file.getvalue()
|
|
231
|
+
|
|
232
|
+
with Live(console=console_instance, refresh_per_second=25, transient=True) as live:
|
|
233
|
+
for i in range(frames):
|
|
234
|
+
progress = i / frames
|
|
235
|
+
current_intensity = int(intensity * (1 - progress * 0.3))
|
|
236
|
+
offset = int(current_intensity * (1 if (i % 4) < 2 else -1) * (1 if (i % 2) == 0 else 0.6))
|
|
237
|
+
|
|
238
|
+
lines = columns_str.split('\n')
|
|
239
|
+
shifted_lines = []
|
|
240
|
+
for line in lines:
|
|
241
|
+
if offset > 0:
|
|
242
|
+
shifted_lines.append(" " * offset + line)
|
|
243
|
+
else:
|
|
244
|
+
shifted_lines.append(line)
|
|
245
|
+
|
|
246
|
+
live.update(Text.from_ansi('\n'.join(shifted_lines)))
|
|
247
|
+
time.sleep(duration / frames)
|
|
248
|
+
|
|
249
|
+
console_instance.print(columns)
|
|
250
|
+
|
|
251
|
+
def shake_panel(console_instance: Console, panel: Panel, duration: float = 1.0, intensity: int = 3):
|
|
252
|
+
"""
|
|
253
|
+
Shake a Rich Panel with earthquake effect
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
console_instance: Rich console instance
|
|
257
|
+
panel: Rich Panel to shake
|
|
258
|
+
duration: Duration of shake in seconds
|
|
259
|
+
intensity: Max spaces to shift
|
|
260
|
+
"""
|
|
261
|
+
frames = int(duration * 25)
|
|
262
|
+
|
|
263
|
+
temp_console = Console(file=StringIO(), force_terminal=True, width=console_instance.width)
|
|
264
|
+
temp_console.print(panel)
|
|
265
|
+
panel_str = temp_console.file.getvalue()
|
|
266
|
+
|
|
267
|
+
with Live(console=console_instance, refresh_per_second=25, transient=True) as live:
|
|
268
|
+
for i in range(frames):
|
|
269
|
+
progress = i / frames
|
|
270
|
+
current_intensity = int(intensity * (1 - progress * 0.3))
|
|
271
|
+
offset = int(current_intensity * (1 if (i % 4) < 2 else -1) * (1 if (i % 2) == 0 else 0.6))
|
|
272
|
+
|
|
273
|
+
lines = panel_str.split('\n')
|
|
274
|
+
shifted_lines = []
|
|
275
|
+
for line in lines:
|
|
276
|
+
if offset > 0:
|
|
277
|
+
shifted_lines.append(" " * offset + line)
|
|
278
|
+
else:
|
|
279
|
+
shifted_lines.append(line)
|
|
280
|
+
|
|
281
|
+
live.update(Text.from_ansi('\n'.join(shifted_lines)))
|
|
282
|
+
time.sleep(duration / frames)
|
|
283
|
+
|
|
284
|
+
console_instance.print(panel)
|
|
285
|
+
|
|
286
|
+
def left_to_right_reveal(console_instance: Console, text: str, style: str = "", duration: float = 0.8):
|
|
287
|
+
"""
|
|
288
|
+
Reveal text from left to right character by character
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
console_instance: Rich console instance
|
|
292
|
+
text: Text to reveal
|
|
293
|
+
style: Rich style to apply
|
|
294
|
+
duration: Total duration of reveal
|
|
295
|
+
"""
|
|
296
|
+
delay = duration / len(text) if text else 0.01
|
|
297
|
+
revealed = ""
|
|
298
|
+
|
|
299
|
+
with Live(console=console_instance, refresh_per_second=60, transient=True) as live:
|
|
300
|
+
for char in text:
|
|
301
|
+
revealed += char
|
|
302
|
+
if style:
|
|
303
|
+
live.update(Text(revealed, style=style))
|
|
304
|
+
else:
|
|
305
|
+
live.update(Text(revealed))
|
|
306
|
+
time.sleep(delay)
|
|
307
|
+
|
|
308
|
+
if style:
|
|
309
|
+
console_instance.print(Text(text, style=style))
|
|
310
|
+
else:
|
|
311
|
+
console_instance.print(text)
|
|
312
|
+
|
|
313
|
+
def top_to_bottom_reveal(console_instance: Console, content: str, style: str = "", duration: float = 0.5):
|
|
314
|
+
"""
|
|
315
|
+
Reveal content line by line from top to bottom
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
console_instance: Rich console instance
|
|
319
|
+
content: Multi-line content to reveal
|
|
320
|
+
style: Rich style string
|
|
321
|
+
duration: Total duration of reveal
|
|
322
|
+
"""
|
|
323
|
+
lines = content.split('\n')
|
|
324
|
+
line_delay = duration / len(lines) if lines else 0.03
|
|
325
|
+
revealed = []
|
|
326
|
+
|
|
327
|
+
with Live(console=console_instance, refresh_per_second=60, transient=True) as live:
|
|
328
|
+
for line in lines:
|
|
329
|
+
revealed.append(line)
|
|
330
|
+
current_display = '\n'.join(revealed)
|
|
331
|
+
if style:
|
|
332
|
+
live.update(Text(current_display, style=style))
|
|
333
|
+
else:
|
|
334
|
+
live.update(Text.from_markup(current_display))
|
|
335
|
+
time.sleep(line_delay)
|
|
336
|
+
|
|
337
|
+
if style:
|
|
338
|
+
console_instance.print(Text(content, style=style))
|
|
339
|
+
else:
|
|
340
|
+
console_instance.print(content)
|
|
341
|
+
|
|
342
|
+
def typewriter_reveal(console_instance: Console, text: str, style: str = "", delay: float = 0.02):
|
|
343
|
+
"""
|
|
344
|
+
Reveal text with typewriter effect (character by character)
|
|
345
|
+
|
|
346
|
+
Args:
|
|
347
|
+
console_instance: Rich console instance
|
|
348
|
+
text: Text to reveal
|
|
349
|
+
style: Rich style to apply
|
|
350
|
+
delay: Delay between each character
|
|
351
|
+
"""
|
|
352
|
+
revealed = ""
|
|
353
|
+
|
|
354
|
+
with Live(console=console_instance, refresh_per_second=60, transient=True) as live:
|
|
355
|
+
for char in text:
|
|
356
|
+
revealed += char
|
|
357
|
+
if style:
|
|
358
|
+
live.update(Text(revealed, style=style))
|
|
359
|
+
else:
|
|
360
|
+
live.update(Text(revealed))
|
|
361
|
+
time.sleep(delay)
|
|
362
|
+
|
|
363
|
+
if style:
|
|
364
|
+
console_instance.print(Text(text, style=style))
|
|
365
|
+
else:
|
|
366
|
+
console_instance.print(text)
|
|
367
|
+
|
|
368
|
+
async def row_by_row_table_reveal(console_instance: Console, table_creator, num_rows: int, duration: float = 1.0):
|
|
369
|
+
"""
|
|
370
|
+
Reveal a table row by row with animation
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
console_instance: Rich console instance
|
|
374
|
+
table_creator: Callable that takes num_rows and returns a Table
|
|
375
|
+
num_rows: Total number of rows to animate
|
|
376
|
+
duration: Total duration for all rows to appear (default 1 second)
|
|
377
|
+
"""
|
|
378
|
+
import asyncio
|
|
379
|
+
|
|
380
|
+
if num_rows == 0:
|
|
381
|
+
return
|
|
382
|
+
|
|
383
|
+
row_delay = duration / num_rows
|
|
384
|
+
|
|
385
|
+
with Live(console=console_instance, refresh_per_second=30, transient=True) as live:
|
|
386
|
+
for i in range(1, num_rows + 1):
|
|
387
|
+
live.update(table_creator(i))
|
|
388
|
+
await asyncio.sleep(row_delay)
|
|
389
|
+
|
|
390
|
+
# Print final table
|
|
391
|
+
console_instance.print(table_creator(num_rows))
|
|
392
|
+
|
|
393
|
+
def stream_text_response(console_instance: Console, text: str, style: str = "", duration: float = 3.0):
|
|
394
|
+
"""
|
|
395
|
+
Stream a long text response with smooth left-to-right flow
|
|
396
|
+
Ideal for AI responses - streams word by word for natural reading
|
|
397
|
+
|
|
398
|
+
Args:
|
|
399
|
+
console_instance: Rich console instance
|
|
400
|
+
text: Text to stream
|
|
401
|
+
style: Rich style to apply
|
|
402
|
+
duration: Total duration of streaming (default 3 seconds)
|
|
403
|
+
"""
|
|
404
|
+
words = text.split()
|
|
405
|
+
if not words:
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
delay = duration / len(words)
|
|
409
|
+
revealed = ""
|
|
410
|
+
|
|
411
|
+
with Live(console=console_instance, refresh_per_second=60, transient=True) as live:
|
|
412
|
+
for word in words:
|
|
413
|
+
revealed += word + " "
|
|
414
|
+
if style:
|
|
415
|
+
live.update(Text(revealed.strip(), style=style))
|
|
416
|
+
else:
|
|
417
|
+
live.update(Text(revealed.strip()))
|
|
418
|
+
time.sleep(delay)
|
|
419
|
+
|
|
420
|
+
if style:
|
|
421
|
+
console_instance.print(Text(text, style=style))
|
|
422
|
+
else:
|
|
423
|
+
console_instance.print(text)
|
|
424
|
+
|
|
425
|
+
def stream_markdown_response(console_instance: Console, text: str, panel_title: str = "", border_style: str = None, duration: float = 3.0):
|
|
426
|
+
"""
|
|
427
|
+
Stream a markdown response inside a panel with smooth word-by-word flow
|
|
428
|
+
Perfect for AI assistant responses
|
|
429
|
+
|
|
430
|
+
Args:
|
|
431
|
+
console_instance: Rich console instance
|
|
432
|
+
text: Markdown text to stream
|
|
433
|
+
panel_title: Title for the panel
|
|
434
|
+
border_style: Border style for the panel (defaults to theme primary color)
|
|
435
|
+
duration: Total duration of streaming (default 3 seconds)
|
|
436
|
+
"""
|
|
437
|
+
from rich.markdown import Markdown
|
|
438
|
+
from ..ui.theme import get_theme_colors
|
|
439
|
+
|
|
440
|
+
if border_style is None:
|
|
441
|
+
colors = get_theme_colors()
|
|
442
|
+
border_style = colors['primary']
|
|
443
|
+
|
|
444
|
+
# For complex markdown (tables, code blocks, long responses), skip streaming animation
|
|
445
|
+
# to avoid visual glitches with partial rendering
|
|
446
|
+
has_complex_markdown = (
|
|
447
|
+
'|' in text and '-' in text and # Tables
|
|
448
|
+
len([line for line in text.split('\n') if '|' in line]) > 2
|
|
449
|
+
) or len(text) > 2000 # Long responses
|
|
450
|
+
|
|
451
|
+
if has_complex_markdown:
|
|
452
|
+
# Just show a brief thinking indicator then display the final result
|
|
453
|
+
with Live(console=console_instance, refresh_per_second=10, transient=True) as live:
|
|
454
|
+
live.update(Panel("Formatting response...", title=panel_title, border_style=border_style))
|
|
455
|
+
time.sleep(0.3)
|
|
456
|
+
|
|
457
|
+
# Print final panel directly
|
|
458
|
+
final_panel = Panel(
|
|
459
|
+
Markdown(text),
|
|
460
|
+
title=panel_title,
|
|
461
|
+
border_style=border_style
|
|
462
|
+
)
|
|
463
|
+
console_instance.print(final_panel)
|
|
464
|
+
return
|
|
465
|
+
|
|
466
|
+
words = text.split()
|
|
467
|
+
if not words:
|
|
468
|
+
return
|
|
469
|
+
|
|
470
|
+
# Cap the streaming duration for very long responses
|
|
471
|
+
delay = min(duration / len(words), 0.05) # Max 50ms per word
|
|
472
|
+
revealed = ""
|
|
473
|
+
|
|
474
|
+
with Live(console=console_instance, refresh_per_second=30, transient=True) as live:
|
|
475
|
+
for word in words:
|
|
476
|
+
revealed += word + " "
|
|
477
|
+
panel = Panel(
|
|
478
|
+
Markdown(revealed.strip()),
|
|
479
|
+
title=panel_title,
|
|
480
|
+
border_style=border_style
|
|
481
|
+
)
|
|
482
|
+
live.update(panel)
|
|
483
|
+
time.sleep(delay)
|
|
484
|
+
|
|
485
|
+
# Print final panel
|
|
486
|
+
final_panel = Panel(
|
|
487
|
+
Markdown(text),
|
|
488
|
+
title=panel_title,
|
|
489
|
+
border_style=border_style
|
|
490
|
+
)
|
|
491
|
+
console_instance.print(final_panel)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def animated_help_line(console_instance: Console, cmd_text: str, desc_text: str, primary_color: str, padding: str, duration: float = 0.5):
|
|
495
|
+
"""
|
|
496
|
+
Animate a command/option line for help pages: reveal command name character by character,
|
|
497
|
+
then show the full line with description.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
console_instance: Rich console instance
|
|
501
|
+
cmd_text: Command or option name to animate
|
|
502
|
+
desc_text: Description text to show after command
|
|
503
|
+
primary_color: Primary theme color for styling
|
|
504
|
+
padding: Padding string between command and description
|
|
505
|
+
duration: Duration of the animation in seconds
|
|
506
|
+
"""
|
|
507
|
+
if not cmd_text:
|
|
508
|
+
return
|
|
509
|
+
|
|
510
|
+
delay = duration / len(cmd_text) if cmd_text else 0.01
|
|
511
|
+
revealed = ""
|
|
512
|
+
|
|
513
|
+
# Animate the command name character by character
|
|
514
|
+
with Live(console=console_instance, refresh_per_second=60, transient=True) as live:
|
|
515
|
+
for char in cmd_text:
|
|
516
|
+
revealed += char
|
|
517
|
+
live.update(Text(f" {revealed}", style=f"bold {primary_color}"))
|
|
518
|
+
time.sleep(delay)
|
|
519
|
+
|
|
520
|
+
# Print the final line with both command and description
|
|
521
|
+
console_instance.print(f" [{primary_color} bold]{cmd_text}[/{primary_color} bold]{padding}{desc_text}")
|
|
522
|
+
|