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.
Files changed (69) hide show
  1. arionxiv/__init__.py +40 -0
  2. arionxiv/__main__.py +10 -0
  3. arionxiv/arxiv_operations/__init__.py +0 -0
  4. arionxiv/arxiv_operations/client.py +225 -0
  5. arionxiv/arxiv_operations/fetcher.py +173 -0
  6. arionxiv/arxiv_operations/searcher.py +122 -0
  7. arionxiv/arxiv_operations/utils.py +293 -0
  8. arionxiv/cli/__init__.py +4 -0
  9. arionxiv/cli/commands/__init__.py +1 -0
  10. arionxiv/cli/commands/analyze.py +587 -0
  11. arionxiv/cli/commands/auth.py +365 -0
  12. arionxiv/cli/commands/chat.py +714 -0
  13. arionxiv/cli/commands/daily.py +482 -0
  14. arionxiv/cli/commands/fetch.py +217 -0
  15. arionxiv/cli/commands/library.py +295 -0
  16. arionxiv/cli/commands/preferences.py +426 -0
  17. arionxiv/cli/commands/search.py +254 -0
  18. arionxiv/cli/commands/settings_unified.py +1407 -0
  19. arionxiv/cli/commands/trending.py +41 -0
  20. arionxiv/cli/commands/welcome.py +168 -0
  21. arionxiv/cli/main.py +407 -0
  22. arionxiv/cli/ui/__init__.py +1 -0
  23. arionxiv/cli/ui/global_theme_manager.py +173 -0
  24. arionxiv/cli/ui/logo.py +127 -0
  25. arionxiv/cli/ui/splash.py +89 -0
  26. arionxiv/cli/ui/theme.py +32 -0
  27. arionxiv/cli/ui/theme_system.py +391 -0
  28. arionxiv/cli/utils/__init__.py +54 -0
  29. arionxiv/cli/utils/animations.py +522 -0
  30. arionxiv/cli/utils/api_client.py +583 -0
  31. arionxiv/cli/utils/api_config.py +505 -0
  32. arionxiv/cli/utils/command_suggestions.py +147 -0
  33. arionxiv/cli/utils/db_config_manager.py +254 -0
  34. arionxiv/github_actions_runner.py +206 -0
  35. arionxiv/main.py +23 -0
  36. arionxiv/prompts/__init__.py +9 -0
  37. arionxiv/prompts/prompts.py +247 -0
  38. arionxiv/rag_techniques/__init__.py +8 -0
  39. arionxiv/rag_techniques/basic_rag.py +1531 -0
  40. arionxiv/scheduler_daemon.py +139 -0
  41. arionxiv/server.py +1000 -0
  42. arionxiv/server_main.py +24 -0
  43. arionxiv/services/__init__.py +73 -0
  44. arionxiv/services/llm_client.py +30 -0
  45. arionxiv/services/llm_inference/__init__.py +58 -0
  46. arionxiv/services/llm_inference/groq_client.py +469 -0
  47. arionxiv/services/llm_inference/llm_utils.py +250 -0
  48. arionxiv/services/llm_inference/openrouter_client.py +564 -0
  49. arionxiv/services/unified_analysis_service.py +872 -0
  50. arionxiv/services/unified_auth_service.py +457 -0
  51. arionxiv/services/unified_config_service.py +456 -0
  52. arionxiv/services/unified_daily_dose_service.py +823 -0
  53. arionxiv/services/unified_database_service.py +1633 -0
  54. arionxiv/services/unified_llm_service.py +366 -0
  55. arionxiv/services/unified_paper_service.py +604 -0
  56. arionxiv/services/unified_pdf_service.py +522 -0
  57. arionxiv/services/unified_prompt_service.py +344 -0
  58. arionxiv/services/unified_scheduler_service.py +589 -0
  59. arionxiv/services/unified_user_service.py +954 -0
  60. arionxiv/utils/__init__.py +51 -0
  61. arionxiv/utils/api_helpers.py +200 -0
  62. arionxiv/utils/file_cleanup.py +150 -0
  63. arionxiv/utils/ip_helper.py +96 -0
  64. arionxiv-1.0.32.dist-info/METADATA +336 -0
  65. arionxiv-1.0.32.dist-info/RECORD +69 -0
  66. arionxiv-1.0.32.dist-info/WHEEL +5 -0
  67. arionxiv-1.0.32.dist-info/entry_points.txt +4 -0
  68. arionxiv-1.0.32.dist-info/licenses/LICENSE +21 -0
  69. arionxiv-1.0.32.dist-info/top_level.txt +1 -0
