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,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
|
+
|