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,217 @@
|
|
|
1
|
+
"""Fetch command for ArionXiv CLI"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import asyncio
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
backend_path = Path(__file__).parent.parent.parent
|
|
9
|
+
sys.path.insert(0, str(backend_path))
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
|
14
|
+
from rich.panel import Panel
|
|
15
|
+
from rich.text import Text
|
|
16
|
+
from typing import Optional
|
|
17
|
+
|
|
18
|
+
from ...arxiv_operations.fetcher import arxiv_fetcher
|
|
19
|
+
from ...arxiv_operations.client import arxiv_client
|
|
20
|
+
from ...arxiv_operations.utils import ArxivUtils
|
|
21
|
+
from ...services.unified_pdf_service import pdf_processor
|
|
22
|
+
from ..utils.db_config_manager import db_config_manager as config_manager
|
|
23
|
+
from ..ui.theme import create_themed_console, print_header, style_text, print_success, print_error, print_warning, get_theme_colors
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
console = create_themed_console()
|
|
28
|
+
|
|
29
|
+
@click.command()
|
|
30
|
+
@click.argument('paper_id')
|
|
31
|
+
@click.option('--save-path', help='Custom save location for the PDF')
|
|
32
|
+
@click.option('--extract-text', is_flag=True, help='Extract text content from PDF')
|
|
33
|
+
@click.option('--no-download', is_flag=True, help='Skip PDF download, only fetch metadata')
|
|
34
|
+
def fetch_command(paper_id: str, save_path: str, extract_text: bool, no_download: bool):
|
|
35
|
+
"""
|
|
36
|
+
Fetch and download a research paper
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
\b
|
|
40
|
+
arionxiv fetch 2301.07041
|
|
41
|
+
arionxiv fetch 2301.07041 --save-path ./papers/
|
|
42
|
+
arionxiv fetch 2301.07041 --extract-text
|
|
43
|
+
arionxiv fetch 2301.07041 --no-download
|
|
44
|
+
"""
|
|
45
|
+
asyncio.run(_fetch_paper(paper_id, save_path, extract_text, no_download))
|
|
46
|
+
|
|
47
|
+
async def _fetch_paper(paper_id: str, save_path: Optional[str], extract_text: bool, no_download: bool):
|
|
48
|
+
"""Execute the paper fetch operation"""
|
|
49
|
+
|
|
50
|
+
logger.info(f"Fetching paper: {paper_id}, extract_text={extract_text}, no_download={no_download}")
|
|
51
|
+
|
|
52
|
+
# Get theme colors for consistent styling
|
|
53
|
+
colors = get_theme_colors()
|
|
54
|
+
|
|
55
|
+
# Clean up paper ID (remove version suffix if present)
|
|
56
|
+
clean_paper_id = ArxivUtils.normalize_arxiv_id(paper_id)
|
|
57
|
+
logger.debug(f"Normalized paper ID: {clean_paper_id}")
|
|
58
|
+
|
|
59
|
+
# Set up progress columns based on download flag
|
|
60
|
+
progress_columns = [
|
|
61
|
+
SpinnerColumn(),
|
|
62
|
+
TextColumn("[progress.description]{task.description}")
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
if not no_download:
|
|
66
|
+
progress_columns.append(BarColumn())
|
|
67
|
+
|
|
68
|
+
with Progress(*progress_columns, console=console) as progress:
|
|
69
|
+
|
|
70
|
+
# Step 1: Fetch metadata
|
|
71
|
+
metadata_task = progress.add_task("Fetching paper metadata...", total=None)
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
logger.debug("Fetching paper metadata from arXiv")
|
|
75
|
+
paper_metadata = arxiv_client.get_paper_by_id(clean_paper_id)
|
|
76
|
+
|
|
77
|
+
if not paper_metadata:
|
|
78
|
+
logger.warning(f"Paper not found: {paper_id}")
|
|
79
|
+
progress.remove_task(metadata_task)
|
|
80
|
+
console.print(f"Paper not found: {paper_id}", style=colors['error'])
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
logger.info(f"Metadata fetched for: {paper_metadata.get('title', 'Unknown')[:50]}")
|
|
84
|
+
progress.update(metadata_task, description="Metadata fetched")
|
|
85
|
+
progress.remove_task(metadata_task)
|
|
86
|
+
|
|
87
|
+
# Display paper info
|
|
88
|
+
_display_paper_info(paper_metadata)
|
|
89
|
+
|
|
90
|
+
if no_download:
|
|
91
|
+
logger.debug("Download skipped as requested")
|
|
92
|
+
console.print("\nMetadata fetch complete (download skipped)", style=colors['primary'])
|
|
93
|
+
return
|
|
94
|
+
|
|
95
|
+
# Step 2: Download PDF
|
|
96
|
+
download_task = progress.add_task("Downloading PDF...", total=100)
|
|
97
|
+
|
|
98
|
+
# Determine save path
|
|
99
|
+
if not save_path:
|
|
100
|
+
downloads_dir = Path(backend_path.parent) / "downloads"
|
|
101
|
+
downloads_dir.mkdir(exist_ok=True)
|
|
102
|
+
save_path = downloads_dir
|
|
103
|
+
else:
|
|
104
|
+
save_path = Path(save_path)
|
|
105
|
+
save_path.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
|
|
107
|
+
logger.debug(f"Save path: {save_path}")
|
|
108
|
+
|
|
109
|
+
# Generate filename
|
|
110
|
+
title_clean = "".join(c for c in paper_metadata.get("title", "paper") if c.isalnum() or c in (' ', '-', '_')).rstrip()
|
|
111
|
+
title_clean = title_clean.replace(' ', '_')[:50] # Limit length
|
|
112
|
+
filename = f"{clean_paper_id}_{title_clean}.pdf"
|
|
113
|
+
file_path = save_path / filename
|
|
114
|
+
|
|
115
|
+
# Download the PDF
|
|
116
|
+
pdf_url = paper_metadata.get("pdf_url", "")
|
|
117
|
+
if not pdf_url:
|
|
118
|
+
logger.error("No PDF URL found for paper")
|
|
119
|
+
progress.remove_task(download_task)
|
|
120
|
+
console.print("No PDF URL found for this paper", style=colors['error'])
|
|
121
|
+
return
|
|
122
|
+
|
|
123
|
+
# Use fetcher to download
|
|
124
|
+
logger.info(f"Downloading PDF from: {pdf_url}")
|
|
125
|
+
download_result = await arxiv_fetcher.download_paper(clean_paper_id, str(save_path))
|
|
126
|
+
|
|
127
|
+
progress.update(download_task, completed=100)
|
|
128
|
+
progress.remove_task(download_task)
|
|
129
|
+
|
|
130
|
+
if not download_result["success"]:
|
|
131
|
+
logger.error(f"Download failed: {download_result.get('error', 'Unknown error')}")
|
|
132
|
+
console.print(f"Download failed: {download_result.get('error', 'Unknown error')}", style=colors['error'])
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
downloaded_path = download_result["file_path"]
|
|
136
|
+
console.print(f"\nPDF downloaded to: {downloaded_path}", style=colors['primary'])
|
|
137
|
+
|
|
138
|
+
# Step 3: Extract text if requested
|
|
139
|
+
if extract_text:
|
|
140
|
+
extract_task = progress.add_task("Extracting text content...", total=None)
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
text_content = pdf_processor.extract_text(downloaded_path)
|
|
144
|
+
|
|
145
|
+
if text_content:
|
|
146
|
+
# Save text to file
|
|
147
|
+
text_file = Path(downloaded_path).with_suffix('.txt')
|
|
148
|
+
with open(text_file, 'w', encoding='utf-8') as f:
|
|
149
|
+
f.write(text_content)
|
|
150
|
+
|
|
151
|
+
progress.update(extract_task, description="Text extracted")
|
|
152
|
+
progress.remove_task(extract_task)
|
|
153
|
+
|
|
154
|
+
console.print(f"Text extracted to: {text_file}", style=colors['primary'])
|
|
155
|
+
|
|
156
|
+
# Show preview
|
|
157
|
+
preview = text_content[:500] + "..." if len(text_content) > 500 else text_content
|
|
158
|
+
console.print(f"\nText Preview:", style="bold")
|
|
159
|
+
console.print(Panel(preview, border_style="dim"))
|
|
160
|
+
else:
|
|
161
|
+
progress.remove_task(extract_task)
|
|
162
|
+
console.print("Could not extract text from PDF", style=colors['warning'])
|
|
163
|
+
|
|
164
|
+
except Exception as e:
|
|
165
|
+
progress.remove_task(extract_task)
|
|
166
|
+
console.print(f"Text extraction failed: {str(e)}", style=colors['error'])
|
|
167
|
+
|
|
168
|
+
# Show next steps
|
|
169
|
+
_show_next_steps(clean_paper_id, downloaded_path)
|
|
170
|
+
|
|
171
|
+
except Exception as e:
|
|
172
|
+
console.print(f"Fetch error: {str(e)}", style=colors['error'])
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
def _display_paper_info(paper):
|
|
176
|
+
"""Display paper information"""
|
|
177
|
+
# Get theme colors for consistent styling
|
|
178
|
+
colors = get_theme_colors()
|
|
179
|
+
|
|
180
|
+
console.print(f"\nPaper Information", style=f"bold {colors['primary']}")
|
|
181
|
+
|
|
182
|
+
title = paper.get("title", "Unknown Title")
|
|
183
|
+
console.print(f"Title: {title}", style="bold")
|
|
184
|
+
|
|
185
|
+
authors = paper.get("authors", [])
|
|
186
|
+
if isinstance(authors, list):
|
|
187
|
+
author_str = ", ".join(authors)
|
|
188
|
+
else:
|
|
189
|
+
author_str = str(authors)
|
|
190
|
+
console.print(f"Authors: {author_str}", style=colors['primary'])
|
|
191
|
+
|
|
192
|
+
categories = paper.get("categories", [])
|
|
193
|
+
category_str = ", ".join(categories) if categories else "Unknown"
|
|
194
|
+
console.print(f"Categories: {category_str}", style=colors['info'])
|
|
195
|
+
|
|
196
|
+
pub_date = paper.get("published", "Unknown")
|
|
197
|
+
console.print(f"Published: {pub_date}", style=colors['warning'])
|
|
198
|
+
|
|
199
|
+
arxiv_id = paper.get("arxiv_id", "")
|
|
200
|
+
if arxiv_id:
|
|
201
|
+
console.print(f"ArXiv ID: {arxiv_id}", style=colors['primary'])
|
|
202
|
+
|
|
203
|
+
abstract = paper.get("abstract", "No abstract available")
|
|
204
|
+
if len(abstract) > 300:
|
|
205
|
+
abstract = abstract[:297] + "..."
|
|
206
|
+
|
|
207
|
+
console.print(f"\nAbstract:", style="bold")
|
|
208
|
+
console.print(Panel(abstract, border_style="dim"))
|
|
209
|
+
|
|
210
|
+
def _show_next_steps(paper_id: str, file_path: str):
|
|
211
|
+
"""Show suggested next steps"""
|
|
212
|
+
console.print(f"\nNext Steps:", style="bold")
|
|
213
|
+
console.print(f" arionxiv analyze {paper_id} - Get AI analysis of this paper")
|
|
214
|
+
console.print(f" arionxiv chat - Start an interactive chat")
|
|
215
|
+
console.print(f" arionxiv library - View your research library")
|
|
216
|
+
|
|
217
|
+
console.print(f"\nFile location: {file_path}", style="dim")
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Library command for ArionXiv CLI - Uses hosted API"""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from typing import List, Dict, Any
|
|
7
|
+
|
|
8
|
+
import click
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
13
|
+
|
|
14
|
+
from ...arxiv_operations.client import arxiv_client
|
|
15
|
+
from ...arxiv_operations.utils import ArxivUtils
|
|
16
|
+
from ..utils.api_client import api_client, APIClientError
|
|
17
|
+
from ..ui.theme import create_themed_console, get_theme_colors
|
|
18
|
+
from ...services.unified_user_service import unified_user_service
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
console = create_themed_console()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class LibraryGroup(click.Group):
|
|
25
|
+
"""Custom Click group for library with proper error handling"""
|
|
26
|
+
|
|
27
|
+
def invoke(self, ctx):
|
|
28
|
+
try:
|
|
29
|
+
return super().invoke(ctx)
|
|
30
|
+
except click.UsageError as e:
|
|
31
|
+
self._show_error(e, ctx)
|
|
32
|
+
raise SystemExit(1)
|
|
33
|
+
|
|
34
|
+
def _show_error(self, error, ctx):
|
|
35
|
+
colors = get_theme_colors()
|
|
36
|
+
error_console = Console()
|
|
37
|
+
|
|
38
|
+
error_console.print()
|
|
39
|
+
error_console.print(f"[bold {colors['error']}]Invalid Library Command[/bold {colors['error']}]")
|
|
40
|
+
error_console.print(f"[bold {colors['error']}]{error}[/bold {colors['error']}]")
|
|
41
|
+
error_console.print()
|
|
42
|
+
|
|
43
|
+
error_console.print(f"[bold white]Available 'library' subcommands:[/bold white]")
|
|
44
|
+
for cmd_name in sorted(self.list_commands(ctx)):
|
|
45
|
+
cmd = self.get_command(ctx, cmd_name)
|
|
46
|
+
if cmd and not cmd.hidden:
|
|
47
|
+
help_text = cmd.get_short_help_str(limit=50)
|
|
48
|
+
error_console.print(f" [bold {colors['primary']}]{cmd_name}[/bold {colors['primary']}] {help_text}")
|
|
49
|
+
|
|
50
|
+
error_console.print()
|
|
51
|
+
error_console.print(f"Run [bold {colors['primary']}]arionxiv library --help[/bold {colors['primary']}] for more information.")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@click.group(cls=LibraryGroup, invoke_without_command=True)
|
|
55
|
+
@click.pass_context
|
|
56
|
+
def library_command(ctx):
|
|
57
|
+
"""
|
|
58
|
+
Manage your research library
|
|
59
|
+
|
|
60
|
+
Examples:
|
|
61
|
+
\b
|
|
62
|
+
arionxiv library # View library dashboard
|
|
63
|
+
arionxiv library list # List all papers
|
|
64
|
+
arionxiv library stats # View statistics
|
|
65
|
+
"""
|
|
66
|
+
if ctx.invoked_subcommand is None:
|
|
67
|
+
asyncio.run(_show_library_dashboard())
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _check_auth() -> bool:
|
|
71
|
+
"""Check if user is authenticated, show error if not"""
|
|
72
|
+
colors = get_theme_colors()
|
|
73
|
+
if not unified_user_service.is_authenticated() and not api_client.is_authenticated():
|
|
74
|
+
console.print("You must be logged in to use the library. Run: arionxiv login", style=f"bold {colors['error']}")
|
|
75
|
+
return False
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
async def _show_library_dashboard():
|
|
80
|
+
"""Show library analytics dashboard"""
|
|
81
|
+
colors = get_theme_colors()
|
|
82
|
+
|
|
83
|
+
if not _check_auth():
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
console.print(f"\n[bold {colors['primary']}]Library Dashboard[/bold {colors['primary']}]")
|
|
87
|
+
console.rule(style=f"bold {colors['primary']}")
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
# Fetch library and chat sessions data with progress spinner
|
|
91
|
+
with Progress(
|
|
92
|
+
SpinnerColumn(style=f"bold {colors['primary']}"),
|
|
93
|
+
TextColumn(f"[bold {colors['primary']}]{{task.description}}[/bold {colors['primary']}]"),
|
|
94
|
+
console=console,
|
|
95
|
+
transient=True
|
|
96
|
+
) as progress:
|
|
97
|
+
task = progress.add_task("Fetching your library data...", total=None)
|
|
98
|
+
library_result = await api_client.get_library(limit=100)
|
|
99
|
+
progress.update(task, description="Fetching chat sessions...")
|
|
100
|
+
sessions_result = await api_client.get_chat_sessions()
|
|
101
|
+
progress.update(task, description="Done!")
|
|
102
|
+
|
|
103
|
+
papers = library_result.get("papers", []) if library_result.get("success") else []
|
|
104
|
+
sessions = sessions_result.get("sessions", []) if sessions_result.get("success") else []
|
|
105
|
+
|
|
106
|
+
total_papers = len(papers)
|
|
107
|
+
active_sessions = len(sessions)
|
|
108
|
+
|
|
109
|
+
# Count papers by read status
|
|
110
|
+
unread = sum(1 for p in papers if p.get("read_status") == "unread")
|
|
111
|
+
reading = sum(1 for p in papers if p.get("read_status") == "reading")
|
|
112
|
+
completed = sum(1 for p in papers if p.get("read_status") == "completed")
|
|
113
|
+
|
|
114
|
+
# Get recent papers (added in last 7 days)
|
|
115
|
+
from datetime import timedelta
|
|
116
|
+
week_ago = datetime.utcnow() - timedelta(days=7)
|
|
117
|
+
recent_papers = 0
|
|
118
|
+
for p in papers:
|
|
119
|
+
added_at = p.get("added_at")
|
|
120
|
+
if added_at:
|
|
121
|
+
if isinstance(added_at, str):
|
|
122
|
+
try:
|
|
123
|
+
added_at = datetime.fromisoformat(added_at.replace('Z', '+00:00').replace('+00:00', ''))
|
|
124
|
+
except:
|
|
125
|
+
continue
|
|
126
|
+
if isinstance(added_at, datetime) and added_at > week_ago:
|
|
127
|
+
recent_papers += 1
|
|
128
|
+
|
|
129
|
+
# Stats panel
|
|
130
|
+
stats_content = (
|
|
131
|
+
f"[bold]Total Papers Saved:[/bold] [bold {colors['primary']}]{total_papers}[/bold {colors['primary']}]\n"
|
|
132
|
+
f"[bold]Added This Week:[/bold] [bold {colors['primary']}]{recent_papers}[/bold {colors['primary']}]\n"
|
|
133
|
+
f"[bold]Active Chat Sessions:[/bold] [bold {colors['primary']}]{active_sessions}[/bold {colors['primary']}]\n\n"
|
|
134
|
+
f"[bold]Reading Progress:[/bold]\n"
|
|
135
|
+
f" [bold {colors['primary']}]Unread:[/bold {colors['primary']}] {unread} "
|
|
136
|
+
f"[bold {colors['primary']}]Reading:[/bold {colors['primary']}] {reading} "
|
|
137
|
+
f"[bold {colors['primary']}]Completed:[/bold {colors['primary']}] {completed}"
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
console.print(Panel(
|
|
141
|
+
stats_content,
|
|
142
|
+
title=f"[bold {colors['primary']}]Library Statistics[/bold {colors['primary']}]",
|
|
143
|
+
border_style=f"bold {colors['primary']}"
|
|
144
|
+
))
|
|
145
|
+
|
|
146
|
+
# Quick actions
|
|
147
|
+
console.print(f"\n[bold {colors['primary']}]Quick Actions:[/bold {colors['primary']}]")
|
|
148
|
+
|
|
149
|
+
actions_table = Table(show_header=False, box=None, padding=(0, 2))
|
|
150
|
+
actions_table.add_column("Command", style=f"bold {colors['primary']}")
|
|
151
|
+
actions_table.add_column("Description", style="white")
|
|
152
|
+
|
|
153
|
+
actions_table.add_row("arionxiv library list", "View all saved papers")
|
|
154
|
+
actions_table.add_row("arionxiv library stats", "View detailed statistics")
|
|
155
|
+
actions_table.add_row("arionxiv chat", "Start chatting with a paper")
|
|
156
|
+
|
|
157
|
+
console.print(actions_table)
|
|
158
|
+
|
|
159
|
+
except APIClientError as e:
|
|
160
|
+
console.print(f"Error loading library: {e.message}", style=f"bold {colors['error']}")
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logger.error(f"Library dashboard error: {e}", exc_info=True)
|
|
163
|
+
console.print(f"Error: {str(e)}", style=f"bold {colors['error']}")
|
|
164
|
+
|
|
165
|
+
@library_command.command()
|
|
166
|
+
@click.option('--tags', help='Filter by tags')
|
|
167
|
+
@click.option('--category', help='Filter by category')
|
|
168
|
+
@click.option('--status', type=click.Choice(['read', 'unread', 'reading']), help='Filter by read status')
|
|
169
|
+
def list(tags: str, category: str, status: str):
|
|
170
|
+
"""List papers in your library"""
|
|
171
|
+
|
|
172
|
+
async def _list_papers():
|
|
173
|
+
colors = get_theme_colors()
|
|
174
|
+
|
|
175
|
+
if not _check_auth():
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
result = await api_client.get_library(limit=100)
|
|
180
|
+
|
|
181
|
+
if not result.get("success"):
|
|
182
|
+
console.print(result.get("message", "Failed to fetch library"), style=f"bold {colors['error']}")
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
library = result.get("papers", [])
|
|
186
|
+
|
|
187
|
+
if not library:
|
|
188
|
+
console.print("Your library is empty. Use 'arionxiv library add <paper_id>' to add papers.", style=f"bold {colors['warning']}")
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
# Apply local filters if specified
|
|
192
|
+
if category:
|
|
193
|
+
library = [p for p in library if category in p.get("categories", [])]
|
|
194
|
+
if status:
|
|
195
|
+
library = [p for p in library if p.get("read_status") == status]
|
|
196
|
+
if tags:
|
|
197
|
+
tag_list = [t.strip() for t in tags.split(',')]
|
|
198
|
+
library = [p for p in library if any(t in p.get("tags", []) for t in tag_list)]
|
|
199
|
+
|
|
200
|
+
if not library:
|
|
201
|
+
console.print("No papers match your filters.", style=f"bold {colors['warning']}")
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
user = unified_user_service.get_current_user()
|
|
205
|
+
user_name = user.get("user_name", "User") if user else "User"
|
|
206
|
+
|
|
207
|
+
table = Table(title=f"{user_name}'s Library", header_style=f"bold {colors['primary']}")
|
|
208
|
+
table.add_column("#", style=f"bold {colors['primary']}", width=4)
|
|
209
|
+
table.add_column("Paper ID", style=f"bold {colors['primary']}", width=12)
|
|
210
|
+
table.add_column("Title", style=f"bold {colors['primary']}", width=50)
|
|
211
|
+
table.add_column("Status", style=f"bold {colors['primary']}", width=10)
|
|
212
|
+
table.add_column("Added", style=f"bold {colors['primary']}", width=12)
|
|
213
|
+
|
|
214
|
+
for i, item in enumerate(library[:20], 1):
|
|
215
|
+
title = item.get('title', 'Unknown')
|
|
216
|
+
|
|
217
|
+
added = item.get('added_at', '')
|
|
218
|
+
if isinstance(added, datetime):
|
|
219
|
+
added_str = added.strftime('%Y-%m-%d')
|
|
220
|
+
else:
|
|
221
|
+
added_str = str(added)[:10] if added else 'Unknown'
|
|
222
|
+
|
|
223
|
+
table.add_row(
|
|
224
|
+
str(i),
|
|
225
|
+
item.get('arxiv_id', 'Unknown')[:12],
|
|
226
|
+
title,
|
|
227
|
+
item.get('read_status', 'unread'),
|
|
228
|
+
added_str
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
console.print(table)
|
|
232
|
+
console.print(f"\nTotal papers: {len(library)}", style=f"bold {colors['primary']}")
|
|
233
|
+
|
|
234
|
+
except APIClientError as e:
|
|
235
|
+
console.print(f"Error: {e.message}", style=f"bold {colors['error']}")
|
|
236
|
+
|
|
237
|
+
asyncio.run(_list_papers())
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@library_command.command()
|
|
241
|
+
def stats():
|
|
242
|
+
"""Show library statistics"""
|
|
243
|
+
|
|
244
|
+
async def _show_stats():
|
|
245
|
+
colors = get_theme_colors()
|
|
246
|
+
|
|
247
|
+
if not _check_auth():
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
try:
|
|
251
|
+
result = await api_client.get_library(limit=100)
|
|
252
|
+
|
|
253
|
+
if not result.get("success"):
|
|
254
|
+
console.print(result.get("message", "Failed to fetch library"), style=f"bold {colors['error']}")
|
|
255
|
+
return
|
|
256
|
+
|
|
257
|
+
library = result.get("papers", [])
|
|
258
|
+
|
|
259
|
+
if not library:
|
|
260
|
+
console.print("Your library is empty.", style=f"bold {colors['warning']}")
|
|
261
|
+
return
|
|
262
|
+
|
|
263
|
+
total = len(library)
|
|
264
|
+
|
|
265
|
+
category_counts: Dict[str, int] = {}
|
|
266
|
+
for paper in library:
|
|
267
|
+
for cat in paper.get("categories", []):
|
|
268
|
+
category_counts[cat] = category_counts.get(cat, 0) + 1
|
|
269
|
+
|
|
270
|
+
top_categories = sorted(category_counts.items(), key=lambda x: x[1], reverse=True)[:5]
|
|
271
|
+
|
|
272
|
+
status_counts: Dict[str, int] = {}
|
|
273
|
+
for paper in library:
|
|
274
|
+
s = paper.get("read_status", "unread")
|
|
275
|
+
status_counts[s] = status_counts.get(s, 0) + 1
|
|
276
|
+
|
|
277
|
+
user = unified_user_service.get_current_user()
|
|
278
|
+
user_name = user.get("user_name", "User") if user else "User"
|
|
279
|
+
|
|
280
|
+
stats_text = f"[bold]Total Papers:[/bold] {total}\n\n"
|
|
281
|
+
stats_text += "[bold]Top Categories:[/bold]\n"
|
|
282
|
+
stats_text += "\n".join([f" - {cat}: {count}" for cat, count in top_categories])
|
|
283
|
+
stats_text += "\n\n[bold]Reading Status:[/bold]\n"
|
|
284
|
+
stats_text += "\n".join([f" - {s}: {count}" for s, count in status_counts.items()])
|
|
285
|
+
|
|
286
|
+
console.print(Panel(
|
|
287
|
+
stats_text,
|
|
288
|
+
title=f"{user_name}'s Library Statistics",
|
|
289
|
+
border_style=f"bold {colors['primary']}"
|
|
290
|
+
))
|
|
291
|
+
|
|
292
|
+
except APIClientError as e:
|
|
293
|
+
console.print(f"Error: {e.message}", style=f"bold {colors['error']}")
|
|
294
|
+
|
|
295
|
+
asyncio.run(_show_stats())
|