@@ -0,0 +1,482 @@
1
+ """Daily dose command for ArionXiv CLI - Uses hosted API"""
2
+
3
+ import asyncio
4
+ import logging
5
+ from datetime import datetime
6
+
7
+ import click
8
+ from rich.console import Console
9
+ from rich.panel import Panel
10
+ from rich.table import Table
11
+ from rich.prompt import Prompt
12
+ from rich.progress import Progress, SpinnerColumn, TextColumn
13
+
14
+ from ..ui.theme import (
15
+ create_themed_console, print_header, style_text,
16
+ print_success, print_warning, print_error, get_theme_colors
17
+ )
18
+ from ..utils.animations import left_to_right_reveal, stream_text_response
19
+ from ..utils.api_client import api_client, APIClientError
20
+ from ..utils.command_suggestions import show_command_suggestions
21
+ from ...services.unified_user_service import unified_user_service
22
+
23
+ console = create_themed_console()
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ def _check_auth() -> bool:
28
+ """Check if user is authenticated"""
29
+ if not unified_user_service.is_authenticated() and not api_client.is_authenticated():
30
+ print_error(console, "You must be logged in to use daily dose")
31
+ console.print("\nUse [bold]arionxiv login[/bold] to log in")
32
+ return False
33
+ return True
34
+
35
+
36
+ @click.command()
37
+ @click.option('--config', '-c', is_flag=True, help='Configure daily dose preferences')
38
+ @click.option('--run', '-r', is_flag=True, help='Run daily analysis now')
39
+ @click.option('--view', '-v', is_flag=True, help='View latest daily dose')
40
+ @click.option('--dose', '-d', is_flag=True, help='Get your daily dose (same as --view)')
41
+ def daily_command(config: bool, run: bool, view: bool, dose: bool):
42
+ """
43
+ Daily dose of research papers - Your personalized paper recommendations
44
+
45
+ Examples:
46
+ \b
47
+ arionxiv daily --dose # Get your daily dose
48
+ arionxiv daily --run # Generate new daily dose
49
+ arionxiv daily --config # Configure daily dose settings
50
+ arionxiv daily --view # View latest daily dose
51
+ """
52
+
53
+ async def _handle_daily():
54
+ print_header(console, "ArionXiv Daily Dose")
55
+
56
+ if not _check_auth():
57
+ return
58
+
59
+ colors = get_theme_colors()
60
+
61
+ if config:
62
+ console.print(f"[bold {colors['primary']}]Daily dose configuration is managed in settings[/]")
63
+ console.print(f"Use [bold {colors['primary']}]arionxiv settings daily[/] to configure")
64
+ elif run:
65
+ await _run_daily_dose()
66
+ elif view or dose:
67
+ await _view_daily_dose()
68
+ else:
69
+ await _show_daily_dashboard()
70
+
71
+ asyncio.run(_handle_daily())
72
+
73
+
74
+ async def _run_daily_dose():
75
+ """Run daily dose generation locally (API has timeout limits)"""
76
+ from ...services.unified_daily_dose_service import daily_dose_service
77
+
78
+ colors = get_theme_colors()
79
+
80
+ console.print(f"\n[bold {colors['primary']}]Running Daily Dose Generation[/]")
81
+ console.rule(style=f"bold {colors['primary']}")
82
+
83
+ try:
84
+ # Get user_id from local session
85
+ current_user = unified_user_service.get_current_user()
86
+ if not current_user:
87
+ print_error(console, "You must be logged in to run daily dose. Use 'arionxiv login' first.")
88
+ return
89
+
90
+ user_id = current_user.get("id") or current_user.get("user_id")
91
+ if not user_id:
92
+ print_error(console, "Could not determine user ID. Please login again.")
93
+ return
94
+
95
+ console.print(f"\n[dim {colors['primary']}]Fetching papers and generating personalized summary...[/dim]")
96
+
97
+ # Progress callback for real-time updates
98
+ def progress_callback(step: str, detail: str = ""):
99
+ if detail:
100
+ console.print(f" [{colors['secondary']}]• {step}:[/] [white]{detail}[/white]")
101
+ else:
102
+ console.print(f" [{colors['secondary']}]• {step}[/]")
103
+
104
+ # Execute locally with progress
105
+ result = await daily_dose_service.execute_daily_dose(user_id, progress_callback=progress_callback)
106
+
107
+ if result.get("success"):
108
+ dose = result.get("dose", {})
109
+ papers = dose.get("papers", [])
110
+ summary = dose.get("summary", {})
111
+
112
+ console.print(f"\n[bold {colors['primary']}]✓ Daily dose generated successfully![/]\n")
113
+
114
+ # Show summary
115
+ if summary:
116
+ total_papers = summary.get("total_papers", 0)
117
+ avg_relevance = summary.get("avg_relevance_score", 0)
118
+
119
+ console.print(f"[bold {colors['primary']}]Summary:[/]")
120
+ console.print(f" [bold {colors['primary']}]Papers analyzed: {total_papers}[/]")
121
+ console.print(f" [bold {colors['primary']}]Average relevance: {avg_relevance:.1f}/10[/]\n")
122
+
123
+ # Show paper count
124
+ if papers:
125
+ console.print(f"[bold {colors['primary']}]Papers found:[/] {len(papers)}")
126
+
127
+ console.print(f"\n[dim]View full details with:[/dim]")
128
+ console.print(f" [bold {colors['primary']}]arionxiv daily --dose[/]")
129
+ else:
130
+ msg = result.get("message", result.get("error", "Unknown error"))
131
+ print_error(console, f"Failed to generate daily dose: {msg}")
132
+
133
+ except Exception as e:
134
+ logger.error(f"Daily dose run error: {e}", exc_info=True)
135
+ print_error(console, str(e))
136
+
137
+
138
+ async def _view_daily_dose():
139
+ """View the latest daily dose via API"""
140
+ colors = get_theme_colors()
141
+
142
+ console.print(f"\n[bold {colors['primary']}]Your Latest Daily Dose[/]")
143
+ console.rule(style=f"bold {colors['primary']}")
144
+
145
+ try:
146
+ result = await api_client.get_daily_analysis()
147
+
148
+ if not result.get("success") or not result.get("dose"):
149
+ print_warning(console, "No daily dose available yet")
150
+ console.print(f"\nGenerate your first daily dose with:")
151
+ console.print(f" [bold {colors['primary']}]arionxiv daily --run[/]")
152
+ return
153
+
154
+ # Vercel API returns {"success": True, "dose": {...}}
155
+ daily_dose = result.get("dose")
156
+ papers = daily_dose.get("papers", [])
157
+ summary = daily_dose.get("summary", {})
158
+ generated_at = daily_dose.get("generated_at")
159
+
160
+ # Format generation time
161
+ if isinstance(generated_at, str):
162
+ try:
163
+ generated_at = datetime.fromisoformat(generated_at.replace('Z', '+00:00'))
164
+ except ValueError:
165
+ generated_at = datetime.utcnow()
166
+ elif not isinstance(generated_at, datetime):
167
+ generated_at = datetime.utcnow()
168
+
169
+ time_str = generated_at.strftime("%B %d, %Y at %H:%M")
170
+
171
+ header_text = f"Daily Dose - {time_str}"
172
+ left_to_right_reveal(console, header_text, style=f"bold {colors['primary']}", duration=1.0)
173
+
174
+ console.print(f"\n[bold {colors['primary']}]Papers found:[/] {summary.get('total_papers', len(papers))}")
175
+ console.print(f"[bold {colors['primary']}]Average relevance:[/] {summary.get('avg_relevance_score', 0):.1f}/10")
176
+
177
+ if not papers:
178
+ print_warning(console, "No papers in this daily dose.")
179
+ return
180
+
181
+ await _display_papers_list(papers, colors)
182
+ await _interactive_paper_view(papers, colors)
183
+
184
+ except APIClientError as e:
185
+ print_error(console, f"API Error: {e.message}")
186
+ except Exception as e:
187
+ logger.error(f"View daily dose error: {e}", exc_info=True)
188
+ error_panel = Panel(
189
+ f"[{colors['error']}]Error:[/] {str(e)}\n\n"
190
+ f"Failed to view your daily dose.\n"
191
+ f"Please try again.",
192
+ title="[bold]Daily Dose View Failed[/bold]",
193
+ border_style=f"bold {colors['error']}"
194
+ )
195
+ console.print(error_panel)
196
+
197
+
198
+ async def _display_papers_list(papers: list, colors: dict):
199
+ """Display list of papers in a table"""
200
+ console.print(f"\n[bold {colors['primary']}]Papers in Your Dose:[/]\n")
201
+
202
+ table = Table(show_header=True, header_style=f"bold {colors['primary']}", border_style=f"bold {colors['primary']}")
203
+ table.add_column("#", style="bold white", width=3)
204
+ table.add_column("Title", style="white", max_width=55)
205
+ table.add_column("Date", style="white", width=10)
206
+ table.add_column("Score", style="white", width=6, justify="center")
207
+ table.add_column("Category", style="white", width=12)
208
+
209
+ for i, paper in enumerate(papers, 1):
210
+ title = paper.get("title", "Unknown Title")
211
+ if len(title) > 52:
212
+ title = title[:49] + "..."
213
+
214
+ # Parse published date
215
+ published = paper.get("published", "")
216
+ if published:
217
+ try:
218
+ from datetime import datetime
219
+ if isinstance(published, str):
220
+ pub_date = datetime.fromisoformat(published.replace('Z', '+00:00'))
221
+ date_str = pub_date.strftime("%Y-%m-%d")
222
+ else:
223
+ date_str = str(published)[:10]
224
+ except:
225
+ date_str = str(published)[:10] if published else "N/A"
226
+ else:
227
+ date_str = "N/A"
228
+
229
+ score = paper.get("relevance_score", 0)
230
+ if isinstance(score, dict):
231
+ score = score.get("relevance_score", 5)
232
+
233
+ categories = paper.get("categories", [])
234
+ primary_cat = categories[0] if categories else "N/A"
235
+
236
+ if score >= 8:
237
+ score_style = f"bold {colors['success']}"
238
+ elif score >= 5:
239
+ score_style = f"bold {colors['primary']}"
240
+ else:
241
+ score_style = f"bold {colors['warning']}"
242
+
243
+ table.add_row(
244
+ str(i),
245
+ title,
246
+ date_str,
247
+ f"[{score_style}]{score}/10[/{score_style}]",
248
+ primary_cat
249
+ )
250
+
251
+ console.print(table)
252
+
253
+
254
+ async def _interactive_paper_view(papers: list, colors: dict):
255
+ """Interactive paper selection and analysis view"""
256
+ console.print(f"\n[bold {colors['primary']}]Select a paper to view its analysis (or 0 to exit):[/]")
257
+
258
+ while True:
259
+ try:
260
+ choice = Prompt.ask(f"[bold {colors['primary']}]Paper number[/]", default="0")
261
+
262
+ if choice == "0" or choice.lower() == "exit":
263
+ show_command_suggestions(console, context='daily')
264
+ break
265
+
266
+ idx = int(choice) - 1
267
+ if 0 <= idx < len(papers):
268
+ paper = papers[idx]
269
+ await _display_paper_analysis(paper, colors)
270
+ console.print(f"\n[bold {colors['primary']}]Enter another paper number or 0 to exit:[/]")
271
+ else:
272
+ print_warning(console, f"Please enter a number between 1 and {len(papers)}")
273
+
274
+ except ValueError:
275
+ print_warning(console, "Please enter a valid number")
276
+ except KeyboardInterrupt:
277
+ show_command_suggestions(console, context='daily')
278
+ break
279
+
280
+
281
+ async def _display_paper_analysis(paper: dict, colors: dict):
282
+ """Display detailed analysis for a paper with properly formatted sections"""
283
+ console.rule(style=f"bold {colors['primary']}")
284
+
285
+ title = paper.get("title", "Unknown Title")
286
+ authors = paper.get("authors", [])
287
+ categories = paper.get("categories", [])
288
+ arxiv_id = paper.get("arxiv_id", "")
289
+ analysis = paper.get("analysis", {})
290
+
291
+ left_to_right_reveal(console, title, style=f"bold {colors['primary']}", duration=1.0)
292
+
293
+ console.print(f"\n[bold {colors['primary']}]Authors:[/] {', '.join(authors[:3])}{'...' if len(authors) > 3 else ''}")
294
+ console.print(f"[bold {colors['primary']}]Categories:[/] {', '.join(categories[:3])}")
295
+ console.print(f"[bold {colors['primary']}]ArXiv ID:[/] {arxiv_id}")
296
+
297
+ if not analysis:
298
+ print_warning(console, "No analysis available for this paper.")
299
+ return
300
+
301
+ console.print(f"\n[bold {colors['primary']}]─── Analysis ───[/]\n")
302
+
303
+ # Helper to clean markdown formatting and section headers from LLM responses
304
+ def clean_text(text):
305
+ if not text:
306
+ return text
307
+ import re
308
+ # Remove markdown bold/italic markers using targeted regex (preserves math notation like A*B)
309
+ text = re.sub(r'\*\*(.+?)\*\*', r'\1', text) # Remove **bold**
310
+ text = re.sub(r'(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)', r'\1', text) # Remove *italic* but not **
311
+ text = re.sub(r'__(.+?)__', r'\1', text) # Remove __bold__
312
+ # Remove any remaining section headers that might have leaked through
313
+ text = re.sub(r'^(?:\d+\.\s*)?(?:SUMMARY|KEY\s*FINDINGS?|METHODOLOGY|SIGNIFICANCE|LIMITATIONS?|RELEVANCE\s*SCORE)[:\s]*', '', text, flags=re.IGNORECASE | re.MULTILINE)
314
+ # Remove leading/trailing whitespace
315
+ return text.strip()
316
+
317
+ # Summary section
318
+ summary = clean_text(analysis.get("summary", ""))
319
+ if summary:
320
+ console.print(f"[bold {colors['primary']}]Summary[/]")
321
+ console.print(f" {summary}\n")
322
+
323
+ # Key findings section - display as numbered list
324
+ key_findings = analysis.get("key_findings", [])
325
+ if key_findings:
326
+ console.print(f"[bold {colors['primary']}]Key Findings[/]")
327
+ if isinstance(key_findings, list):
328
+ import re
329
+ for i, finding in enumerate(key_findings, 1):
330
+ if finding:
331
+ cleaned = clean_text(finding)
332
+ # Remove leading numbers that might have leaked through (e.g., "1. " at start)
333
+ cleaned = re.sub(r'^[\d]+[\.\)]\s*', '', cleaned)
334
+ if cleaned:
335
+ console.print(f" - {cleaned}")
336
+ else:
337
+ console.print(f" {clean_text(key_findings)}")
338
+ console.print()
339
+
340
+ # Methodology section
341
+ methodology = clean_text(analysis.get("methodology", ""))
342
+ if methodology:
343
+ console.print(f"[bold {colors['primary']}]Methodology[/]")
344
+ console.print(f" {methodology}\n")
345
+
346
+ # Significance section
347
+ significance = clean_text(analysis.get("significance", ""))
348
+ if significance:
349
+ console.print(f"[bold {colors['primary']}]Significance[/]")
350
+ console.print(f" {significance}\n")
351
+
352
+ # Limitations section - displayed as text
353
+ limitations = analysis.get("limitations", "")
354
+ if limitations:
355
+ console.print(f"[bold {colors['primary']}]Limitations[/]")
356
+ console.print(f" {clean_text(limitations)}\n")
357
+
358
+ # Relevance score with color coding
359
+ score = analysis.get("relevance_score", 5)
360
+ if score >= 8:
361
+ score_style = colors['success']
362
+ elif score >= 5:
363
+ score_style = colors['primary']
364
+ else:
365
+ score_style = colors['warning']
366
+
367
+ console.print(f"[bold {colors['primary']}]Relevance Score:[/] [{score_style}]{score}/10[/{score_style}]")
368
+
369
+ pdf_url = paper.get("pdf_url", "")
370
+ if pdf_url:
371
+ console.print(f"\n[bold {colors['primary']}]PDF:[/] {pdf_url}")
372
+
373
+ console.rule(style=f"bold {colors['primary']}")
374
+
375
+
376
+ async def _show_daily_dashboard():
377
+ """Show daily dose dashboard via Vercel API"""
378
+ colors = get_theme_colors()
379
+
380
+ console.print(f"\n[bold {colors['primary']}]Daily Dose Dashboard[/]")
381
+ console.rule(style=f"bold {colors['primary']}")
382
+
383
+ try:
384
+ # Get settings from Vercel API (new dedicated endpoint)
385
+ settings_result = await api_client.get_daily_dose_settings()
386
+ settings = settings_result.get("settings", {}) if settings_result.get("success") else {}
387
+
388
+ # Get latest daily dose
389
+ dose_result = await api_client.get_daily_analysis()
390
+
391
+ # Settings panel
392
+ enabled = settings.get("enabled", False)
393
+ scheduled_time = settings.get("scheduled_time", "Not set")
394
+ max_papers = settings.get("max_papers", 5)
395
+ keywords = settings.get("keywords", [])
396
+
397
+ status_color = colors['primary'] if enabled else colors['warning']
398
+
399
+ settings_content = (
400
+ f"[bold]Status:[/bold] [bold {status_color}]{'Enabled' if enabled else 'Disabled'}[/bold {status_color}]\n"
401
+ f"[bold]Scheduled Time (UTC):[/bold] [bold {colors['primary']}] {scheduled_time if scheduled_time else 'Not configured'}[/]\n"
402
+ f"[bold]Max Papers:[/bold] [bold {colors['primary']}] {max_papers}[/]\n"
403
+ f"[bold]Keywords:[/bold] [bold {colors['primary']}] {', '.join(keywords[:5]) if keywords else 'None configured'}[/]"
404
+ )
405
+
406
+ settings_panel = Panel(
407
+ settings_content,
408
+ title=f"[bold {colors['primary']}]Settings[/]",
409
+ border_style=f"bold {colors['primary']}"
410
+ )
411
+ console.print(settings_panel)
412
+
413
+ # Latest dose status
414
+ if dose_result.get("success") and dose_result.get("dose"):
415
+ # Vercel API returns {"success": True, "dose": {...}}
416
+ daily_dose = dose_result.get("dose")
417
+ generated_at = daily_dose.get("generated_at")
418
+ summary = daily_dose.get("summary", {})
419
+
420
+ if isinstance(generated_at, str):
421
+ try:
422
+ generated_at = datetime.fromisoformat(generated_at.replace('Z', '+00:00'))
423
+ except ValueError:
424
+ generated_at = datetime.utcnow()
425
+ elif not isinstance(generated_at, datetime):
426
+ generated_at = datetime.utcnow()
427
+
428
+ time_str = generated_at.strftime("%B %d, %Y at %H:%M")
429
+
430
+ dose_content = (
431
+ f"[bold]Last Generated:[/bold] [bold {colors['primary']}]{time_str}[/]\n"
432
+ f"[bold]Papers Analyzed:[/bold] [bold {colors['primary']}]{summary.get('total_papers', 0)}[/]\n"
433
+ f"[bold]Avg Relevance:[/bold] [bold {colors['primary']}]{summary.get('avg_relevance_score', 0):.1f}/10[/]\n"
434
+ f"[bold]Status:[/bold] [bold {colors['primary']}]Ready[/]"
435
+ )
436
+
437
+ dose_panel = Panel(
438
+ dose_content,
439
+ title=f"[bold {colors['primary']}]Latest Dose[/]",
440
+ border_style=f"bold {colors['primary']}"
441
+ )
442
+ else:
443
+ dose_panel = Panel(
444
+ "No daily dose available yet.\n"
445
+ "Generate your first dose with the options below.",
446
+ title=f"[bold {colors['warning']}]Latest Dose[/]",
447
+ border_style=f"bold {colors['warning']}"
448
+ )
449
+
450
+ console.print(dose_panel)
451
+
452
+ # Quick actions
453
+ console.print(f"\n[bold {colors['primary']}]Quick Actions:[/]")
454
+
455
+ actions_table = Table(show_header=False, box=None, padding=(0, 2))
456
+ actions_table.add_column("Command", style=f"bold {colors['primary']}")
457
+ actions_table.add_column("Description", style="white")
458
+
459
+ actions_table.add_row("arionxiv daily --dose", "View your latest daily dose")
460
+ actions_table.add_row("arionxiv daily --run", "Generate new daily dose")
461
+ actions_table.add_row("arionxiv settings daily", "Configure daily dose settings")
462
+
463
+ console.print(actions_table)
464
+ show_command_suggestions(console, context='daily')
465
+
466
+ except APIClientError as e:
467
+ print_error(console, f"API Error: {e.message}")
468
+ except Exception as e:
469
+ logger.error(f"Dashboard error: {e}", exc_info=True)
470
+ error_panel = Panel(
471
+ f"[{colors['error']}]Error:[/{colors['error']}] {str(e)}\n\n"
472
+ f"Failed to load the daily dose dashboard.",
473
+ title="[bold]Dashboard Load Failed[/bold]",
474
+ border_style=f"bold {colors['error']}"
475
+ )
476
+ console.print(error_panel)
477
+
478
+
479
+ if __name__ == "__main__":
480
+ daily_command()
481
+
482
+