Spaces:
Sleeping
Sleeping
DVampire
commited on
Commit
·
21fd477
1
Parent(s):
360e958
update
Browse files- .gitignore +63 -0
- DEPLOYMENT.md +107 -0
- Dockerfile +0 -0
- README.md +146 -11
- agents/__init__.py +2 -0
- agents/evaluator.py +162 -0
- agents/prompt.py +397 -0
- app.py +13 -0
- cli.py +65 -0
- frontend/index.html +88 -0
- frontend/main.js +459 -0
- frontend/paper.html +88 -0
- frontend/paper.js +578 -0
- frontend/styles.css +1375 -0
- requirements.txt +11 -0
- server.py +731 -0
.gitignore
ADDED
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Python
|
2 |
+
__pycache__/
|
3 |
+
*.py[cod]
|
4 |
+
*$py.class
|
5 |
+
*.so
|
6 |
+
.Python
|
7 |
+
build/
|
8 |
+
develop-eggs/
|
9 |
+
dist/
|
10 |
+
downloads/
|
11 |
+
eggs/
|
12 |
+
.eggs/
|
13 |
+
lib/
|
14 |
+
lib64/
|
15 |
+
parts/
|
16 |
+
sdist/
|
17 |
+
var/
|
18 |
+
wheels/
|
19 |
+
*.egg-info/
|
20 |
+
.installed.cfg
|
21 |
+
*.egg
|
22 |
+
MANIFEST
|
23 |
+
|
24 |
+
# Virtual environments
|
25 |
+
venv/
|
26 |
+
env/
|
27 |
+
ENV/
|
28 |
+
env.bak/
|
29 |
+
venv.bak/
|
30 |
+
|
31 |
+
# IDE
|
32 |
+
.vscode/
|
33 |
+
.idea/
|
34 |
+
*.swp
|
35 |
+
*.swo
|
36 |
+
*~
|
37 |
+
|
38 |
+
# OS
|
39 |
+
.DS_Store
|
40 |
+
.DS_Store?
|
41 |
+
._*
|
42 |
+
.Spotlight-V100
|
43 |
+
.Trashes
|
44 |
+
ehthumbs.db
|
45 |
+
Thumbs.db
|
46 |
+
|
47 |
+
# Project specific
|
48 |
+
*.db
|
49 |
+
papers_cache.db
|
50 |
+
workdir/
|
51 |
+
data/pdfs/
|
52 |
+
.env
|
53 |
+
.env.local
|
54 |
+
.env.*.local
|
55 |
+
.env.example
|
56 |
+
|
57 |
+
# Logs
|
58 |
+
*.log
|
59 |
+
logs/
|
60 |
+
|
61 |
+
# Temporary files
|
62 |
+
*.tmp
|
63 |
+
*.temp
|
DEPLOYMENT.md
ADDED
@@ -0,0 +1,107 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# PaperIndex Deployment Guide
|
2 |
+
|
3 |
+
## Hugging Face Spaces Deployment
|
4 |
+
|
5 |
+
### 1. Create Space
|
6 |
+
|
7 |
+
1. Visit [Hugging Face Spaces](https://huggingface.co/spaces)
|
8 |
+
2. Click "Create new Space"
|
9 |
+
3. Select "Docker" as SDK
|
10 |
+
4. Set Space name (e.g., "paperindex")
|
11 |
+
5. Choose "Public" or "Private"
|
12 |
+
|
13 |
+
### 2. Secure API Key Configuration
|
14 |
+
|
15 |
+
**Important: Never hardcode API keys in your code!**
|
16 |
+
|
17 |
+
#### Method 1: Use HF Spaces Secrets (Recommended)
|
18 |
+
|
19 |
+
1. On your Space page, click the "Settings" tab
|
20 |
+
2. Find "Repository secrets" in the left menu
|
21 |
+
3. Click "New secret"
|
22 |
+
4. Add the following secret:
|
23 |
+
- **Name**: `ANTHROPIC_API_KEY`
|
24 |
+
- **Value**: Your Anthropic API key
|
25 |
+
|
26 |
+
#### Method 2: Use Environment Variables (Local Development Only)
|
27 |
+
|
28 |
+
Create a `.env` file (do not commit to Git):
|
29 |
+
```bash
|
30 |
+
ANTHROPIC_API_KEY=your_api_key_here
|
31 |
+
```
|
32 |
+
|
33 |
+
### 3. Push Code
|
34 |
+
|
35 |
+
```bash
|
36 |
+
# Ensure database files use Git LFS
|
37 |
+
git lfs track "*.db"
|
38 |
+
git add .gitattributes
|
39 |
+
|
40 |
+
# Commit and push
|
41 |
+
git add .
|
42 |
+
git commit -m "Update deployment configuration"
|
43 |
+
git push origin main
|
44 |
+
```
|
45 |
+
|
46 |
+
### 4. Verify Deployment
|
47 |
+
|
48 |
+
1. Wait for HF Spaces to auto-build (usually 2-5 minutes)
|
49 |
+
2. Visit your Space URL: `https://huggingface.co/spaces/your-username/paperindex`
|
50 |
+
3. Check if the application is running properly
|
51 |
+
|
52 |
+
## Local Docker Deployment
|
53 |
+
|
54 |
+
### 1. Build Image
|
55 |
+
|
56 |
+
```bash
|
57 |
+
docker build -t paperindex .
|
58 |
+
```
|
59 |
+
|
60 |
+
### 2. Run Container
|
61 |
+
|
62 |
+
```bash
|
63 |
+
# Using environment variables
|
64 |
+
docker run -d -p 7860:7860 \
|
65 |
+
-e ANTHROPIC_API_KEY=your_api_key_here \
|
66 |
+
--name paperindex-app paperindex
|
67 |
+
|
68 |
+
# Or using .env file
|
69 |
+
docker run -d -p 7860:7860 \
|
70 |
+
--env-file .env \
|
71 |
+
--name paperindex-app paperindex
|
72 |
+
```
|
73 |
+
|
74 |
+
### 3. Access Application
|
75 |
+
|
76 |
+
Open browser and visit: `http://localhost:7860`
|
77 |
+
|
78 |
+
## Security Considerations
|
79 |
+
|
80 |
+
1. **Never** hardcode API keys in your code
|
81 |
+
2. **Never** commit `.env` files to Git repository
|
82 |
+
3. Use HF Spaces Secrets to store sensitive information
|
83 |
+
4. Regularly rotate API keys
|
84 |
+
5. Monitor API usage
|
85 |
+
|
86 |
+
## Troubleshooting
|
87 |
+
|
88 |
+
### Common Issues
|
89 |
+
|
90 |
+
1. **API Key Error**
|
91 |
+
- Check if HF Spaces Secrets are set correctly
|
92 |
+
- Verify API key is valid
|
93 |
+
|
94 |
+
2. **Build Failure**
|
95 |
+
- Check Dockerfile syntax
|
96 |
+
- View build logs
|
97 |
+
|
98 |
+
3. **Application Won't Start**
|
99 |
+
- Check port configuration
|
100 |
+
- View container logs: `docker logs paperindex-app`
|
101 |
+
|
102 |
+
### Getting Help
|
103 |
+
|
104 |
+
If you encounter issues:
|
105 |
+
1. Check HF Spaces build logs
|
106 |
+
2. View application logs
|
107 |
+
3. Report issues in GitHub Issues
|
Dockerfile
ADDED
File without changes
|
README.md
CHANGED
@@ -1,11 +1,146 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# PaperIndex
|
2 |
+
|
3 |
+
A beautiful web application for browsing and evaluating daily papers from Hugging Face, featuring a modern UI inspired by Hugging Face's design.
|
4 |
+
|
5 |
+
## Features
|
6 |
+
|
7 |
+
- 📰 **Daily Papers**: Browse the latest papers from Hugging Face
|
8 |
+
- 🎨 **Beautiful UI**: Modern design with day/night theme switching
|
9 |
+
- 📊 **Paper Evaluation**: Detailed evaluation pages with radar charts
|
10 |
+
- 🔄 **Smart Caching**: Intelligent caching system for better performance
|
11 |
+
- 📱 **Responsive**: Works perfectly on desktop and mobile devices
|
12 |
+
|
13 |
+
## Live Demo
|
14 |
+
|
15 |
+
🌐 **Hugging Face Spaces**: [PaperIndex Demo](https://huggingface.co/spaces/your-username/paperindex)
|
16 |
+
|
17 |
+
## Local Development
|
18 |
+
|
19 |
+
### Prerequisites
|
20 |
+
|
21 |
+
- Python 3.8+
|
22 |
+
- pip
|
23 |
+
|
24 |
+
### Installation
|
25 |
+
|
26 |
+
1. Clone the repository:
|
27 |
+
```bash
|
28 |
+
git clone https://github.com/your-username/PaperIndex.git
|
29 |
+
cd PaperIndex
|
30 |
+
```
|
31 |
+
|
32 |
+
2. Install dependencies:
|
33 |
+
```bash
|
34 |
+
pip install -r requirements.txt
|
35 |
+
```
|
36 |
+
|
37 |
+
3. Run the development server:
|
38 |
+
```bash
|
39 |
+
python -m uvicorn server:app --reload --host 0.0.0.0 --port 8000
|
40 |
+
```
|
41 |
+
|
42 |
+
4. Open your browser and visit: `http://localhost:8000`
|
43 |
+
|
44 |
+
## Deployment
|
45 |
+
|
46 |
+
### Hugging Face Spaces (Recommended) - FREE!
|
47 |
+
|
48 |
+
**Hugging Face Spaces 完全免费,包含:**
|
49 |
+
- ✅ 无限个人项目
|
50 |
+
- ✅ 自动 HTTPS
|
51 |
+
- ✅ 全球 CDN
|
52 |
+
- ✅ 自动部署
|
53 |
+
- ✅ 自定义域名支持
|
54 |
+
|
55 |
+
#### 部署步骤:
|
56 |
+
|
57 |
+
1. **创建 Space**:
|
58 |
+
- 访问 [Hugging Face Spaces](https://huggingface.co/spaces)
|
59 |
+
- 点击 "Create new Space"
|
60 |
+
- 选择 "Gradio" 作为 SDK
|
61 |
+
- 设置 Space 名称(如 "paperindex")
|
62 |
+
- 选择 "Public" 或 "Private"
|
63 |
+
|
64 |
+
2. **配置访问令牌**:
|
65 |
+
```bash
|
66 |
+
# 复制环境变量模板
|
67 |
+
cp env.example .env
|
68 |
+
|
69 |
+
# 编辑 .env 文件,添加你的 Hugging Face 访问令牌
|
70 |
+
# 获取令牌:https://huggingface.co/settings/tokens
|
71 |
+
# 确保选择 "Write" 权限
|
72 |
+
```
|
73 |
+
|
74 |
+
3. **一键部署**:
|
75 |
+
```bash
|
76 |
+
# 使用部署脚本
|
77 |
+
./deploy_to_hf.sh your-username paperindex
|
78 |
+
```
|
79 |
+
|
80 |
+
4. **自动部署**:
|
81 |
+
- Spaces 会自动检测 `app.py` 作为入口点
|
82 |
+
- 自动安装 `requirements.txt` 中的依赖
|
83 |
+
- 自动启动服务
|
84 |
+
|
85 |
+
5. **访问你的应用**:
|
86 |
+
- 你的应用将在以下地址可用:`https://huggingface.co/spaces/your-username/paperindex`
|
87 |
+
- 部署完成后,你可以分享这个链接给任何人
|
88 |
+
|
89 |
+
#### 重要文件说明:
|
90 |
+
|
91 |
+
- `app.py` - Spaces 的入口点文件
|
92 |
+
- `requirements.txt` - Python 依赖
|
93 |
+
- `server.py` - 主要的 FastAPI 应用
|
94 |
+
- `frontend/` - 静态前端文件
|
95 |
+
|
96 |
+
### Alternative Deployment Options
|
97 |
+
|
98 |
+
- **Vercel**: 免费计划,无限个人项目
|
99 |
+
- **Railway**: 简单部署,$5/月免费额度
|
100 |
+
- **Render**: 免费层级可用
|
101 |
+
|
102 |
+
## Project Structure
|
103 |
+
|
104 |
+
```
|
105 |
+
PaperIndex/
|
106 |
+
├── frontend/ # Static frontend files
|
107 |
+
│ ├── index.html # Main page
|
108 |
+
│ ├── paper.html # Paper evaluation page
|
109 |
+
│ ├── main.js # Main page JavaScript
|
110 |
+
│ ├── paper.js # Evaluation page JavaScript
|
111 |
+
│ └── styles.css # Shared styles
|
112 |
+
├── agents/ # AI evaluation agents
|
113 |
+
├── workdir/ # Evaluation data storage
|
114 |
+
├── data/ # Data files
|
115 |
+
├── server.py # FastAPI backend
|
116 |
+
├── app.py # Hugging Face Spaces entry point
|
117 |
+
├── requirements.txt # Python dependencies
|
118 |
+
└── README.md # This file
|
119 |
+
```
|
120 |
+
|
121 |
+
## API Endpoints
|
122 |
+
|
123 |
+
- `GET /` - Main page
|
124 |
+
- `GET /paper.html?id={paper_id}` - Paper evaluation page
|
125 |
+
- `GET /api/daily?date_str={date}` - Get daily papers
|
126 |
+
- `GET /api/eval/{paper_id}` - Get paper evaluation
|
127 |
+
- `GET /api/cache/status` - Get cache status
|
128 |
+
- `POST /api/cache/clear` - Clear cache
|
129 |
+
|
130 |
+
## Contributing
|
131 |
+
|
132 |
+
1. Fork the repository
|
133 |
+
2. Create a feature branch
|
134 |
+
3. Make your changes
|
135 |
+
4. Submit a pull request
|
136 |
+
|
137 |
+
## License
|
138 |
+
|
139 |
+
MIT License - see LICENSE file for details.
|
140 |
+
|
141 |
+
## Acknowledgments
|
142 |
+
|
143 |
+
- Inspired by Hugging Face's beautiful paper browsing interface
|
144 |
+
- Built with FastAPI and modern web technologies
|
145 |
+
|
146 |
+
|
agents/__init__.py
ADDED
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
1 |
+
|
2 |
+
|
agents/evaluator.py
ADDED
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from __future__ import annotations
|
2 |
+
|
3 |
+
import base64
|
4 |
+
import os
|
5 |
+
import json
|
6 |
+
import asyncio
|
7 |
+
from typing import Any, Dict, List, Optional
|
8 |
+
from pathlib import Path
|
9 |
+
from datetime import datetime
|
10 |
+
|
11 |
+
from anthropic import Anthropic
|
12 |
+
from langgraph.graph import END, StateGraph
|
13 |
+
from pydantic import BaseModel, Field
|
14 |
+
from agents.prompt import REVIEWER_SYSTEM_PROMPT, EVALUATION_PROMPT_TEMPLATE, TOOLS, TOOL_CHOICE
|
15 |
+
|
16 |
+
# Import API key function from server module
|
17 |
+
import sys
|
18 |
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
19 |
+
from server import get_anthropic_api_key
|
20 |
+
|
21 |
+
|
22 |
+
class ConversationState(BaseModel):
|
23 |
+
"""State for the conversation graph"""
|
24 |
+
messages: List[Dict[str, Any]] = Field(default_factory=list)
|
25 |
+
response_text: str = ""
|
26 |
+
tool_result: Optional[Dict[str, Any]] = None
|
27 |
+
|
28 |
+
|
29 |
+
def _load_pdf_as_content(pdf_path: str) -> Dict[str, Any]:
|
30 |
+
if os.path.exists(pdf_path):
|
31 |
+
with open(pdf_path, "rb") as f:
|
32 |
+
data_b64 = base64.b64encode(f.read()).decode("utf-8")
|
33 |
+
return {
|
34 |
+
"type": "document",
|
35 |
+
"source": {
|
36 |
+
"type": "base64",
|
37 |
+
"media_type": "application/pdf",
|
38 |
+
"data": data_b64,
|
39 |
+
},
|
40 |
+
}
|
41 |
+
if pdf_path.startswith("http"):
|
42 |
+
return {
|
43 |
+
"type": "document",
|
44 |
+
"source": {
|
45 |
+
"type": "url",
|
46 |
+
"url": pdf_path,
|
47 |
+
},
|
48 |
+
}
|
49 |
+
raise FileNotFoundError(f"PDF not found or invalid path: {pdf_path}")
|
50 |
+
|
51 |
+
|
52 |
+
class Evaluator:
|
53 |
+
def __init__(self, api_key: Optional[str] = None):
|
54 |
+
api_key = api_key or get_anthropic_api_key()
|
55 |
+
if not api_key:
|
56 |
+
raise ValueError("Anthropic API key is required. Please set HF_SECRET_ANTHROPIC_API_KEY in Hugging Face Spaces secrets or ANTHROPIC_API_KEY environment variable.")
|
57 |
+
self.client = Anthropic(api_key=api_key)
|
58 |
+
self.system_prompt = REVIEWER_SYSTEM_PROMPT
|
59 |
+
self.eval_template = EVALUATION_PROMPT_TEMPLATE
|
60 |
+
|
61 |
+
async def __call__(self, state: ConversationState) -> ConversationState:
|
62 |
+
"""Evaluate the paper using the conversation state"""
|
63 |
+
# Prepare messages for the API call
|
64 |
+
messages = [{"role": "system", "content": self.system_prompt}]
|
65 |
+
messages.extend(state.messages)
|
66 |
+
|
67 |
+
# Add the evaluation prompt
|
68 |
+
messages.append({
|
69 |
+
"role": "user",
|
70 |
+
"content": self.eval_template
|
71 |
+
})
|
72 |
+
|
73 |
+
try:
|
74 |
+
# Call Anthropic API with tools
|
75 |
+
response = await self.client.messages.create(
|
76 |
+
model="claude-3-5-sonnet-20241022",
|
77 |
+
max_tokens=4000,
|
78 |
+
messages=messages,
|
79 |
+
tools=TOOLS,
|
80 |
+
tool_choice=TOOL_CHOICE
|
81 |
+
)
|
82 |
+
|
83 |
+
# Process the response
|
84 |
+
state.messages.append({
|
85 |
+
"role": "assistant",
|
86 |
+
"content": response.content[0].text if response.content else ""
|
87 |
+
})
|
88 |
+
|
89 |
+
# Extract tool result if present
|
90 |
+
tool_result = None
|
91 |
+
if response.content and hasattr(response.content[0], 'tool_use'):
|
92 |
+
tool_use = response.content[0].tool_use
|
93 |
+
if tool_use:
|
94 |
+
tool_result = json.loads(tool_use.input)
|
95 |
+
|
96 |
+
# Prefer tool JSON; if absent, fall back to raw text
|
97 |
+
if tool_result is not None:
|
98 |
+
state.response_text = json.dumps(tool_result, ensure_ascii=False, indent=2)
|
99 |
+
else:
|
100 |
+
state.response_text = response.content[0].text if response.content else ""
|
101 |
+
|
102 |
+
except Exception as e:
|
103 |
+
state.response_text = f"Error during evaluation: {str(e)}"
|
104 |
+
|
105 |
+
return state
|
106 |
+
|
107 |
+
|
108 |
+
async def save_node(state: ConversationState) -> ConversationState:
|
109 |
+
"""Save the evaluation result to a file"""
|
110 |
+
try:
|
111 |
+
# Create workdir if it doesn't exist
|
112 |
+
workdir = os.getenv("WORKDIR", "workdir")
|
113 |
+
os.makedirs(workdir, exist_ok=True)
|
114 |
+
|
115 |
+
# Generate filename based on timestamp
|
116 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
117 |
+
filename = f"evaluation_{timestamp}.json"
|
118 |
+
filepath = os.path.join(workdir, filename)
|
119 |
+
|
120 |
+
# Save the evaluation result
|
121 |
+
with open(filepath, 'w', encoding='utf-8') as f:
|
122 |
+
json.dump({
|
123 |
+
"timestamp": timestamp,
|
124 |
+
"response": state.response_text,
|
125 |
+
"tool_result": state.tool_result
|
126 |
+
}, f, ensure_ascii=False, indent=2)
|
127 |
+
|
128 |
+
state.response_text += f"\n\nEvaluation saved to: {filename}"
|
129 |
+
|
130 |
+
except Exception as e:
|
131 |
+
state.response_text += f"\n\nError saving evaluation: {str(e)}"
|
132 |
+
|
133 |
+
return state
|
134 |
+
|
135 |
+
|
136 |
+
def build_graph(api_key: Optional[str] = None):
|
137 |
+
"""Build the evaluation graph"""
|
138 |
+
graph = StateGraph(ConversationState)
|
139 |
+
evaluator = Evaluator(api_key=api_key)
|
140 |
+
graph.add_node("evaluate", evaluator)
|
141 |
+
graph.add_node("save", save_node)
|
142 |
+
|
143 |
+
# Define the flow
|
144 |
+
graph.set_entry_point("evaluate")
|
145 |
+
graph.add_edge("evaluate", "save")
|
146 |
+
graph.add_edge("save", END)
|
147 |
+
|
148 |
+
return graph.compile()
|
149 |
+
|
150 |
+
|
151 |
+
def run_evaluation(pdf_path: str, output_file: Optional[str] = None, api_key: Optional[str] = None) -> str:
|
152 |
+
app = build_graph(api_key=api_key)
|
153 |
+
initial = ConversationState(pdf_path=pdf_path, output_file=output_file)
|
154 |
+
# Ensure compatibility with LangGraph's dict-based state
|
155 |
+
final_state = app.invoke(initial.model_dump())
|
156 |
+
if isinstance(final_state, dict):
|
157 |
+
return str(final_state.get("response_text", ""))
|
158 |
+
if isinstance(final_state, ConversationState):
|
159 |
+
return final_state.response_text
|
160 |
+
return str(getattr(final_state, "response_text", ""))
|
161 |
+
|
162 |
+
|
agents/prompt.py
ADDED
@@ -0,0 +1,397 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Prompts moved from test_pdf_parser.py to make the agent self-contained
|
2 |
+
REVIEWER_SYSTEM_PROMPT = """You are a senior AI research expert and technology assessment consultant, specializing in evaluating the potential for scientific research work to be automated by current or near-future AI systems.
|
3 |
+
Your assessment should be:
|
4 |
+
1. Systematic and evidence-based using the 12-dimensional framework
|
5 |
+
2. Objective in analyzing current AI capability boundaries
|
6 |
+
3. Realistic in predicting technology development trends
|
7 |
+
4. Comprehensive in considering automation barriers and societal impacts
|
8 |
+
|
9 |
+
Maintain critical thinking and provide detailed justifications for each score. Your evaluation will influence research directions and resource allocation decisions."""
|
10 |
+
|
11 |
+
EVALUATION_PROMPT_TEMPLATE = """
|
12 |
+
# Systematic AI Automation Assessment Framework
|
13 |
+
|
14 |
+
Please conduct a comprehensive evaluation of the provided academic work using the following 12-dimensional framework. For each dimension, provide detailed analysis and justification for your scoring.
|
15 |
+
|
16 |
+
## 12-Dimensional Evaluation Framework
|
17 |
+
|
18 |
+
### 1. **Task Formalization** (Score: 0-4)
|
19 |
+
**What to Evaluate**: Whether the task has clear rules/mathematical objectives
|
20 |
+
**Score Anchors**:
|
21 |
+
- 0: Ill-defined
|
22 |
+
- 1: Partly formal
|
23 |
+
- 2: Mostly formal
|
24 |
+
- 3: Fully formal with minor caveats
|
25 |
+
- 4: Mathematically exact
|
26 |
+
|
27 |
+
**Analysis Required**: Examine the clarity of problem definition, mathematical formulation, and objective functions.
|
28 |
+
|
29 |
+
### 2. **Data & Resource Availability** (Score: 0-4)
|
30 |
+
**What to Evaluate**: Public data, simulators, tool chains availability
|
31 |
+
**Score Anchors**:
|
32 |
+
- 0: None
|
33 |
+
- 1: Sparse/private
|
34 |
+
- 2: Moderate
|
35 |
+
- 3: Rich
|
36 |
+
- 4: Abundant & public
|
37 |
+
|
38 |
+
**Analysis Required**: Assess the availability and quality of datasets, existing tools, and computational resources.
|
39 |
+
|
40 |
+
### 3. **Input-Output Complexity** (Score: 0-4)
|
41 |
+
**What to Evaluate**: Modal diversity, structure and length complexity
|
42 |
+
**Score Anchors**:
|
43 |
+
- 0: Chaotic
|
44 |
+
- 1: High complexity
|
45 |
+
- 2: Moderate complexity
|
46 |
+
- 3: Low complexity
|
47 |
+
- 4: Highly regular
|
48 |
+
|
49 |
+
**Analysis Required**: Evaluate the complexity of input processing and output generation requirements.
|
50 |
+
|
51 |
+
### 4. **Real-World Interaction** (Score: 0-4)
|
52 |
+
**What to Evaluate**: Need for physical/social/online feedback
|
53 |
+
**Score Anchors**:
|
54 |
+
- 0: Constant interaction needed
|
55 |
+
- 1: Frequent interaction
|
56 |
+
- 2: Occasional interaction
|
57 |
+
- 3: Rare interaction
|
58 |
+
- 4: None (offline)
|
59 |
+
|
60 |
+
**Analysis Required**: Determine the extent of real-world interaction and feedback requirements.
|
61 |
+
|
62 |
+
### 5. **Existing AI Coverage** (Score: 0-4)
|
63 |
+
**What to Evaluate**: Proportion of work already completed by existing AI models
|
64 |
+
**Score Anchors**:
|
65 |
+
- 0: < 5%
|
66 |
+
- 1: ≈ 25%
|
67 |
+
- 2: ≈ 50%
|
68 |
+
- 3: ≈ 75%
|
69 |
+
- 4: > 95%
|
70 |
+
|
71 |
+
**Analysis Required**: Identify specific existing AI tools/models and quantify coverage percentage.
|
72 |
+
|
73 |
+
### 6. **Automation Barriers** (Qualitative Analysis - No Score)
|
74 |
+
**What to Evaluate**: Major obstacles like creativity, common sense, legal issues
|
75 |
+
**Analysis Required**: List and explain key barriers preventing full automation:
|
76 |
+
- Creativity requirements
|
77 |
+
- Common sense reasoning
|
78 |
+
- Domain expertise
|
79 |
+
- Legal/ethical constraints
|
80 |
+
- Tacit knowledge
|
81 |
+
- Other specific barriers
|
82 |
+
|
83 |
+
### 7. **Human Originality/Irreplaceability** (Score: 0-4)
|
84 |
+
**What to Evaluate**: Dependence on human creativity and originality
|
85 |
+
**Score Anchors**:
|
86 |
+
- 0: Routine work
|
87 |
+
- 1: Incremental innovation
|
88 |
+
- 2: Moderately novel
|
89 |
+
- 3: Clearly novel
|
90 |
+
- 4: Paradigm-shifting
|
91 |
+
|
92 |
+
**Analysis Required**: Assess the level of human creativity, insight, and original thinking required.
|
93 |
+
|
94 |
+
### 8. **Safety & Ethical Criticality** (Score: 0-4, Reverse Scoring)
|
95 |
+
**What to Evaluate**: Consequences of failure/misuse
|
96 |
+
**Score Anchors**:
|
97 |
+
- 0: Catastrophic consequences
|
98 |
+
- 1: Serious consequences
|
99 |
+
- 2: Manageable consequences
|
100 |
+
- 3: Minor consequences
|
101 |
+
- 4: Negligible consequences
|
102 |
+
|
103 |
+
**Analysis Required**: Evaluate risks and potential negative impacts of automation.
|
104 |
+
|
105 |
+
### 9. **Societal/Economic Impact** (Qualitative Analysis - No Score)
|
106 |
+
**What to Evaluate**: Net impact after full automation
|
107 |
+
**Analysis Required**: Describe comprehensive societal and economic implications:
|
108 |
+
- Job displacement effects
|
109 |
+
- Research quality changes
|
110 |
+
- Innovation ecosystem impacts
|
111 |
+
- Economic benefits/costs
|
112 |
+
- Social implications
|
113 |
+
|
114 |
+
### 10. **Technical Maturity Needed** (Score: 0-4)
|
115 |
+
**What to Evaluate**: Required R&D depth for automation
|
116 |
+
**Score Anchors**:
|
117 |
+
- 0: Multiple breakthroughs needed
|
118 |
+
- 1: One major breakthrough needed
|
119 |
+
- 2: Cutting-edge R&D required
|
120 |
+
- 3: Incremental work needed
|
121 |
+
- 4: Already solved
|
122 |
+
|
123 |
+
**Analysis Required**: Identify specific technical advances needed and their feasibility.
|
124 |
+
|
125 |
+
### 11. **3-Year Feasibility** (Probability: 0-100%)
|
126 |
+
**What to Evaluate**: Probability of AI reaching expert level within 3 years
|
127 |
+
**Analysis Required**: Provide realistic probability estimate with detailed justification considering:
|
128 |
+
- Current AI development pace
|
129 |
+
- Required technical breakthroughs
|
130 |
+
- Resource availability
|
131 |
+
- Market incentives
|
132 |
+
|
133 |
+
### 12. **Overall Automatability** (Score: 0-4)
|
134 |
+
**What to Evaluate**: Comprehensive automation feasibility
|
135 |
+
**Score Anchors**:
|
136 |
+
- 0: Not automatable
|
137 |
+
- 1: Hard to automate
|
138 |
+
- 2: Moderately automatable
|
139 |
+
- 3: Highly automatable
|
140 |
+
- 4: Already automatable
|
141 |
+
|
142 |
+
**Analysis Required**: Synthesize all dimensions into overall assessment.
|
143 |
+
|
144 |
+
## Output Format Requirements
|
145 |
+
|
146 |
+
Please structure your response as follows:
|
147 |
+
|
148 |
+
# AI Automation Assessment Report
|
149 |
+
|
150 |
+
## Executive Summary
|
151 |
+
[Provide a concise 150-word summary of key findings and overall assessment]
|
152 |
+
|
153 |
+
## Detailed Dimensional Analysis
|
154 |
+
|
155 |
+
### 1. Task Formalization
|
156 |
+
**Score: X/4**
|
157 |
+
[Detailed analysis and justification]
|
158 |
+
|
159 |
+
### 2. Data & Resource Availability
|
160 |
+
**Score: X/4**
|
161 |
+
[Detailed analysis and justification]
|
162 |
+
|
163 |
+
### 3. Input-Output Complexity
|
164 |
+
**Score: X/4**
|
165 |
+
[Detailed analysis and justification]
|
166 |
+
|
167 |
+
### 4. Real-World Interaction
|
168 |
+
**Score: X/4**
|
169 |
+
[Detailed analysis and justification]
|
170 |
+
|
171 |
+
### 5. Existing AI Coverage
|
172 |
+
**Score: X/4**
|
173 |
+
[Detailed analysis with specific tools/models and coverage percentage]
|
174 |
+
|
175 |
+
### 6. Automation Barriers
|
176 |
+
[Comprehensive list and explanation of key barriers]
|
177 |
+
|
178 |
+
### 7. Human Originality/Irreplaceability
|
179 |
+
**Score: X/4**
|
180 |
+
[Detailed analysis and justification]
|
181 |
+
|
182 |
+
### 8. Safety & Ethical Criticality
|
183 |
+
**Score: X/4**
|
184 |
+
[Detailed risk analysis and justification]
|
185 |
+
|
186 |
+
### 9. Societal/Economic Impact
|
187 |
+
[Comprehensive impact analysis]
|
188 |
+
|
189 |
+
### 10. Technical Maturity Needed
|
190 |
+
**Score: X/4**
|
191 |
+
[Detailed analysis of required advances]
|
192 |
+
|
193 |
+
### 11. 3-Year Feasibility
|
194 |
+
**Probability: X%**
|
195 |
+
[Detailed probability assessment with reasoning]
|
196 |
+
|
197 |
+
### 12. Overall Automatability
|
198 |
+
**Score: X/4**
|
199 |
+
[Synthesis of all dimensions with final assessment]
|
200 |
+
|
201 |
+
## Summary Scorecard
|
202 |
+
|
203 |
+
| Dimension | Score | Key Insight |
|
204 |
+
|-----------|-------|-------------|
|
205 |
+
| Task Formalization | X/4 | [Brief insight] |
|
206 |
+
| Data & Resource Availability | X/4 | [Brief insight] |
|
207 |
+
| Input-Output Complexity | X/4 | [Brief insight] |
|
208 |
+
| Real-World Interaction | X/4 | [Brief insight] |
|
209 |
+
| Existing AI Coverage | X/4 | [Brief insight] |
|
210 |
+
| Human Originality | X/4 | [Brief insight] |
|
211 |
+
| Safety & Ethics | X/4 | [Brief insight] |
|
212 |
+
| Technical Maturity | X/4 | [Brief insight] |
|
213 |
+
| 3-Year Feasibility | X% | [Brief insight] |
|
214 |
+
| **Overall Automatability** | **X/4** | **[Key conclusion]** |
|
215 |
+
|
216 |
+
## Strategic Recommendations
|
217 |
+
|
218 |
+
### For Researchers
|
219 |
+
[Specific recommendations for researchers in this field]
|
220 |
+
|
221 |
+
### For Institutions
|
222 |
+
[Recommendations for research institutions and funding bodies]
|
223 |
+
|
224 |
+
### For AI Development
|
225 |
+
[Recommendations for AI researchers and developers]
|
226 |
+
|
227 |
+
## Assessment Limitations and Uncertainties
|
228 |
+
[List key limitations, assumptions, and areas of uncertainty in the assessment]
|
229 |
+
|
230 |
+
---
|
231 |
+
|
232 |
+
**Instructions**:
|
233 |
+
- Provide specific evidence and examples for each score
|
234 |
+
- Be conservative in scoring when uncertain
|
235 |
+
- Consider both current capabilities and realistic near-term developments
|
236 |
+
- Justify all numerical scores with detailed reasoning
|
237 |
+
- For qualitative dimensions, provide comprehensive analysis
|
238 |
+
|
239 |
+
Now please begin the systematic evaluation of the provided academic work.
|
240 |
+
"""
|
241 |
+
|
242 |
+
# Tools schema for function calling (Anthropic tools)
|
243 |
+
# The model must call `return_assessment` to output a strict JSON object
|
244 |
+
TOOLS = [
|
245 |
+
{
|
246 |
+
"name": "return_assessment",
|
247 |
+
"description": "Return the complete 12D AI automation assessment as a single JSON object.",
|
248 |
+
"input_schema": {
|
249 |
+
"type": "object",
|
250 |
+
"properties": {
|
251 |
+
"metadata": {
|
252 |
+
"type": "object",
|
253 |
+
"properties": {
|
254 |
+
"assessed_at": {"type": "string"},
|
255 |
+
"model": {"type": "string"},
|
256 |
+
"version": {"type": "string"},
|
257 |
+
"paper_path": {"type": "string"},
|
258 |
+
},
|
259 |
+
"required": ["assessed_at", "model", "version", "paper_path"],
|
260 |
+
},
|
261 |
+
"executive_summary": {"type": "string"},
|
262 |
+
"dimensions": {
|
263 |
+
"type": "object",
|
264 |
+
"properties": {
|
265 |
+
"task_formalization": {
|
266 |
+
"type": "object",
|
267 |
+
"properties": {"score": {"type": "number"}, "analysis": {"type": "string"}},
|
268 |
+
"required": ["score", "analysis"],
|
269 |
+
},
|
270 |
+
"data_resource_availability": {
|
271 |
+
"type": "object",
|
272 |
+
"properties": {"score": {"type": "number"}, "analysis": {"type": "string"}},
|
273 |
+
"required": ["score", "analysis"],
|
274 |
+
},
|
275 |
+
"input_output_complexity": {
|
276 |
+
"type": "object",
|
277 |
+
"properties": {"score": {"type": "number"}, "analysis": {"type": "string"}},
|
278 |
+
"required": ["score", "analysis"],
|
279 |
+
},
|
280 |
+
"real_world_interaction": {
|
281 |
+
"type": "object",
|
282 |
+
"properties": {"score": {"type": "number"}, "analysis": {"type": "string"}},
|
283 |
+
"required": ["score", "analysis"],
|
284 |
+
},
|
285 |
+
"existing_ai_coverage": {
|
286 |
+
"type": "object",
|
287 |
+
"properties": {
|
288 |
+
"score": {"type": "number"},
|
289 |
+
"analysis": {"type": "string"},
|
290 |
+
"tools_models": {"type": "array", "items": {"type": "string"}},
|
291 |
+
"coverage_pct_estimate": {"type": "number"},
|
292 |
+
},
|
293 |
+
"required": ["score", "analysis"],
|
294 |
+
},
|
295 |
+
"automation_barriers": {
|
296 |
+
"type": "object",
|
297 |
+
"properties": {"analysis": {"type": "string"}},
|
298 |
+
"required": ["analysis"],
|
299 |
+
},
|
300 |
+
"human_originality": {
|
301 |
+
"type": "object",
|
302 |
+
"properties": {"score": {"type": "number"}, "analysis": {"type": "string"}},
|
303 |
+
"required": ["score", "analysis"],
|
304 |
+
},
|
305 |
+
"safety_ethics": {
|
306 |
+
"type": "object",
|
307 |
+
"properties": {"score": {"type": "number"}, "analysis": {"type": "string"}},
|
308 |
+
"required": ["score", "analysis"],
|
309 |
+
},
|
310 |
+
"societal_economic_impact": {
|
311 |
+
"type": "object",
|
312 |
+
"properties": {"analysis": {"type": "string"}},
|
313 |
+
"required": ["analysis"],
|
314 |
+
},
|
315 |
+
"technical_maturity_needed": {
|
316 |
+
"type": "object",
|
317 |
+
"properties": {"score": {"type": "number"}, "analysis": {"type": "string"}},
|
318 |
+
"required": ["score", "analysis"],
|
319 |
+
},
|
320 |
+
"three_year_feasibility": {
|
321 |
+
"type": "object",
|
322 |
+
"properties": {"probability_pct": {"type": "number"}, "analysis": {"type": "string"}},
|
323 |
+
"required": ["probability_pct", "analysis"],
|
324 |
+
},
|
325 |
+
"overall_automatability": {
|
326 |
+
"type": "object",
|
327 |
+
"properties": {"score": {"type": "number"}, "analysis": {"type": "string"}},
|
328 |
+
"required": ["score", "analysis"],
|
329 |
+
},
|
330 |
+
},
|
331 |
+
"required": [
|
332 |
+
"task_formalization",
|
333 |
+
"data_resource_availability",
|
334 |
+
"input_output_complexity",
|
335 |
+
"real_world_interaction",
|
336 |
+
"existing_ai_coverage",
|
337 |
+
"automation_barriers",
|
338 |
+
"human_originality",
|
339 |
+
"safety_ethics",
|
340 |
+
"societal_economic_impact",
|
341 |
+
"technical_maturity_needed",
|
342 |
+
"three_year_feasibility",
|
343 |
+
"overall_automatability",
|
344 |
+
],
|
345 |
+
},
|
346 |
+
"scorecard": {
|
347 |
+
"type": "object",
|
348 |
+
"properties": {
|
349 |
+
"task_formalization": {"type": "number"},
|
350 |
+
"data_resource_availability": {"type": "number"},
|
351 |
+
"input_output_complexity": {"type": "number"},
|
352 |
+
"real_world_interaction": {"type": "number"},
|
353 |
+
"existing_ai_coverage": {"type": "number"},
|
354 |
+
"human_originality": {"type": "number"},
|
355 |
+
"safety_ethics": {"type": "number"},
|
356 |
+
"technical_maturity_needed": {"type": "number"},
|
357 |
+
"three_year_feasibility_pct": {"type": "number"},
|
358 |
+
"overall_automatability": {"type": "number"},
|
359 |
+
},
|
360 |
+
"required": [
|
361 |
+
"task_formalization",
|
362 |
+
"data_resource_availability",
|
363 |
+
"input_output_complexity",
|
364 |
+
"real_world_interaction",
|
365 |
+
"existing_ai_coverage",
|
366 |
+
"human_originality",
|
367 |
+
"safety_ethics",
|
368 |
+
"technical_maturity_needed",
|
369 |
+
"three_year_feasibility_pct",
|
370 |
+
"overall_automatability",
|
371 |
+
],
|
372 |
+
},
|
373 |
+
"recommendations": {
|
374 |
+
"type": "object",
|
375 |
+
"properties": {
|
376 |
+
"for_researchers": {"type": "array", "items": {"type": "string"}},
|
377 |
+
"for_institutions": {"type": "array", "items": {"type": "string"}},
|
378 |
+
"for_ai_development": {"type": "array", "items": {"type": "string"}},
|
379 |
+
},
|
380 |
+
"required": ["for_researchers", "for_institutions", "for_ai_development"],
|
381 |
+
},
|
382 |
+
"limitations_uncertainties": {"type": "array", "items": {"type": "string"}},
|
383 |
+
},
|
384 |
+
"required": [
|
385 |
+
"metadata",
|
386 |
+
"executive_summary",
|
387 |
+
"dimensions",
|
388 |
+
"scorecard",
|
389 |
+
"recommendations",
|
390 |
+
"limitations_uncertainties",
|
391 |
+
],
|
392 |
+
"additionalProperties": False,
|
393 |
+
},
|
394 |
+
}
|
395 |
+
]
|
396 |
+
|
397 |
+
TOOL_CHOICE = {"type": "tool", "name": "return_assessment"}
|
app.py
ADDED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import sys
|
3 |
+
from pathlib import Path
|
4 |
+
|
5 |
+
# Add the current directory to Python path
|
6 |
+
sys.path.insert(0, str(Path(__file__).parent))
|
7 |
+
|
8 |
+
# Import and run the FastAPI app from server.py
|
9 |
+
from server import app
|
10 |
+
|
11 |
+
if __name__ == "__main__":
|
12 |
+
import uvicorn
|
13 |
+
uvicorn.run(app, host="0.0.0.0", port=7860)
|
cli.py
ADDED
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import argparse
|
2 |
+
import os
|
3 |
+
import sys
|
4 |
+
from typing import Optional
|
5 |
+
from dotenv import load_dotenv
|
6 |
+
load_dotenv()
|
7 |
+
|
8 |
+
from rich.console import Console
|
9 |
+
from rich.panel import Panel
|
10 |
+
|
11 |
+
from agents.evaluator import run_evaluation
|
12 |
+
|
13 |
+
|
14 |
+
console = Console()
|
15 |
+
|
16 |
+
|
17 |
+
def build_parser() -> argparse.ArgumentParser:
|
18 |
+
parser = argparse.ArgumentParser(
|
19 |
+
description="AI Automation Evaluator (LangGraph) — evaluate a paper PDF or arXiv URL",
|
20 |
+
epilog="Example: python cli.py https://arxiv.org/pdf/2507.14683 -o /abs/path/save_dir/eval_2507_14683",
|
21 |
+
)
|
22 |
+
parser.add_argument("pdf", help="Local PDF absolute path or URL (e.g., https://arxiv.org/pdf/xxxx)")
|
23 |
+
parser.add_argument(
|
24 |
+
"-o",
|
25 |
+
"--output-prefix",
|
26 |
+
dest="output_prefix",
|
27 |
+
help="Output file prefix (if provided, will save as <prefix>_YYYYMMDD_HHMMSS.md)",
|
28 |
+
)
|
29 |
+
parser.add_argument(
|
30 |
+
"--api-key",
|
31 |
+
dest="api_key",
|
32 |
+
default=os.getenv("ANTHROPIC_API_KEY"),
|
33 |
+
help="Anthropic API key (overrides ANTHROPIC_API_KEY env)",
|
34 |
+
)
|
35 |
+
return parser
|
36 |
+
|
37 |
+
|
38 |
+
def main(argv: Optional[list[str]] = None):
|
39 |
+
parser = build_parser()
|
40 |
+
args = parser.parse_args(argv)
|
41 |
+
|
42 |
+
pdf_path: str = args.pdf
|
43 |
+
output_prefix: Optional[str] = args.output_prefix
|
44 |
+
api_key: Optional[str] = args.api_key or os.getenv("ANTHROPIC_API_KEY")
|
45 |
+
|
46 |
+
if not api_key:
|
47 |
+
console.print("[yellow]Warning:[/yellow] ANTHROPIC_API_KEY not set and --api-key not provided.", highlight=False)
|
48 |
+
|
49 |
+
console.print(Panel.fit(f"Evaluating: {pdf_path}"))
|
50 |
+
try:
|
51 |
+
result = run_evaluation(pdf_path=pdf_path, output_file=output_prefix, api_key=api_key)
|
52 |
+
console.print("\n[bold green]Done.[/bold green]\n")
|
53 |
+
if output_prefix:
|
54 |
+
console.print(f"Saved to prefix: {output_prefix}_<timestamp>.md")
|
55 |
+
else:
|
56 |
+
console.print(result)
|
57 |
+
except Exception as e:
|
58 |
+
console.print(f"[bold red]Error:[/bold red] {e}")
|
59 |
+
sys.exit(2)
|
60 |
+
|
61 |
+
|
62 |
+
if __name__ == "__main__":
|
63 |
+
main()
|
64 |
+
|
65 |
+
|
frontend/index.html
ADDED
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!doctype html>
|
2 |
+
<html lang="en" data-theme="light">
|
3 |
+
<head>
|
4 |
+
<meta charset="utf-8" />
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
6 |
+
<title>PaperIndex — Daily Papers</title>
|
7 |
+
<link rel="stylesheet" href="/styles.css?v=9" />
|
8 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
9 |
+
</head>
|
10 |
+
<body>
|
11 |
+
<!-- Navigation Bar -->
|
12 |
+
<nav class="navbar">
|
13 |
+
<div class="nav-container">
|
14 |
+
<div class="nav-left">
|
15 |
+
<div class="logo">
|
16 |
+
<i class="fas fa-book-open"></i>
|
17 |
+
<span>PaperIndex</span>
|
18 |
+
</div>
|
19 |
+
</div>
|
20 |
+
|
21 |
+
<div class="nav-center">
|
22 |
+
<div class="search-container">
|
23 |
+
<i class="fas fa-search search-icon"></i>
|
24 |
+
<input type="text" placeholder="Search papers..." class="search-input">
|
25 |
+
<i class="fas fa-magic search-magic"></i>
|
26 |
+
</div>
|
27 |
+
</div>
|
28 |
+
|
29 |
+
<div class="nav-right">
|
30 |
+
<button class="theme-toggle" id="themeToggle">
|
31 |
+
<i class="fas fa-sun light-icon"></i>
|
32 |
+
<i class="fas fa-moon dark-icon"></i>
|
33 |
+
</button>
|
34 |
+
</div>
|
35 |
+
</div>
|
36 |
+
</nav>
|
37 |
+
|
38 |
+
<!-- Main Header -->
|
39 |
+
<header class="main-header">
|
40 |
+
<div class="header-container">
|
41 |
+
<div class="header-left">
|
42 |
+
<h1>Daily Papers</h1>
|
43 |
+
<p class="subtitle">by PaperIndex and the research community</p>
|
44 |
+
</div>
|
45 |
+
|
46 |
+
<div class="header-center">
|
47 |
+
<div class="ai-search-container">
|
48 |
+
<i class="fas fa-sparkles"></i>
|
49 |
+
<input type="text" placeholder="Search any paper with AI..." class="ai-search-input">
|
50 |
+
<i class="fas fa-cube"></i>
|
51 |
+
</div>
|
52 |
+
</div>
|
53 |
+
|
54 |
+
<div class="header-right">
|
55 |
+
<div class="filter-buttons">
|
56 |
+
<button class="filter-btn active">Daily</button>
|
57 |
+
<button class="filter-btn">Weekly</button>
|
58 |
+
<button class="filter-btn">Monthly</button>
|
59 |
+
<button class="filter-btn star-btn">
|
60 |
+
<i class="fas fa-star"></i>
|
61 |
+
</button>
|
62 |
+
</div>
|
63 |
+
|
64 |
+
<div class="date-navigation">
|
65 |
+
<button class="nav-btn" id="prevDate">
|
66 |
+
<i class="fas fa-chevron-left"></i>
|
67 |
+
</button>
|
68 |
+
<span class="date-display" id="dateDisplay"></span>
|
69 |
+
<button class="nav-btn" id="nextDate">
|
70 |
+
<i class="fas fa-chevron-right"></i>
|
71 |
+
</button>
|
72 |
+
</div>
|
73 |
+
</div>
|
74 |
+
</div>
|
75 |
+
</header>
|
76 |
+
|
77 |
+
<!-- Main Content -->
|
78 |
+
<main class="main-content">
|
79 |
+
<div class="content-container">
|
80 |
+
<section id="cards" class="cards-grid"></section>
|
81 |
+
</div>
|
82 |
+
</main>
|
83 |
+
|
84 |
+
<script src="/main.js"></script>
|
85 |
+
</body>
|
86 |
+
</html>
|
87 |
+
|
88 |
+
|
frontend/main.js
ADDED
@@ -0,0 +1,459 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// Theme Management
|
2 |
+
class ThemeManager {
|
3 |
+
constructor() {
|
4 |
+
this.theme = localStorage.getItem('theme') || 'light';
|
5 |
+
this.init();
|
6 |
+
}
|
7 |
+
|
8 |
+
init() {
|
9 |
+
document.documentElement.setAttribute('data-theme', this.theme);
|
10 |
+
this.updateThemeIcon();
|
11 |
+
}
|
12 |
+
|
13 |
+
toggle() {
|
14 |
+
this.theme = this.theme === 'light' ? 'dark' : 'light';
|
15 |
+
document.documentElement.setAttribute('data-theme', this.theme);
|
16 |
+
localStorage.setItem('theme', this.theme);
|
17 |
+
this.updateThemeIcon();
|
18 |
+
}
|
19 |
+
|
20 |
+
updateThemeIcon() {
|
21 |
+
const lightIcon = document.querySelector('.light-icon');
|
22 |
+
const darkIcon = document.querySelector('.dark-icon');
|
23 |
+
|
24 |
+
if (this.theme === 'light') {
|
25 |
+
lightIcon.style.display = 'block';
|
26 |
+
darkIcon.style.display = 'none';
|
27 |
+
} else {
|
28 |
+
lightIcon.style.display = 'none';
|
29 |
+
darkIcon.style.display = 'block';
|
30 |
+
}
|
31 |
+
}
|
32 |
+
}
|
33 |
+
|
34 |
+
// Date Management
|
35 |
+
class DateManager {
|
36 |
+
constructor() {
|
37 |
+
// Start with today's date, but it will be updated when we get the actual available date
|
38 |
+
this.currentDate = new Date();
|
39 |
+
this.init();
|
40 |
+
}
|
41 |
+
|
42 |
+
init() {
|
43 |
+
this.updateDateDisplay();
|
44 |
+
this.bindEvents();
|
45 |
+
}
|
46 |
+
|
47 |
+
formatDate(date) {
|
48 |
+
const options = {
|
49 |
+
year: 'numeric',
|
50 |
+
month: 'short',
|
51 |
+
day: 'numeric'
|
52 |
+
};
|
53 |
+
return date.toLocaleDateString('en-US', options);
|
54 |
+
}
|
55 |
+
|
56 |
+
updateDateDisplay() {
|
57 |
+
const dateDisplay = document.getElementById('dateDisplay');
|
58 |
+
dateDisplay.textContent = this.formatDate(this.currentDate);
|
59 |
+
}
|
60 |
+
|
61 |
+
navigateDate(direction) {
|
62 |
+
const newDate = new Date(this.currentDate);
|
63 |
+
newDate.setDate(newDate.getDate() + direction);
|
64 |
+
this.currentDate = newDate;
|
65 |
+
this.updateDateDisplay();
|
66 |
+
this.loadDaily();
|
67 |
+
}
|
68 |
+
|
69 |
+
bindEvents() {
|
70 |
+
document.getElementById('prevDate').addEventListener('click', () => {
|
71 |
+
this.navigateDate(-1);
|
72 |
+
});
|
73 |
+
|
74 |
+
document.getElementById('nextDate').addEventListener('click', () => {
|
75 |
+
this.navigateDate(1);
|
76 |
+
});
|
77 |
+
}
|
78 |
+
|
79 |
+
getDateString() {
|
80 |
+
const pad = (n) => String(n).padStart(2, '0');
|
81 |
+
return `${this.currentDate.getFullYear()}-${pad(this.currentDate.getMonth()+1)}-${pad(this.currentDate.getDate())}`;
|
82 |
+
}
|
83 |
+
}
|
84 |
+
|
85 |
+
// Search Management
|
86 |
+
class SearchManager {
|
87 |
+
constructor() {
|
88 |
+
this.init();
|
89 |
+
}
|
90 |
+
|
91 |
+
init() {
|
92 |
+
this.bindEvents();
|
93 |
+
}
|
94 |
+
|
95 |
+
bindEvents() {
|
96 |
+
const searchInput = document.querySelector('.search-input');
|
97 |
+
const aiSearchInput = document.querySelector('.ai-search-input');
|
98 |
+
|
99 |
+
searchInput.addEventListener('input', (e) => {
|
100 |
+
this.handleSearch(e.target.value);
|
101 |
+
});
|
102 |
+
|
103 |
+
aiSearchInput.addEventListener('input', (e) => {
|
104 |
+
this.handleAISearch(e.target.value);
|
105 |
+
});
|
106 |
+
}
|
107 |
+
|
108 |
+
handleSearch(query) {
|
109 |
+
// Implement search functionality
|
110 |
+
console.log('Search query:', query);
|
111 |
+
}
|
112 |
+
|
113 |
+
handleAISearch(query) {
|
114 |
+
// Implement AI search functionality
|
115 |
+
console.log('AI search query:', query);
|
116 |
+
}
|
117 |
+
}
|
118 |
+
|
119 |
+
// Paper Card Renderer
|
120 |
+
class PaperCardRenderer {
|
121 |
+
constructor() {
|
122 |
+
this.cardsContainer = document.getElementById('cards');
|
123 |
+
}
|
124 |
+
|
125 |
+
generateThumbnail(title) {
|
126 |
+
// Generate a simple thumbnail based on title
|
127 |
+
const canvas = document.createElement('canvas');
|
128 |
+
canvas.width = 400;
|
129 |
+
canvas.height = 120;
|
130 |
+
const ctx = canvas.getContext('2d');
|
131 |
+
|
132 |
+
// Create gradient background
|
133 |
+
const gradient = ctx.createLinearGradient(0, 0, 400, 120);
|
134 |
+
gradient.addColorStop(0, '#3b82f6');
|
135 |
+
gradient.addColorStop(1, '#06b6d4');
|
136 |
+
ctx.fillStyle = gradient;
|
137 |
+
ctx.fillRect(0, 0, 400, 120);
|
138 |
+
|
139 |
+
// Add text
|
140 |
+
ctx.fillStyle = 'rgba(255, 255, 255, 0.9)';
|
141 |
+
ctx.font = 'bold 16px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
142 |
+
ctx.textAlign = 'center';
|
143 |
+
ctx.textBaseline = 'middle';
|
144 |
+
|
145 |
+
const words = title.split(' ');
|
146 |
+
const lines = [];
|
147 |
+
let currentLine = '';
|
148 |
+
|
149 |
+
for (const word of words) {
|
150 |
+
const testLine = currentLine + word + ' ';
|
151 |
+
const metrics = ctx.measureText(testLine);
|
152 |
+
if (metrics.width > 350 && currentLine !== '') {
|
153 |
+
lines.push(currentLine);
|
154 |
+
currentLine = word + ' ';
|
155 |
+
} else {
|
156 |
+
currentLine = testLine;
|
157 |
+
}
|
158 |
+
}
|
159 |
+
lines.push(currentLine);
|
160 |
+
|
161 |
+
const yStart = 60 - (lines.length * 20) / 2;
|
162 |
+
lines.forEach((line, index) => {
|
163 |
+
ctx.fillText(line.trim(), 200, yStart + index * 20);
|
164 |
+
});
|
165 |
+
|
166 |
+
return canvas.toDataURL();
|
167 |
+
}
|
168 |
+
|
169 |
+
generateAuthorAvatars(authorCount) {
|
170 |
+
const avatars = [];
|
171 |
+
const count = Math.min(authorCount, 5);
|
172 |
+
|
173 |
+
for (let i = 0; i < count; i++) {
|
174 |
+
avatars.push(`<li title="Author ${i + 1}"></li>`);
|
175 |
+
}
|
176 |
+
|
177 |
+
return avatars.join('');
|
178 |
+
}
|
179 |
+
|
180 |
+
renderCard(paper) {
|
181 |
+
const title = paper.title || 'Untitled Paper';
|
182 |
+
const abstract = paper.abstract || 'No abstract available';
|
183 |
+
const authors = paper.authors || [];
|
184 |
+
const authorCount = paper.author_count || authors.length || 0;
|
185 |
+
const upvotes = paper.upvotes || 0;
|
186 |
+
const githubStars = paper.github_stars || 0;
|
187 |
+
const comments = paper.comments || 0;
|
188 |
+
const submitter = paper.submitter || 'Anonymous';
|
189 |
+
|
190 |
+
// Generate thumbnail URL - try to use HF thumbnail if available
|
191 |
+
const arxivId = paper.arxiv_id;
|
192 |
+
const thumbnailUrl = arxivId ?
|
193 |
+
`https://cdn-thumbnails.huggingface.co/social-thumbnails/papers/${arxivId}.png` :
|
194 |
+
this.generateThumbnail(title);
|
195 |
+
|
196 |
+
const authorAvatars = this.generateAuthorAvatars(authorCount);
|
197 |
+
|
198 |
+
const card = document.createElement('article');
|
199 |
+
card.className = 'hf-paper-card';
|
200 |
+
card.innerHTML = `
|
201 |
+
<a href="${paper.huggingface_url || '#'}" class="paper-thumbnail-link" target="_blank" rel="noreferrer">
|
202 |
+
<img src="${thumbnailUrl}" loading="lazy" decoding="async" alt="" class="paper-thumbnail-img">
|
203 |
+
</a>
|
204 |
+
|
205 |
+
<div class="submitted-by-badge">
|
206 |
+
<span>Submitted by</span>
|
207 |
+
<div class="submitter-avatar-placeholder">
|
208 |
+
<i class="fas fa-user"></i>
|
209 |
+
</div>
|
210 |
+
${submitter}
|
211 |
+
</div>
|
212 |
+
|
213 |
+
<div class="card-content">
|
214 |
+
<div class="upvote-section">
|
215 |
+
<label class="upvote-button">
|
216 |
+
<input type="checkbox" class="upvote-checkbox">
|
217 |
+
<svg class="upvote-icon" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 12 12">
|
218 |
+
<path fill="currentColor" d="M5.19 2.67a.94.94 0 0 1 1.62 0l3.31 5.72a.94.94 0 0 1-.82 1.4H2.7a.94.94 0 0 1-.82-1.4l3.31-5.7v-.02Z"></path>
|
219 |
+
</svg>
|
220 |
+
<div class="upvote-count">${upvotes}</div>
|
221 |
+
</label>
|
222 |
+
</div>
|
223 |
+
|
224 |
+
<div class="paper-info">
|
225 |
+
<h3 class="paper-title">
|
226 |
+
<a href="${paper.huggingface_url || '#'}" class="title-link">
|
227 |
+
${title}
|
228 |
+
</a>
|
229 |
+
</h3>
|
230 |
+
|
231 |
+
<div class="paper-meta">
|
232 |
+
<div class="authors-section">
|
233 |
+
<a href="${paper.huggingface_url || '#'}" class="authors-link">
|
234 |
+
<ul class="author-avatars-list">
|
235 |
+
${authorAvatars}
|
236 |
+
</ul>
|
237 |
+
<div class="author-count">· ${authorCount} authors</div>
|
238 |
+
</a>
|
239 |
+
</div>
|
240 |
+
|
241 |
+
<div class="engagement-metrics">
|
242 |
+
<a href="${paper.huggingface_url || '#'}" class="metric-link">
|
243 |
+
<svg class="github-icon" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" focusable="false" role="img" width="1.03em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 250">
|
244 |
+
<path d="M128.001 0C57.317 0 0 57.307 0 128.001c0 56.554 36.676 104.535 87.535 121.46c6.397 1.185 8.746-2.777 8.746-6.158c0-3.052-.12-13.135-.174-23.83c-35.61 7.742-43.124-15.103-43.124-15.103c-5.823-14.795-14.213-18.73-14.213-18.73c-11.613-7.944.876-7.78.876-7.78c12.853.902 19.621 13.19 19.621 13.19c11.417 19.568 29.945 13.911 37.249 10.64c1.149-8.272 4.466-13.92 8.127-17.116c-28.431-3.236-58.318-14.212-58.318-63.258c0-13.975 5-25.394 13.188-34.358c-1.329-3.224-5.71-16.242 1.24-33.874c0 0 10.749-3.44 35.21 13.121c10.21-2.836 21.16-4.258 32.038-4.307c10.878.049 21.837 1.47 32.066 4.307c24.431-16.56 35.165-13.12 35.165-13.12c6.967 17.63 2.584 30.65 1.255 33.873c8.207 8.964 13.173 20.383 13.173 34.358c0 49.163-29.944 59.988-58.447 63.157c4.591 3.972 8.682 11.762 8.682 23.704c0 17.126-.148 30.91-.148 35.126c0 3.407 2.304 7.398 8.792 6.14C219.37 232.5 256 184.537 256 128.002C256 57.307 198.691 0 128.001 0zm-80.06 182.34c-.282.636-1.283.827-2.194.39c-.929-.417-1.45-1.284-1.15-1.922c.276-.655 1.279-.838 2.205-.399c.93.418 1.46 1.293 1.139 1.931zm6.296 5.618c-.61.566-1.804.303-2.614-.591c-.837-.892-.994-2.086-.375-2.66c.63-.566 1.787-.301 2.626.591c.838.903 1 2.088.363 2.66zm4.32 7.188c-.785.545-2.067.034-2.86-1.104c-.784-1.138-.784-2.503.017-3.05c.795-.547 2.058-.055 2.861 1.075c.782 1.157.782 2.522-.019 3.08zm7.304 8.325c-.701.774-2.196.566-3.29-.49c-1.119-1.032-1.43-2.496-.726-3.27c.71-.776 2.213-.558 3.315.49c1.11 1.03 1.45 2.505.701 3.27zm9.442 2.81c-.31 1.003-1.75 1.459-3.199 1.033c-1.448-.439-2.395-1.613-2.103-2.626c.301-1.01 1.747-1.484 3.207-1.028c1.446.436 2.396 1.602 2.095 2.622zm10.744 1.193c.036 1.055-1.193 1.93-2.715 1.95c-1.53.034-2.769-.82-2.786-1.86c0-1.065 1.202-1.932 2.733-1.958c1.522-.03 2.768.818 2.768 1.868zm10.555-.405c.182 1.03-.875 2.088-2.387 2.37c-1.485.271-2.861-.365-3.05-1.386c-.184-1.056.893-2.114 2.376-2.387c1.514-.263 2.868.356 3.061 1.403z" fill="currentColor"></path>
|
245 |
+
</svg>
|
246 |
+
<span>${githubStars}</span>
|
247 |
+
</a>
|
248 |
+
<a href="${paper.huggingface_url || '#'}" class="metric-link">
|
249 |
+
<svg class="comment-icon" xmlns="http://www.w3.org/2000/svg" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 24 24">
|
250 |
+
<path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
|
251 |
+
</svg>
|
252 |
+
<span>${comments}</span>
|
253 |
+
</a>
|
254 |
+
</div>
|
255 |
+
</div>
|
256 |
+
</div>
|
257 |
+
</div>
|
258 |
+
|
259 |
+
${paper.arxiv_id ? `
|
260 |
+
<div class="card-actions">
|
261 |
+
<a href="/paper.html?id=${encodeURIComponent(paper.arxiv_id)}" class="eval-button">
|
262 |
+
<i class="fas fa-chart-line"></i>Evaluation
|
263 |
+
</a>
|
264 |
+
</div>
|
265 |
+
` : ''}
|
266 |
+
`;
|
267 |
+
|
268 |
+
return card;
|
269 |
+
}
|
270 |
+
|
271 |
+
renderCards(papers) {
|
272 |
+
this.cardsContainer.innerHTML = '';
|
273 |
+
|
274 |
+
if (!papers || papers.length === 0) {
|
275 |
+
this.cardsContainer.innerHTML = `
|
276 |
+
<div style="grid-column: 1 / -1; text-align: center; padding: 48px; color: var(--text-muted);">
|
277 |
+
<i class="fas fa-search" style="font-size: 48px; margin-bottom: 16px; opacity: 0.5;"></i>
|
278 |
+
<h3>No papers found</h3>
|
279 |
+
<p>Try selecting a different date or check back later.</p>
|
280 |
+
</div>
|
281 |
+
`;
|
282 |
+
return;
|
283 |
+
}
|
284 |
+
|
285 |
+
papers.forEach(paper => {
|
286 |
+
const card = this.renderCard(paper);
|
287 |
+
this.cardsContainer.appendChild(card);
|
288 |
+
});
|
289 |
+
}
|
290 |
+
}
|
291 |
+
|
292 |
+
// Main Application
|
293 |
+
class PaperIndexApp {
|
294 |
+
constructor() {
|
295 |
+
this.themeManager = new ThemeManager();
|
296 |
+
this.dateManager = new DateManager();
|
297 |
+
this.searchManager = new SearchManager();
|
298 |
+
this.cardRenderer = new PaperCardRenderer();
|
299 |
+
this.init();
|
300 |
+
}
|
301 |
+
|
302 |
+
init() {
|
303 |
+
this.bindEvents();
|
304 |
+
this.loadDaily();
|
305 |
+
}
|
306 |
+
|
307 |
+
bindEvents() {
|
308 |
+
// Theme toggle
|
309 |
+
document.getElementById('themeToggle').addEventListener('click', () => {
|
310 |
+
this.themeManager.toggle();
|
311 |
+
});
|
312 |
+
|
313 |
+
// Filter buttons
|
314 |
+
document.querySelectorAll('.filter-btn').forEach(btn => {
|
315 |
+
btn.addEventListener('click', (e) => {
|
316 |
+
document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active'));
|
317 |
+
e.target.classList.add('active');
|
318 |
+
});
|
319 |
+
});
|
320 |
+
}
|
321 |
+
|
322 |
+
async loadDaily() {
|
323 |
+
const dateStr = this.dateManager.getDateString();
|
324 |
+
|
325 |
+
try {
|
326 |
+
const response = await fetch(`/api/daily?date_str=${encodeURIComponent(dateStr)}`);
|
327 |
+
|
328 |
+
if (!response.ok) {
|
329 |
+
throw new Error('Failed to load daily papers');
|
330 |
+
}
|
331 |
+
|
332 |
+
const data = await response.json();
|
333 |
+
|
334 |
+
console.log('API Response:', {
|
335 |
+
requested_date: data.requested_date,
|
336 |
+
actual_date: data.date,
|
337 |
+
fallback_used: data.fallback_used,
|
338 |
+
cards_count: data.cards?.length
|
339 |
+
});
|
340 |
+
|
341 |
+
// Update the date display if a fallback was used or if we got a different date than requested
|
342 |
+
if (data.date && data.requested_date && data.date !== data.requested_date) {
|
343 |
+
console.log('Updating date display from', data.requested_date, 'to', data.date);
|
344 |
+
const fallbackDate = new Date(data.date);
|
345 |
+
this.dateManager.currentDate = fallbackDate;
|
346 |
+
this.dateManager.updateDateDisplay();
|
347 |
+
|
348 |
+
// Show a notification about the fallback
|
349 |
+
this.showFallbackNotification(data.requested_date, data.date);
|
350 |
+
}
|
351 |
+
|
352 |
+
// Show cache status if available
|
353 |
+
if (data.cached) {
|
354 |
+
this.showCacheNotification(data.cached_at);
|
355 |
+
}
|
356 |
+
|
357 |
+
this.cardRenderer.renderCards(data.cards || []);
|
358 |
+
|
359 |
+
} catch (error) {
|
360 |
+
console.error('Error loading papers:', error);
|
361 |
+
this.cardRenderer.renderCards([]);
|
362 |
+
|
363 |
+
// Show fallback message
|
364 |
+
this.cardRenderer.cardsContainer.innerHTML = `
|
365 |
+
<div style="grid-column: 1 / -1; text-align: center; padding: 48px; color: var(--text-muted);">
|
366 |
+
<i class="fas fa-exclamation-triangle" style="font-size: 48px; margin-bottom: 16px; opacity: 0.5;"></i>
|
367 |
+
<h3>Unable to load papers</h3>
|
368 |
+
<p>Backend unavailable on static hosting. Try opening the daily page on Hugging Face:</p>
|
369 |
+
<a class="action-btn primary" href="https://huggingface.co/papers/date/${encodeURIComponent(dateStr)}" target="_blank" rel="noreferrer">
|
370 |
+
<i class="fas fa-external-link-alt"></i>Open on Hugging Face
|
371 |
+
</a>
|
372 |
+
</div>
|
373 |
+
`;
|
374 |
+
}
|
375 |
+
}
|
376 |
+
|
377 |
+
showFallbackNotification(requestedDate, actualDate) {
|
378 |
+
// Create a temporary notification
|
379 |
+
const notification = document.createElement('div');
|
380 |
+
notification.style.cssText = `
|
381 |
+
position: fixed;
|
382 |
+
top: 20px;
|
383 |
+
right: 20px;
|
384 |
+
background: var(--bg-primary);
|
385 |
+
border: 1px solid var(--border-medium);
|
386 |
+
border-radius: 8px;
|
387 |
+
padding: 16px;
|
388 |
+
box-shadow: var(--shadow-lg);
|
389 |
+
z-index: 1000;
|
390 |
+
max-width: 300px;
|
391 |
+
color: var(--text-primary);
|
392 |
+
`;
|
393 |
+
|
394 |
+
notification.innerHTML = `
|
395 |
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
396 |
+
<i class="fas fa-info-circle" style="color: var(--accent-primary);"></i>
|
397 |
+
<strong>Date Updated</strong>
|
398 |
+
</div>
|
399 |
+
<p style="margin: 0; font-size: 14px; color: var(--text-secondary);">
|
400 |
+
Papers for ${requestedDate} not available. Showing latest available: ${actualDate}
|
401 |
+
</p>
|
402 |
+
`;
|
403 |
+
|
404 |
+
document.body.appendChild(notification);
|
405 |
+
|
406 |
+
// Remove notification after 5 seconds
|
407 |
+
setTimeout(() => {
|
408 |
+
if (notification.parentNode) {
|
409 |
+
notification.parentNode.removeChild(notification);
|
410 |
+
}
|
411 |
+
}, 5000);
|
412 |
+
}
|
413 |
+
|
414 |
+
showCacheNotification(cachedAt) {
|
415 |
+
// Create a temporary notification
|
416 |
+
const notification = document.createElement('div');
|
417 |
+
notification.style.cssText = `
|
418 |
+
position: fixed;
|
419 |
+
top: 20px;
|
420 |
+
right: 20px;
|
421 |
+
background: var(--bg-primary);
|
422 |
+
border: 1px solid var(--border-medium);
|
423 |
+
border-radius: 8px;
|
424 |
+
padding: 16px;
|
425 |
+
box-shadow: var(--shadow-lg);
|
426 |
+
z-index: 1000;
|
427 |
+
max-width: 300px;
|
428 |
+
color: var(--text-primary);
|
429 |
+
`;
|
430 |
+
|
431 |
+
const cacheTime = new Date(cachedAt).toLocaleString();
|
432 |
+
|
433 |
+
notification.innerHTML = `
|
434 |
+
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
|
435 |
+
<i class="fas fa-database" style="color: var(--accent-success);"></i>
|
436 |
+
<strong>Cached Data</strong>
|
437 |
+
</div>
|
438 |
+
<p style="margin: 0; font-size: 14px; color: var(--text-secondary);">
|
439 |
+
Showing cached data from ${cacheTime}
|
440 |
+
</p>
|
441 |
+
`;
|
442 |
+
|
443 |
+
document.body.appendChild(notification);
|
444 |
+
|
445 |
+
// Remove notification after 3 seconds
|
446 |
+
setTimeout(() => {
|
447 |
+
if (notification.parentNode) {
|
448 |
+
notification.parentNode.removeChild(notification);
|
449 |
+
}
|
450 |
+
}, 3000);
|
451 |
+
}
|
452 |
+
}
|
453 |
+
|
454 |
+
// Initialize the application when DOM is loaded
|
455 |
+
document.addEventListener('DOMContentLoaded', () => {
|
456 |
+
new PaperIndexApp();
|
457 |
+
});
|
458 |
+
|
459 |
+
|
frontend/paper.html
ADDED
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!doctype html>
|
2 |
+
<html lang="en" data-theme="light">
|
3 |
+
<head>
|
4 |
+
<meta charset="utf-8" />
|
5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
6 |
+
<title>Paper Evaluation</title>
|
7 |
+
<link rel="stylesheet" href="/styles.css?v=9" />
|
8 |
+
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
|
9 |
+
</head>
|
10 |
+
<body>
|
11 |
+
<!-- Navigation Bar -->
|
12 |
+
<nav class="navbar">
|
13 |
+
<div class="nav-container">
|
14 |
+
<div class="nav-left">
|
15 |
+
<a href="/" class="back-link">
|
16 |
+
<i class="fas fa-arrow-left"></i>
|
17 |
+
<span>Back to Daily Papers</span>
|
18 |
+
</a>
|
19 |
+
</div>
|
20 |
+
|
21 |
+
<div class="nav-center">
|
22 |
+
<div class="page-title">
|
23 |
+
<i class="fas fa-chart-line"></i>
|
24 |
+
<span>Paper Evaluation</span>
|
25 |
+
</div>
|
26 |
+
</div>
|
27 |
+
|
28 |
+
<div class="nav-right">
|
29 |
+
<button class="theme-toggle" id="themeToggle">
|
30 |
+
<i class="fas fa-sun light-icon"></i>
|
31 |
+
<i class="fas fa-moon dark-icon"></i>
|
32 |
+
</button>
|
33 |
+
</div>
|
34 |
+
</div>
|
35 |
+
</nav>
|
36 |
+
|
37 |
+
<!-- Main Content -->
|
38 |
+
<main class="paper-main">
|
39 |
+
<div class="paper-container">
|
40 |
+
<!-- Paper Header -->
|
41 |
+
<div class="paper-header">
|
42 |
+
<div class="paper-meta">
|
43 |
+
<h1 id="title">Paper Evaluation</h1>
|
44 |
+
<div class="meta-grid" id="metaGrid">
|
45 |
+
<!-- Meta information will be populated by JavaScript -->
|
46 |
+
</div>
|
47 |
+
</div>
|
48 |
+
</div>
|
49 |
+
|
50 |
+
<!-- Content Layout -->
|
51 |
+
<div class="content-layout">
|
52 |
+
<!-- Main Content -->
|
53 |
+
<div class="main-content">
|
54 |
+
<article id="content" class="evaluation-content">
|
55 |
+
<!-- Content will be populated by JavaScript -->
|
56 |
+
</article>
|
57 |
+
</div>
|
58 |
+
|
59 |
+
<!-- Sidebar -->
|
60 |
+
<aside class="sidebar">
|
61 |
+
<div class="scorecard-panel">
|
62 |
+
<div class="panel-header">
|
63 |
+
<h2><i class="fas fa-radar"></i> Scorecard</h2>
|
64 |
+
<div class="overall-score" id="overallScore">
|
65 |
+
<span class="score-number">-</span>
|
66 |
+
<span class="score-label">Overall</span>
|
67 |
+
</div>
|
68 |
+
</div>
|
69 |
+
<div class="radar-container">
|
70 |
+
<canvas id="radar" width="280" height="280"></canvas>
|
71 |
+
</div>
|
72 |
+
<div class="radar-legend">
|
73 |
+
<h3>Dimensions</h3>
|
74 |
+
<ul id="radar-legend" class="legend-list">
|
75 |
+
<!-- Legend will be populated by JavaScript -->
|
76 |
+
</ul>
|
77 |
+
</div>
|
78 |
+
</div>
|
79 |
+
</aside>
|
80 |
+
</div>
|
81 |
+
</div>
|
82 |
+
</main>
|
83 |
+
|
84 |
+
<script src="/paper.js"></script>
|
85 |
+
</body>
|
86 |
+
</html>
|
87 |
+
|
88 |
+
|
frontend/paper.js
ADDED
@@ -0,0 +1,578 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
// Theme Management
|
2 |
+
class ThemeManager {
|
3 |
+
constructor() {
|
4 |
+
this.theme = localStorage.getItem('theme') || 'light';
|
5 |
+
this.init();
|
6 |
+
}
|
7 |
+
|
8 |
+
init() {
|
9 |
+
document.documentElement.setAttribute('data-theme', this.theme);
|
10 |
+
this.updateThemeIcon();
|
11 |
+
}
|
12 |
+
|
13 |
+
toggle() {
|
14 |
+
this.theme = this.theme === 'light' ? 'dark' : 'light';
|
15 |
+
document.documentElement.setAttribute('data-theme', this.theme);
|
16 |
+
localStorage.setItem('theme', this.theme);
|
17 |
+
this.updateThemeIcon();
|
18 |
+
}
|
19 |
+
|
20 |
+
updateThemeIcon() {
|
21 |
+
const lightIcon = document.querySelector('.light-icon');
|
22 |
+
const darkIcon = document.querySelector('.dark-icon');
|
23 |
+
|
24 |
+
if (this.theme === 'light') {
|
25 |
+
lightIcon.style.display = 'block';
|
26 |
+
darkIcon.style.display = 'none';
|
27 |
+
} else {
|
28 |
+
lightIcon.style.display = 'none';
|
29 |
+
darkIcon.style.display = 'block';
|
30 |
+
}
|
31 |
+
}
|
32 |
+
}
|
33 |
+
|
34 |
+
// Utility functions
|
35 |
+
function getParam(name) {
|
36 |
+
const url = new URL(window.location.href);
|
37 |
+
return url.searchParams.get(name);
|
38 |
+
}
|
39 |
+
|
40 |
+
function esc(s) {
|
41 |
+
return String(s).replace(/[&<>"]/g, (c) => ({'&':'&','<':'<','>':'>','"':'"'}[c]));
|
42 |
+
}
|
43 |
+
|
44 |
+
function parseMaybeJSON(str) {
|
45 |
+
if (typeof str !== 'string') return str;
|
46 |
+
try {
|
47 |
+
return JSON.parse(str);
|
48 |
+
} catch (e) {
|
49 |
+
const start = str.indexOf('{');
|
50 |
+
const end = str.lastIndexOf('}');
|
51 |
+
if (start !== -1 && end !== -1 && end > start) {
|
52 |
+
const sliced = str.slice(start, end + 1);
|
53 |
+
try { return JSON.parse(sliced); } catch {}
|
54 |
+
}
|
55 |
+
}
|
56 |
+
return str;
|
57 |
+
}
|
58 |
+
|
59 |
+
// Paper Evaluation Renderer
|
60 |
+
class PaperEvaluationRenderer {
|
61 |
+
constructor() {
|
62 |
+
this.themeManager = new ThemeManager();
|
63 |
+
this.init();
|
64 |
+
}
|
65 |
+
|
66 |
+
init() {
|
67 |
+
this.bindEvents();
|
68 |
+
}
|
69 |
+
|
70 |
+
bindEvents() {
|
71 |
+
// Theme toggle
|
72 |
+
const themeToggle = document.getElementById('themeToggle');
|
73 |
+
if (themeToggle) {
|
74 |
+
themeToggle.addEventListener('click', () => {
|
75 |
+
this.themeManager.toggle();
|
76 |
+
});
|
77 |
+
}
|
78 |
+
}
|
79 |
+
|
80 |
+
renderMetaGrid(meta) {
|
81 |
+
const metaGrid = document.getElementById('metaGrid');
|
82 |
+
if (!metaGrid) return;
|
83 |
+
|
84 |
+
const metaItems = [
|
85 |
+
{ label: 'Assessed At', value: meta.assessed_at || '-', icon: 'fas fa-calendar' },
|
86 |
+
{ label: 'Model', value: meta.model || '-', icon: 'fas fa-robot' },
|
87 |
+
{ label: 'Version', value: meta.version || '-', icon: 'fas fa-tag' },
|
88 |
+
{ label: 'Paper Path', value: meta.paper_path || '-', icon: 'fas fa-file-pdf', isLink: true }
|
89 |
+
];
|
90 |
+
|
91 |
+
metaGrid.innerHTML = metaItems.map(item => `
|
92 |
+
<div class="meta-item">
|
93 |
+
<span class="meta-label">
|
94 |
+
<i class="${item.icon}"></i>
|
95 |
+
${item.label}
|
96 |
+
</span>
|
97 |
+
<span class="meta-value">
|
98 |
+
${item.isLink && item.value !== '-' ?
|
99 |
+
`<a href="${esc(item.value)}" target="_blank">${esc(item.value)}</a>` :
|
100 |
+
esc(item.value)
|
101 |
+
}
|
102 |
+
</span>
|
103 |
+
</div>
|
104 |
+
`).join('');
|
105 |
+
}
|
106 |
+
|
107 |
+
renderDimensionCard(label, key, data, icon = 'fas fa-chart-bar') {
|
108 |
+
const item = data[key] || {};
|
109 |
+
const score = item.score !== undefined ? item.score : null;
|
110 |
+
const probability = item.probability_pct !== undefined ? item.probability_pct : null;
|
111 |
+
const analysis = item.analysis || '';
|
112 |
+
|
113 |
+
const extras = [];
|
114 |
+
if (Array.isArray(item.tools_models)) {
|
115 |
+
extras.push(`<span class="meta-item"><i class="fas fa-tools"></i> Tools/Models: ${item.tools_models.map(esc).join(', ')}</span>`);
|
116 |
+
}
|
117 |
+
if (item.coverage_pct_estimate !== undefined) {
|
118 |
+
extras.push(`<span class="meta-item"><i class="fas fa-percentage"></i> Coverage: ${esc(item.coverage_pct_estimate)}%</span>`);
|
119 |
+
}
|
120 |
+
|
121 |
+
return `
|
122 |
+
<div class="dimension-card">
|
123 |
+
<div class="dimension-header">
|
124 |
+
<div class="dimension-title">
|
125 |
+
<i class="${icon}"></i>
|
126 |
+
${esc(label)}
|
127 |
+
</div>
|
128 |
+
<div class="dimension-score">
|
129 |
+
${score !== null ? `<span class="score-badge">${score}</span>` : ''}
|
130 |
+
${probability !== null ? `<span class="score-badge probability">${probability}%</span>` : ''}
|
131 |
+
</div>
|
132 |
+
</div>
|
133 |
+
${extras.length > 0 ? `<div class="dimension-meta">${extras.join('')}</div>` : ''}
|
134 |
+
${analysis ? `<div class="dimension-analysis">${esc(analysis).replace(/\n/g, '<br/>')}</div>` : ''}
|
135 |
+
</div>
|
136 |
+
`;
|
137 |
+
}
|
138 |
+
|
139 |
+
renderContent(json) {
|
140 |
+
const contentEl = document.getElementById('content');
|
141 |
+
const titleEl = document.getElementById('title');
|
142 |
+
if (!contentEl || !titleEl) return;
|
143 |
+
|
144 |
+
const meta = json.metadata || {};
|
145 |
+
const paperId = getParam('id');
|
146 |
+
|
147 |
+
// Update title with paper ID
|
148 |
+
titleEl.textContent = `Paper Evaluation - ${paperId}`;
|
149 |
+
|
150 |
+
// Render meta grid
|
151 |
+
this.renderMetaGrid(meta);
|
152 |
+
|
153 |
+
// Executive Summary - styled like Hugging Face abstract
|
154 |
+
const execSummary = json.executive_summary ? `
|
155 |
+
<section class="evaluation-section">
|
156 |
+
<div class="section-header">
|
157 |
+
<h2><i class="fas fa-chart-pie"></i> Executive Summary</h2>
|
158 |
+
</div>
|
159 |
+
<div class="section-content">
|
160 |
+
<div class="summary-card">
|
161 |
+
<p class="summary-text">${esc(json.executive_summary)}</p>
|
162 |
+
</div>
|
163 |
+
</div>
|
164 |
+
</section>
|
165 |
+
` : '';
|
166 |
+
|
167 |
+
// Dimensions - create beautiful cards
|
168 |
+
const d = parseMaybeJSON(json.dimensions) || {};
|
169 |
+
const dims = [
|
170 |
+
['Task Formalization', 'task_formalization', 'fas fa-tasks'],
|
171 |
+
['Data & Resource Availability', 'data_resource_availability', 'fas fa-database'],
|
172 |
+
['Input-Output Complexity', 'input_output_complexity', 'fas fa-exchange-alt'],
|
173 |
+
['Real-World Interaction', 'real_world_interaction', 'fas fa-globe'],
|
174 |
+
['Existing AI Coverage', 'existing_ai_coverage', 'fas fa-robot'],
|
175 |
+
['Automation Barriers', 'automation_barriers', 'fas fa-shield-alt'],
|
176 |
+
['Human Originality', 'human_originality', 'fas fa-lightbulb'],
|
177 |
+
['Safety & Ethics', 'safety_ethics', 'fas fa-balance-scale'],
|
178 |
+
['Societal/Economic Impact', 'societal_economic_impact', 'fas fa-chart-line'],
|
179 |
+
['Technical Maturity Needed', 'technical_maturity_needed', 'fas fa-cogs'],
|
180 |
+
['3-Year Feasibility', 'three_year_feasibility', 'fas fa-calendar-alt'],
|
181 |
+
['Overall Automatability', 'overall_automatability', 'fas fa-magic'],
|
182 |
+
];
|
183 |
+
|
184 |
+
const dimensionsHtml = dims.map(([label, key, icon]) =>
|
185 |
+
this.renderDimensionCard(label, key, d, icon)
|
186 |
+
).join('');
|
187 |
+
|
188 |
+
// Recommendations - styled sections
|
189 |
+
const rec = json.recommendations || {};
|
190 |
+
const renderList = (arr) => {
|
191 |
+
return Array.isArray(arr) && arr.length ?
|
192 |
+
`<ul class="recommendation-list">${arr.map(x => `<li>${esc(x)}</li>`).join('')}</ul>` :
|
193 |
+
'<p class="no-data">No recommendations available.</p>';
|
194 |
+
};
|
195 |
+
|
196 |
+
const recommendationsHtml = `
|
197 |
+
<section class="evaluation-section">
|
198 |
+
<div class="section-header">
|
199 |
+
<h2><i class="fas fa-lightbulb"></i> Recommendations</h2>
|
200 |
+
</div>
|
201 |
+
<div class="section-content">
|
202 |
+
<div class="recommendations-grid">
|
203 |
+
<div class="recommendation-card">
|
204 |
+
<h3><i class="fas fa-user-graduate"></i> For Researchers</h3>
|
205 |
+
${renderList(rec.for_researchers)}
|
206 |
+
</div>
|
207 |
+
<div class="recommendation-card">
|
208 |
+
<h3><i class="fas fa-university"></i> For Institutions</h3>
|
209 |
+
${renderList(rec.for_institutions)}
|
210 |
+
</div>
|
211 |
+
<div class="recommendation-card">
|
212 |
+
<h3><i class="fas fa-cogs"></i> For AI Development</h3>
|
213 |
+
${renderList(rec.for_ai_development)}
|
214 |
+
</div>
|
215 |
+
</div>
|
216 |
+
</div>
|
217 |
+
</section>
|
218 |
+
`;
|
219 |
+
|
220 |
+
// Limitations
|
221 |
+
const lim = Array.isArray(json.limitations_uncertainties) ? json.limitations_uncertainties : [];
|
222 |
+
const limitationsHtml = `
|
223 |
+
<section class="evaluation-section">
|
224 |
+
<div class="section-header">
|
225 |
+
<h2><i class="fas fa-exclamation-triangle"></i> Limitations & Uncertainties</h2>
|
226 |
+
</div>
|
227 |
+
<div class="section-content">
|
228 |
+
<div class="limitations-card">
|
229 |
+
${lim.length ? `<ul class="limitations-list">${lim.map(x => `<li>${esc(x)}</li>`).join('')}</ul>` : '<p class="no-data">No limitations documented.</p>'}
|
230 |
+
</div>
|
231 |
+
</div>
|
232 |
+
</section>
|
233 |
+
`;
|
234 |
+
|
235 |
+
contentEl.innerHTML = execSummary +
|
236 |
+
`<section class="evaluation-section">
|
237 |
+
<div class="section-header">
|
238 |
+
<h2><i class="fas fa-chart-bar"></i> Detailed Dimensional Analysis</h2>
|
239 |
+
</div>
|
240 |
+
<div class="section-content">
|
241 |
+
<div class="dimensions-grid">
|
242 |
+
${dimensionsHtml}
|
243 |
+
</div>
|
244 |
+
</div>
|
245 |
+
</section>` +
|
246 |
+
recommendationsHtml +
|
247 |
+
limitationsHtml;
|
248 |
+
}
|
249 |
+
|
250 |
+
updateRadarChart(json) {
|
251 |
+
const radarEl = document.getElementById('radar');
|
252 |
+
const legendEl = document.getElementById('radar-legend');
|
253 |
+
const overallScoreEl = document.getElementById('overallScore');
|
254 |
+
|
255 |
+
if (!radarEl) return;
|
256 |
+
|
257 |
+
try {
|
258 |
+
const score = json.scorecard || {};
|
259 |
+
const d = parseMaybeJSON(json.dimensions) || {};
|
260 |
+
|
261 |
+
const labels = [
|
262 |
+
'Task Formalization',
|
263 |
+
'Data & Resources',
|
264 |
+
'Input-Output Complexity',
|
265 |
+
'Real-World Interaction',
|
266 |
+
'Existing AI Coverage',
|
267 |
+
'Human Originality',
|
268 |
+
'Safety & Ethics',
|
269 |
+
'Technical Maturity',
|
270 |
+
'3-Year Feasibility',
|
271 |
+
'Overall Automatability',
|
272 |
+
];
|
273 |
+
|
274 |
+
const values = [
|
275 |
+
Number(score.task_formalization ?? d.task_formalization?.score ?? 0),
|
276 |
+
Number(score.data_resource_availability ?? d.data_resource_availability?.score ?? 0),
|
277 |
+
Number(score.input_output_complexity ?? d.input_output_complexity?.score ?? 0),
|
278 |
+
Number(score.real_world_interaction ?? d.real_world_interaction?.score ?? 0),
|
279 |
+
Number(score.existing_ai_coverage ?? d.existing_ai_coverage?.score ?? 0),
|
280 |
+
Number(score.human_originality ?? d.human_originality?.score ?? 0),
|
281 |
+
Number(score.safety_ethics ?? d.safety_ethics?.score ?? 0),
|
282 |
+
Number(score.technical_maturity_needed ?? d.technical_maturity_needed?.score ?? 0),
|
283 |
+
Number((score.three_year_feasibility_pct ?? d.three_year_feasibility?.probability_pct ?? 0) / 25),
|
284 |
+
Number(score.overall_automatability ?? d.overall_automatability?.score ?? 0),
|
285 |
+
];
|
286 |
+
|
287 |
+
// Calculate overall score
|
288 |
+
const validScores = values.filter(v => v > 0);
|
289 |
+
const overallScore = validScores.length > 0 ?
|
290 |
+
(validScores.reduce((a, b) => a + b, 0) / validScores.length).toFixed(1) : '-';
|
291 |
+
|
292 |
+
if (overallScoreEl) {
|
293 |
+
overallScoreEl.innerHTML = `
|
294 |
+
<span class="score-number">${overallScore}</span>
|
295 |
+
<span class="score-label">Overall</span>
|
296 |
+
`;
|
297 |
+
}
|
298 |
+
|
299 |
+
this.drawRadar(radarEl, labels, values, 4);
|
300 |
+
this.setupRadarInteractions(radarEl);
|
301 |
+
|
302 |
+
if (legendEl) {
|
303 |
+
const labelConfig = [
|
304 |
+
{ abbr: 'TASK', color: '#3b82f6', full: 'Task Formalization' },
|
305 |
+
{ abbr: 'DATA', color: '#10b981', full: 'Data & Resources' },
|
306 |
+
{ abbr: 'I/O', color: '#f59e0b', full: 'Input-Output Complexity' },
|
307 |
+
{ abbr: 'REAL', color: '#8b5cf6', full: 'Real-World Interaction' },
|
308 |
+
{ abbr: 'AI', color: '#ef4444', full: 'Existing AI Coverage' },
|
309 |
+
{ abbr: 'HUMAN', color: '#06b6d4', full: 'Human Originality' },
|
310 |
+
{ abbr: 'SAFETY', color: '#84cc16', full: 'Safety & Ethics' },
|
311 |
+
{ abbr: 'TECH', color: '#f97316', full: 'Technical Maturity' },
|
312 |
+
{ abbr: '3YR', color: '#ec4899', full: '3-Year Feasibility' },
|
313 |
+
{ abbr: 'AUTO', color: '#6366f1', full: 'Overall Automatability' }
|
314 |
+
];
|
315 |
+
|
316 |
+
legendEl.innerHTML = `
|
317 |
+
<ul class="legend-list">
|
318 |
+
${labelConfig.map(config => `
|
319 |
+
<li>
|
320 |
+
<span class="legend-color" style="background-color: ${config.color}"></span>
|
321 |
+
<span class="legend-abbr" style="color: ${config.color}; font-weight: bold;">${config.abbr}</span>
|
322 |
+
<span class="legend-full">${esc(config.full)}</span>
|
323 |
+
</li>
|
324 |
+
`).join('')}
|
325 |
+
</ul>
|
326 |
+
`;
|
327 |
+
}
|
328 |
+
} catch (error) {
|
329 |
+
console.error('Error updating radar chart:', error);
|
330 |
+
}
|
331 |
+
}
|
332 |
+
|
333 |
+
setupRadarInteractions(canvas) {
|
334 |
+
if (!canvas || !this.dotPositions) return;
|
335 |
+
|
336 |
+
// Create tooltip element
|
337 |
+
let tooltip = document.getElementById('radar-tooltip');
|
338 |
+
if (!tooltip) {
|
339 |
+
tooltip = document.createElement('div');
|
340 |
+
tooltip.id = 'radar-tooltip';
|
341 |
+
tooltip.className = 'radar-tooltip';
|
342 |
+
document.body.appendChild(tooltip);
|
343 |
+
}
|
344 |
+
|
345 |
+
const handleMouseMove = (e) => {
|
346 |
+
const rect = canvas.getBoundingClientRect();
|
347 |
+
const x = e.clientX - rect.left;
|
348 |
+
const y = e.clientY - rect.top;
|
349 |
+
|
350 |
+
// Check if mouse is near any dot
|
351 |
+
let hoveredDot = null;
|
352 |
+
const hoverRadius = 15;
|
353 |
+
|
354 |
+
for (const dot of this.dotPositions) {
|
355 |
+
const distance = Math.sqrt((x - dot.x) ** 2 + (y - dot.y) ** 2);
|
356 |
+
if (distance <= hoverRadius) {
|
357 |
+
hoveredDot = dot;
|
358 |
+
break;
|
359 |
+
}
|
360 |
+
}
|
361 |
+
|
362 |
+
if (hoveredDot) {
|
363 |
+
tooltip.style.display = 'block';
|
364 |
+
tooltip.style.left = e.clientX + 10 + 'px';
|
365 |
+
tooltip.style.top = e.clientY - 30 + 'px';
|
366 |
+
tooltip.innerHTML = `
|
367 |
+
<div class="tooltip-content">
|
368 |
+
<span class="tooltip-abbr" style="color: ${hoveredDot.config.color}">${hoveredDot.config.abbr}</span>
|
369 |
+
<span class="tooltip-full">${hoveredDot.config.full || ''}</span>
|
370 |
+
</div>
|
371 |
+
`;
|
372 |
+
canvas.style.cursor = 'pointer';
|
373 |
+
} else {
|
374 |
+
tooltip.style.display = 'none';
|
375 |
+
canvas.style.cursor = 'default';
|
376 |
+
}
|
377 |
+
};
|
378 |
+
|
379 |
+
const handleMouseLeave = () => {
|
380 |
+
tooltip.style.display = 'none';
|
381 |
+
canvas.style.cursor = 'default';
|
382 |
+
};
|
383 |
+
|
384 |
+
// Remove existing listeners
|
385 |
+
canvas.removeEventListener('mousemove', handleMouseMove);
|
386 |
+
canvas.removeEventListener('mouseleave', handleMouseLeave);
|
387 |
+
|
388 |
+
// Add new listeners
|
389 |
+
canvas.addEventListener('mousemove', handleMouseMove);
|
390 |
+
canvas.addEventListener('mouseleave', handleMouseLeave);
|
391 |
+
}
|
392 |
+
|
393 |
+
drawRadar(canvas, labels, values, maxValue) {
|
394 |
+
if (!canvas || !canvas.getContext) return;
|
395 |
+
|
396 |
+
const ctx = canvas.getContext('2d');
|
397 |
+
const w = canvas.width, h = canvas.height;
|
398 |
+
ctx.clearRect(0, 0, w, h);
|
399 |
+
|
400 |
+
const cx = w / 2, cy = h / 2, radius = Math.min(w, h) * 0.42;
|
401 |
+
const n = values.length;
|
402 |
+
const angleStep = (Math.PI * 2) / n;
|
403 |
+
|
404 |
+
// Get theme colors
|
405 |
+
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
406 |
+
const gridColor = isDark ? '#475569' : '#e2e8f0';
|
407 |
+
const axisColor = isDark ? '#64748b' : '#cbd5e1';
|
408 |
+
const fillColor = isDark ? 'rgba(34, 211, 238, 0.2)' : 'rgba(59, 130, 246, 0.2)';
|
409 |
+
const strokeColor = isDark ? '#22d3ee' : '#3b82f6';
|
410 |
+
const textColor = isDark ? '#e2e8f0' : '#475569';
|
411 |
+
|
412 |
+
// Define colors and abbreviations for each dimension
|
413 |
+
const labelConfig = [
|
414 |
+
{ color: '#3b82f6', abbr: 'TASK' }, // Task Formalization
|
415 |
+
{ color: '#10b981', abbr: 'DATA' }, // Data & Resources
|
416 |
+
{ color: '#f59e0b', abbr: 'I/O' }, // Input-Output Complexity
|
417 |
+
{ color: '#8b5cf6', abbr: 'REAL' }, // Real-World Interaction
|
418 |
+
{ color: '#ef4444', abbr: 'AI' }, // Existing AI Coverage
|
419 |
+
{ color: '#06b6d4', abbr: 'HUMAN' }, // Human Originality
|
420 |
+
{ color: '#84cc16', abbr: 'SAFETY' }, // Safety & Ethics
|
421 |
+
{ color: '#f97316', abbr: 'TECH' }, // Technical Maturity
|
422 |
+
{ color: '#ec4899', abbr: '3YR' }, // 3-Year Feasibility
|
423 |
+
{ color: '#6366f1', abbr: 'AUTO' } // Overall Automatability
|
424 |
+
];
|
425 |
+
|
426 |
+
// Store dot positions for mouse interaction
|
427 |
+
this.dotPositions = [];
|
428 |
+
|
429 |
+
// Draw grid (5 rings)
|
430 |
+
ctx.strokeStyle = gridColor;
|
431 |
+
ctx.lineWidth = 1;
|
432 |
+
for (let r = 1; r <= 5; r++) {
|
433 |
+
const rr = (radius * r) / 5;
|
434 |
+
ctx.beginPath();
|
435 |
+
for (let i = 0; i < n; i++) {
|
436 |
+
const ang = i * angleStep - Math.PI / 2;
|
437 |
+
const x = cx + rr * Math.cos(ang);
|
438 |
+
const y = cy + rr * Math.sin(ang);
|
439 |
+
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
440 |
+
}
|
441 |
+
ctx.closePath();
|
442 |
+
ctx.stroke();
|
443 |
+
}
|
444 |
+
|
445 |
+
// Draw axes
|
446 |
+
ctx.strokeStyle = axisColor;
|
447 |
+
for (let i = 0; i < n; i++) {
|
448 |
+
const ang = i * angleStep - Math.PI / 2;
|
449 |
+
const x = cx + radius * Math.cos(ang);
|
450 |
+
const y = cy + radius * Math.sin(ang);
|
451 |
+
ctx.beginPath();
|
452 |
+
ctx.moveTo(cx, cy);
|
453 |
+
ctx.lineTo(x, y);
|
454 |
+
ctx.stroke();
|
455 |
+
}
|
456 |
+
|
457 |
+
// Draw polygon
|
458 |
+
ctx.fillStyle = fillColor;
|
459 |
+
ctx.strokeStyle = strokeColor;
|
460 |
+
ctx.lineWidth = 2;
|
461 |
+
ctx.beginPath();
|
462 |
+
for (let i = 0; i < n; i++) {
|
463 |
+
const v = Math.max(0, Math.min(maxValue, Number(values[i] || 0)));
|
464 |
+
const r = (v / maxValue) * radius;
|
465 |
+
const ang = i * angleStep - Math.PI / 2;
|
466 |
+
const x = cx + r * Math.cos(ang);
|
467 |
+
const y = cy + r * Math.sin(ang);
|
468 |
+
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
|
469 |
+
}
|
470 |
+
ctx.closePath();
|
471 |
+
ctx.fill();
|
472 |
+
ctx.stroke();
|
473 |
+
|
474 |
+
// Draw axis endpoints with colored dots and store positions
|
475 |
+
for (let i = 0; i < n; i++) {
|
476 |
+
const ang = i * angleStep - Math.PI / 2;
|
477 |
+
const x = cx + radius * Math.cos(ang);
|
478 |
+
const y = cy + radius * Math.sin(ang);
|
479 |
+
|
480 |
+
// Store position for mouse interaction
|
481 |
+
this.dotPositions[i] = { x, y, index: i, config: labelConfig[i] };
|
482 |
+
|
483 |
+
// Draw colored dot at the end of each axis
|
484 |
+
const dotRadius = 6; // Slightly larger for better hover detection
|
485 |
+
ctx.fillStyle = labelConfig[i]?.color || textColor;
|
486 |
+
ctx.beginPath();
|
487 |
+
ctx.arc(x, y, dotRadius, 0, Math.PI * 2);
|
488 |
+
ctx.fill();
|
489 |
+
|
490 |
+
// Draw white border around dot
|
491 |
+
ctx.strokeStyle = isDark ? '#1e293b' : '#ffffff';
|
492 |
+
ctx.lineWidth = 2;
|
493 |
+
ctx.stroke();
|
494 |
+
}
|
495 |
+
}
|
496 |
+
|
497 |
+
render(json) {
|
498 |
+
this.renderContent(json);
|
499 |
+
this.updateRadarChart(json);
|
500 |
+
}
|
501 |
+
}
|
502 |
+
|
503 |
+
// Main Application
|
504 |
+
class PaperEvaluationApp {
|
505 |
+
constructor() {
|
506 |
+
this.renderer = new PaperEvaluationRenderer();
|
507 |
+
this.init();
|
508 |
+
}
|
509 |
+
|
510 |
+
async init() {
|
511 |
+
const id = getParam('id');
|
512 |
+
console.log('PaperEvaluationApp init with ID:', id);
|
513 |
+
|
514 |
+
if (!id) {
|
515 |
+
const contentEl = document.getElementById('content');
|
516 |
+
if (contentEl) {
|
517 |
+
contentEl.innerHTML = `
|
518 |
+
<div style="text-align: center; padding: 48px; color: var(--text-muted);">
|
519 |
+
<i class="fas fa-exclamation-triangle" style="font-size: 48px; margin-bottom: 16px; opacity: 0.5;"></i>
|
520 |
+
<h3>Missing Paper ID</h3>
|
521 |
+
<p>Please provide a valid paper ID in the URL.</p>
|
522 |
+
</div>
|
523 |
+
`;
|
524 |
+
}
|
525 |
+
return;
|
526 |
+
}
|
527 |
+
|
528 |
+
try {
|
529 |
+
console.log('Fetching evaluation for ID:', id);
|
530 |
+
const response = await fetch(`/api/eval/${encodeURIComponent(id)}`);
|
531 |
+
console.log('Response status:', response.status);
|
532 |
+
|
533 |
+
if (!response.ok) {
|
534 |
+
throw new Error(`Evaluation not found: ${response.status}`);
|
535 |
+
}
|
536 |
+
|
537 |
+
const json = await response.json();
|
538 |
+
console.log('Received JSON data:', Object.keys(json));
|
539 |
+
|
540 |
+
// Fix stringified dimensions
|
541 |
+
if (json && typeof json.dimensions === 'string') {
|
542 |
+
try {
|
543 |
+
json.dimensions = JSON.parse(json.dimensions);
|
544 |
+
console.log('Successfully parsed dimensions JSON');
|
545 |
+
} catch (e) {
|
546 |
+
console.warn('Failed to parse dimensions JSON:', e);
|
547 |
+
}
|
548 |
+
}
|
549 |
+
|
550 |
+
console.log('Rendering evaluation...');
|
551 |
+
this.renderer.render(json);
|
552 |
+
console.log('Evaluation rendered successfully');
|
553 |
+
|
554 |
+
} catch (error) {
|
555 |
+
console.error('Error loading evaluation:', error);
|
556 |
+
const contentEl = document.getElementById('content');
|
557 |
+
if (contentEl) {
|
558 |
+
contentEl.innerHTML = `
|
559 |
+
<div style="text-align: center; padding: 48px; color: var(--text-muted);">
|
560 |
+
<i class="fas fa-exclamation-triangle" style="font-size: 48px; margin-bottom: 16px; opacity: 0.5;"></i>
|
561 |
+
<h3>Evaluation Not Found</h3>
|
562 |
+
<p>The requested evaluation could not be loaded: ${error.message}</p>
|
563 |
+
<a href="/" class="action-btn primary" style="margin-top: 16px; display: inline-flex; align-items: center; gap: 8px;">
|
564 |
+
<i class="fas fa-arrow-left"></i>Back to Daily Papers
|
565 |
+
</a>
|
566 |
+
</div>
|
567 |
+
`;
|
568 |
+
}
|
569 |
+
}
|
570 |
+
}
|
571 |
+
}
|
572 |
+
|
573 |
+
// Initialize the application when DOM is loaded
|
574 |
+
document.addEventListener('DOMContentLoaded', () => {
|
575 |
+
new PaperEvaluationApp();
|
576 |
+
});
|
577 |
+
|
578 |
+
|
frontend/styles.css
ADDED
@@ -0,0 +1,1375 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
/* CSS Variables for Theme Switching */
|
2 |
+
:root {
|
3 |
+
/* Light Theme */
|
4 |
+
--bg-primary: #ffffff;
|
5 |
+
--bg-secondary: #f8fafc;
|
6 |
+
--bg-tertiary: #f1f5f9;
|
7 |
+
--text-primary: #0f172a;
|
8 |
+
--text-secondary: #475569;
|
9 |
+
--text-muted: #64748b;
|
10 |
+
--border-light: #e2e8f0;
|
11 |
+
--border-medium: #cbd5e1;
|
12 |
+
--accent-primary: #3b82f6;
|
13 |
+
--accent-secondary: #06b6d4;
|
14 |
+
--accent-success: #10b981;
|
15 |
+
--accent-warning: #f59e0b;
|
16 |
+
--accent-danger: #ef4444;
|
17 |
+
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
18 |
+
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
19 |
+
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
20 |
+
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
|
21 |
+
}
|
22 |
+
|
23 |
+
[data-theme="dark"] {
|
24 |
+
/* Dark Theme */
|
25 |
+
--bg-primary: #0f172a;
|
26 |
+
--bg-secondary: #1e293b;
|
27 |
+
--bg-tertiary: #334155;
|
28 |
+
--text-primary: #f8fafc;
|
29 |
+
--text-secondary: #cbd5e1;
|
30 |
+
--text-muted: #94a3b8;
|
31 |
+
--border-light: #334155;
|
32 |
+
--border-medium: #475569;
|
33 |
+
--accent-primary: #60a5fa;
|
34 |
+
--accent-secondary: #22d3ee;
|
35 |
+
--accent-success: #34d399;
|
36 |
+
--accent-warning: #fbbf24;
|
37 |
+
--accent-danger: #f87171;
|
38 |
+
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
|
39 |
+
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.3), 0 2px 4px -2px rgb(0 0 0 / 0.3);
|
40 |
+
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.3), 0 4px 6px -4px rgb(0 0 0 / 0.3);
|
41 |
+
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.3), 0 8px 10px -6px rgb(0 0 0 / 0.3);
|
42 |
+
}
|
43 |
+
|
44 |
+
/* Reset and Base Styles */
|
45 |
+
* {
|
46 |
+
box-sizing: border-box;
|
47 |
+
margin: 0;
|
48 |
+
padding: 0;
|
49 |
+
}
|
50 |
+
|
51 |
+
body {
|
52 |
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
53 |
+
background-color: var(--bg-primary);
|
54 |
+
color: var(--text-primary);
|
55 |
+
line-height: 1.6;
|
56 |
+
transition: background-color 0.3s ease, color 0.3s ease;
|
57 |
+
}
|
58 |
+
|
59 |
+
/* Navigation Bar */
|
60 |
+
.navbar {
|
61 |
+
background-color: var(--bg-primary);
|
62 |
+
border-bottom: 1px solid var(--border-light);
|
63 |
+
position: sticky;
|
64 |
+
top: 0;
|
65 |
+
z-index: 1000;
|
66 |
+
box-shadow: var(--shadow-sm);
|
67 |
+
}
|
68 |
+
|
69 |
+
.nav-container {
|
70 |
+
max-width: 1400px;
|
71 |
+
margin: 0 auto;
|
72 |
+
padding: 0 24px;
|
73 |
+
height: 64px;
|
74 |
+
display: flex;
|
75 |
+
align-items: center;
|
76 |
+
justify-content: space-between;
|
77 |
+
}
|
78 |
+
|
79 |
+
.nav-left .logo {
|
80 |
+
display: flex;
|
81 |
+
align-items: center;
|
82 |
+
gap: 12px;
|
83 |
+
font-size: 20px;
|
84 |
+
font-weight: 700;
|
85 |
+
color: var(--text-primary);
|
86 |
+
}
|
87 |
+
|
88 |
+
.logo i {
|
89 |
+
color: var(--accent-primary);
|
90 |
+
font-size: 24px;
|
91 |
+
}
|
92 |
+
|
93 |
+
.nav-center {
|
94 |
+
flex: 1;
|
95 |
+
max-width: 600px;
|
96 |
+
margin: 0 48px;
|
97 |
+
}
|
98 |
+
|
99 |
+
.search-container {
|
100 |
+
position: relative;
|
101 |
+
width: 100%;
|
102 |
+
}
|
103 |
+
|
104 |
+
.search-input {
|
105 |
+
width: 100%;
|
106 |
+
padding: 12px 48px;
|
107 |
+
border: 1px solid var(--border-medium);
|
108 |
+
border-radius: 12px;
|
109 |
+
background-color: var(--bg-secondary);
|
110 |
+
color: var(--text-primary);
|
111 |
+
font-size: 14px;
|
112 |
+
transition: all 0.2s ease;
|
113 |
+
}
|
114 |
+
|
115 |
+
.search-input:focus {
|
116 |
+
outline: none;
|
117 |
+
border-color: var(--accent-primary);
|
118 |
+
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
|
119 |
+
}
|
120 |
+
|
121 |
+
.search-icon {
|
122 |
+
position: absolute;
|
123 |
+
left: 16px;
|
124 |
+
top: 50%;
|
125 |
+
transform: translateY(-50%);
|
126 |
+
color: var(--text-muted);
|
127 |
+
font-size: 14px;
|
128 |
+
}
|
129 |
+
|
130 |
+
.search-magic {
|
131 |
+
position: absolute;
|
132 |
+
right: 16px;
|
133 |
+
top: 50%;
|
134 |
+
transform: translateY(-50%);
|
135 |
+
color: var(--accent-secondary);
|
136 |
+
font-size: 14px;
|
137 |
+
cursor: pointer;
|
138 |
+
}
|
139 |
+
|
140 |
+
.theme-toggle {
|
141 |
+
background: none;
|
142 |
+
border: none;
|
143 |
+
padding: 8px;
|
144 |
+
border-radius: 8px;
|
145 |
+
cursor: pointer;
|
146 |
+
color: var(--text-secondary);
|
147 |
+
transition: all 0.2s ease;
|
148 |
+
position: relative;
|
149 |
+
width: 40px;
|
150 |
+
height: 40px;
|
151 |
+
display: flex;
|
152 |
+
align-items: center;
|
153 |
+
justify-content: center;
|
154 |
+
}
|
155 |
+
|
156 |
+
.theme-toggle:hover {
|
157 |
+
background-color: var(--bg-secondary);
|
158 |
+
color: var(--text-primary);
|
159 |
+
}
|
160 |
+
|
161 |
+
.theme-toggle .light-icon {
|
162 |
+
display: block;
|
163 |
+
}
|
164 |
+
|
165 |
+
.theme-toggle .dark-icon {
|
166 |
+
display: none;
|
167 |
+
}
|
168 |
+
|
169 |
+
[data-theme="dark"] .theme-toggle .light-icon {
|
170 |
+
display: none;
|
171 |
+
}
|
172 |
+
|
173 |
+
[data-theme="dark"] .theme-toggle .dark-icon {
|
174 |
+
display: block;
|
175 |
+
}
|
176 |
+
|
177 |
+
/* Main Header */
|
178 |
+
.main-header {
|
179 |
+
background-color: var(--bg-primary);
|
180 |
+
border-bottom: 1px solid var(--border-light);
|
181 |
+
padding: 32px 0;
|
182 |
+
}
|
183 |
+
|
184 |
+
.header-container {
|
185 |
+
max-width: 1400px;
|
186 |
+
margin: 0 auto;
|
187 |
+
padding: 0 24px;
|
188 |
+
display: grid;
|
189 |
+
grid-template-columns: 1fr 2fr 1fr;
|
190 |
+
gap: 32px;
|
191 |
+
align-items: center;
|
192 |
+
}
|
193 |
+
|
194 |
+
.header-left h1 {
|
195 |
+
font-size: 32px;
|
196 |
+
font-weight: 800;
|
197 |
+
color: var(--text-primary);
|
198 |
+
margin-bottom: 8px;
|
199 |
+
}
|
200 |
+
|
201 |
+
.subtitle {
|
202 |
+
color: var(--text-secondary);
|
203 |
+
font-size: 16px;
|
204 |
+
}
|
205 |
+
|
206 |
+
.ai-search-container {
|
207 |
+
position: relative;
|
208 |
+
width: 100%;
|
209 |
+
}
|
210 |
+
|
211 |
+
.ai-search-input {
|
212 |
+
width: 100%;
|
213 |
+
padding: 16px 48px;
|
214 |
+
border: 1px solid var(--border-medium);
|
215 |
+
border-radius: 16px;
|
216 |
+
background-color: var(--bg-secondary);
|
217 |
+
color: var(--text-primary);
|
218 |
+
font-size: 16px;
|
219 |
+
transition: all 0.2s ease;
|
220 |
+
}
|
221 |
+
|
222 |
+
.ai-search-input:focus {
|
223 |
+
outline: none;
|
224 |
+
border-color: var(--accent-primary);
|
225 |
+
box-shadow: 0 0 0 3px rgb(59 130 246 / 0.1);
|
226 |
+
}
|
227 |
+
|
228 |
+
.ai-search-container i:first-child {
|
229 |
+
position: absolute;
|
230 |
+
left: 16px;
|
231 |
+
top: 50%;
|
232 |
+
transform: translateY(-50%);
|
233 |
+
color: var(--accent-secondary);
|
234 |
+
font-size: 16px;
|
235 |
+
}
|
236 |
+
|
237 |
+
.ai-search-container i:last-child {
|
238 |
+
position: absolute;
|
239 |
+
right: 16px;
|
240 |
+
top: 50%;
|
241 |
+
transform: translateY(-50%);
|
242 |
+
color: var(--text-muted);
|
243 |
+
font-size: 16px;
|
244 |
+
}
|
245 |
+
|
246 |
+
.header-right {
|
247 |
+
display: flex;
|
248 |
+
flex-direction: column;
|
249 |
+
gap: 16px;
|
250 |
+
align-items: flex-end;
|
251 |
+
}
|
252 |
+
|
253 |
+
.filter-buttons {
|
254 |
+
display: flex;
|
255 |
+
gap: 8px;
|
256 |
+
}
|
257 |
+
|
258 |
+
.filter-btn {
|
259 |
+
padding: 8px 16px;
|
260 |
+
border: 1px solid var(--border-medium);
|
261 |
+
border-radius: 8px;
|
262 |
+
background-color: var(--bg-secondary);
|
263 |
+
color: var(--text-secondary);
|
264 |
+
font-size: 14px;
|
265 |
+
font-weight: 500;
|
266 |
+
cursor: pointer;
|
267 |
+
transition: all 0.2s ease;
|
268 |
+
}
|
269 |
+
|
270 |
+
.filter-btn:hover {
|
271 |
+
background-color: var(--bg-tertiary);
|
272 |
+
color: var(--text-primary);
|
273 |
+
}
|
274 |
+
|
275 |
+
.filter-btn.active {
|
276 |
+
background-color: var(--accent-primary);
|
277 |
+
color: white;
|
278 |
+
border-color: var(--accent-primary);
|
279 |
+
}
|
280 |
+
|
281 |
+
.star-btn {
|
282 |
+
padding: 8px;
|
283 |
+
color: var(--accent-warning);
|
284 |
+
}
|
285 |
+
|
286 |
+
.date-navigation {
|
287 |
+
display: flex;
|
288 |
+
align-items: center;
|
289 |
+
gap: 12px;
|
290 |
+
}
|
291 |
+
|
292 |
+
.nav-btn {
|
293 |
+
background: none;
|
294 |
+
border: 1px solid var(--border-medium);
|
295 |
+
border-radius: 8px;
|
296 |
+
padding: 8px 12px;
|
297 |
+
color: var(--text-secondary);
|
298 |
+
cursor: pointer;
|
299 |
+
transition: all 0.2s ease;
|
300 |
+
}
|
301 |
+
|
302 |
+
.nav-btn:hover {
|
303 |
+
background-color: var(--bg-secondary);
|
304 |
+
color: var(--text-primary);
|
305 |
+
}
|
306 |
+
|
307 |
+
.date-display {
|
308 |
+
font-weight: 600;
|
309 |
+
color: var(--text-primary);
|
310 |
+
min-width: 80px;
|
311 |
+
text-align: center;
|
312 |
+
}
|
313 |
+
|
314 |
+
/* Main Content */
|
315 |
+
.main-content {
|
316 |
+
background-color: var(--bg-secondary);
|
317 |
+
min-height: calc(100vh - 200px);
|
318 |
+
padding: 32px 0;
|
319 |
+
}
|
320 |
+
|
321 |
+
.content-container {
|
322 |
+
max-width: 1400px;
|
323 |
+
margin: 0 auto;
|
324 |
+
padding: 0 24px;
|
325 |
+
}
|
326 |
+
|
327 |
+
.cards-grid {
|
328 |
+
display: grid;
|
329 |
+
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
330 |
+
gap: 24px;
|
331 |
+
}
|
332 |
+
|
333 |
+
/* Hugging Face Style Paper Cards */
|
334 |
+
.hf-paper-card {
|
335 |
+
position: relative;
|
336 |
+
display: flex;
|
337 |
+
flex-direction: column;
|
338 |
+
overflow: hidden;
|
339 |
+
border-radius: 12px;
|
340 |
+
border: 1px solid var(--border-light);
|
341 |
+
background-color: var(--bg-primary);
|
342 |
+
transition: all 0.2s ease;
|
343 |
+
}
|
344 |
+
|
345 |
+
.hf-paper-card:hover {
|
346 |
+
border-color: var(--border-medium);
|
347 |
+
box-shadow: var(--shadow-md);
|
348 |
+
}
|
349 |
+
|
350 |
+
/* Paper Thumbnail */
|
351 |
+
.paper-thumbnail-link {
|
352 |
+
position: relative;
|
353 |
+
display: block;
|
354 |
+
height: 224px;
|
355 |
+
width: 100%;
|
356 |
+
cursor: pointer;
|
357 |
+
overflow: hidden;
|
358 |
+
border-radius: 12px;
|
359 |
+
background-color: var(--bg-secondary);
|
360 |
+
}
|
361 |
+
|
362 |
+
.paper-thumbnail-img {
|
363 |
+
height: 100%;
|
364 |
+
width: 100%;
|
365 |
+
object-fit: cover;
|
366 |
+
object-position: top;
|
367 |
+
opacity: 0.8;
|
368 |
+
transition: opacity 0.2s ease;
|
369 |
+
}
|
370 |
+
|
371 |
+
[data-theme="dark"] .paper-thumbnail-img {
|
372 |
+
opacity: 0.7;
|
373 |
+
filter: invert(1);
|
374 |
+
}
|
375 |
+
|
376 |
+
.paper-thumbnail-link:hover .paper-thumbnail-img {
|
377 |
+
opacity: 1;
|
378 |
+
}
|
379 |
+
|
380 |
+
/* Submitted by badge */
|
381 |
+
.submitted-by-badge {
|
382 |
+
position: absolute;
|
383 |
+
right: 8px;
|
384 |
+
top: 216px;
|
385 |
+
margin-top: -32px;
|
386 |
+
display: flex;
|
387 |
+
height: 24px;
|
388 |
+
align-items: center;
|
389 |
+
gap: 4px;
|
390 |
+
align-self: flex-end;
|
391 |
+
white-space: nowrap;
|
392 |
+
border-radius: 6px;
|
393 |
+
border: 1px solid var(--border-light);
|
394 |
+
background-color: var(--bg-primary);
|
395 |
+
padding: 0 8px;
|
396 |
+
font-size: 12px;
|
397 |
+
line-height: 1;
|
398 |
+
color: var(--text-secondary);
|
399 |
+
}
|
400 |
+
|
401 |
+
.submitter-avatar-img {
|
402 |
+
width: 10px;
|
403 |
+
height: 10px;
|
404 |
+
border-radius: 50%;
|
405 |
+
flex-shrink: 0;
|
406 |
+
}
|
407 |
+
|
408 |
+
.submitter-avatar-placeholder {
|
409 |
+
width: 16px;
|
410 |
+
height: 16px;
|
411 |
+
border-radius: 50%;
|
412 |
+
background-color: var(--accent-primary);
|
413 |
+
display: flex;
|
414 |
+
align-items: center;
|
415 |
+
justify-content: center;
|
416 |
+
flex-shrink: 0;
|
417 |
+
margin-right: 8px;
|
418 |
+
}
|
419 |
+
|
420 |
+
.submitter-avatar-placeholder i {
|
421 |
+
font-size: 10px;
|
422 |
+
color: white;
|
423 |
+
}
|
424 |
+
|
425 |
+
/* Card Content */
|
426 |
+
.card-content {
|
427 |
+
background: linear-gradient(to bottom, var(--bg-secondary), var(--bg-primary));
|
428 |
+
margin-top: -8px;
|
429 |
+
display: flex;
|
430 |
+
padding: 32px 24px 24px 24px;
|
431 |
+
gap: 24px;
|
432 |
+
}
|
433 |
+
|
434 |
+
/* Upvote Section */
|
435 |
+
.upvote-section {
|
436 |
+
display: flex;
|
437 |
+
flex-wrap: wrap;
|
438 |
+
align-items: center;
|
439 |
+
gap: 10px;
|
440 |
+
padding-top: 4px;
|
441 |
+
z-index: 1;
|
442 |
+
}
|
443 |
+
|
444 |
+
.upvote-button {
|
445 |
+
display: flex;
|
446 |
+
height: 56px;
|
447 |
+
width: 48px;
|
448 |
+
gap: 4px;
|
449 |
+
border-radius: 12px;
|
450 |
+
flex-shrink: 0;
|
451 |
+
cursor: pointer;
|
452 |
+
user-select: none;
|
453 |
+
flex-direction: column;
|
454 |
+
align-items: center;
|
455 |
+
justify-content: center;
|
456 |
+
align-self: flex-start;
|
457 |
+
border: 1px solid var(--border-medium);
|
458 |
+
background-color: var(--bg-primary);
|
459 |
+
box-shadow: var(--shadow-sm);
|
460 |
+
transition: all 0.2s ease;
|
461 |
+
}
|
462 |
+
|
463 |
+
.upvote-button:hover {
|
464 |
+
border-color: var(--accent-primary);
|
465 |
+
box-shadow: var(--shadow-md);
|
466 |
+
}
|
467 |
+
|
468 |
+
.upvote-checkbox {
|
469 |
+
display: none;
|
470 |
+
}
|
471 |
+
|
472 |
+
.upvote-checkbox:checked + .upvote-icon {
|
473 |
+
color: var(--accent-primary);
|
474 |
+
}
|
475 |
+
|
476 |
+
.upvote-icon {
|
477 |
+
font-size: 14px;
|
478 |
+
color: var(--text-muted);
|
479 |
+
transition: color 0.2s ease;
|
480 |
+
}
|
481 |
+
|
482 |
+
.upvote-count {
|
483 |
+
font-size: 14px;
|
484 |
+
font-weight: 600;
|
485 |
+
line-height: 1;
|
486 |
+
color: var(--text-primary);
|
487 |
+
}
|
488 |
+
|
489 |
+
/* Paper Info */
|
490 |
+
.paper-info {
|
491 |
+
width: 100%;
|
492 |
+
}
|
493 |
+
|
494 |
+
.paper-title {
|
495 |
+
margin-bottom: 4px;
|
496 |
+
font-size: 18px;
|
497 |
+
line-height: 1.5;
|
498 |
+
font-weight: 600;
|
499 |
+
}
|
500 |
+
|
501 |
+
.title-link {
|
502 |
+
display: -webkit-box;
|
503 |
+
-webkit-line-clamp: 3;
|
504 |
+
-webkit-box-orient: vertical;
|
505 |
+
overflow: hidden;
|
506 |
+
cursor: pointer;
|
507 |
+
text-decoration: none;
|
508 |
+
color: var(--text-primary);
|
509 |
+
transition: color 0.2s ease;
|
510 |
+
}
|
511 |
+
|
512 |
+
.title-link:hover {
|
513 |
+
text-decoration: underline;
|
514 |
+
}
|
515 |
+
|
516 |
+
/* Paper Meta */
|
517 |
+
.paper-meta {
|
518 |
+
display: flex;
|
519 |
+
align-items: center;
|
520 |
+
justify-content: space-between;
|
521 |
+
gap: 8px;
|
522 |
+
}
|
523 |
+
|
524 |
+
.authors-section {
|
525 |
+
display: flex;
|
526 |
+
align-items: center;
|
527 |
+
}
|
528 |
+
|
529 |
+
.authors-link {
|
530 |
+
display: flex;
|
531 |
+
text-decoration: none;
|
532 |
+
color: inherit;
|
533 |
+
}
|
534 |
+
|
535 |
+
.author-avatars-list {
|
536 |
+
display: flex;
|
537 |
+
align-items: center;
|
538 |
+
flex-direction: row-reverse;
|
539 |
+
font-size: 14px;
|
540 |
+
list-style: none;
|
541 |
+
padding: 0;
|
542 |
+
margin: 0;
|
543 |
+
}
|
544 |
+
|
545 |
+
.author-avatars-list li {
|
546 |
+
margin-right: -8px;
|
547 |
+
height: 16px;
|
548 |
+
width: 16px;
|
549 |
+
background: linear-gradient(to bottom right, var(--bg-tertiary), var(--bg-secondary));
|
550 |
+
display: block;
|
551 |
+
flex-shrink: 0;
|
552 |
+
border-radius: 50%;
|
553 |
+
border: 2px solid var(--bg-primary);
|
554 |
+
}
|
555 |
+
|
556 |
+
@media (min-width: 768px) {
|
557 |
+
.author-avatars-list li {
|
558 |
+
height: 20px;
|
559 |
+
width: 20px;
|
560 |
+
}
|
561 |
+
}
|
562 |
+
|
563 |
+
.author-count {
|
564 |
+
margin-left: 12px;
|
565 |
+
color: var(--text-muted);
|
566 |
+
font-size: 14px;
|
567 |
+
}
|
568 |
+
|
569 |
+
/* Engagement Metrics */
|
570 |
+
.engagement-metrics {
|
571 |
+
display: flex;
|
572 |
+
align-items: center;
|
573 |
+
gap: 8px;
|
574 |
+
}
|
575 |
+
|
576 |
+
.metric-link {
|
577 |
+
display: flex;
|
578 |
+
align-items: center;
|
579 |
+
gap: 4px;
|
580 |
+
border-radius: 6px;
|
581 |
+
border: 1px solid var(--border-light);
|
582 |
+
padding: 2px 4px;
|
583 |
+
font-size: 12px;
|
584 |
+
color: var(--text-muted);
|
585 |
+
text-decoration: none;
|
586 |
+
transition: all 0.2s ease;
|
587 |
+
}
|
588 |
+
|
589 |
+
@media (min-width: 640px) {
|
590 |
+
.metric-link {
|
591 |
+
padding: 4px 8px;
|
592 |
+
font-size: 14px;
|
593 |
+
}
|
594 |
+
}
|
595 |
+
|
596 |
+
.metric-link:hover {
|
597 |
+
border-color: var(--border-medium);
|
598 |
+
color: var(--text-primary);
|
599 |
+
}
|
600 |
+
|
601 |
+
.github-icon {
|
602 |
+
width: 12px;
|
603 |
+
height: 12px;
|
604 |
+
color: var(--text-primary);
|
605 |
+
}
|
606 |
+
|
607 |
+
.comment-icon {
|
608 |
+
width: 14px;
|
609 |
+
height: 14px;
|
610 |
+
color: var(--text-primary);
|
611 |
+
}
|
612 |
+
|
613 |
+
/* Card Actions */
|
614 |
+
.card-actions {
|
615 |
+
padding: 0 24px 24px 24px;
|
616 |
+
display: flex;
|
617 |
+
gap: 8px;
|
618 |
+
}
|
619 |
+
|
620 |
+
.eval-button {
|
621 |
+
display: inline-flex;
|
622 |
+
align-items: center;
|
623 |
+
gap: 6px;
|
624 |
+
padding: 8px 16px;
|
625 |
+
border: 1px solid var(--border-medium);
|
626 |
+
border-radius: 8px;
|
627 |
+
background-color: var(--bg-secondary);
|
628 |
+
color: var(--text-secondary);
|
629 |
+
font-size: 12px;
|
630 |
+
font-weight: 500;
|
631 |
+
text-decoration: none;
|
632 |
+
cursor: pointer;
|
633 |
+
transition: all 0.2s ease;
|
634 |
+
}
|
635 |
+
|
636 |
+
.eval-button:hover {
|
637 |
+
background-color: var(--bg-tertiary);
|
638 |
+
color: var(--text-primary);
|
639 |
+
border-color: var(--border-medium);
|
640 |
+
}
|
641 |
+
|
642 |
+
.badge {
|
643 |
+
display: inline-flex;
|
644 |
+
align-items: center;
|
645 |
+
gap: 6px;
|
646 |
+
background-color: var(--bg-tertiary);
|
647 |
+
color: var(--accent-warning);
|
648 |
+
border: 1px solid var(--border-medium);
|
649 |
+
padding: 4px 8px;
|
650 |
+
border-radius: 999px;
|
651 |
+
font-size: 12px;
|
652 |
+
font-weight: 500;
|
653 |
+
}
|
654 |
+
|
655 |
+
/* Responsive Design */
|
656 |
+
@media (max-width: 1024px) {
|
657 |
+
.header-container {
|
658 |
+
grid-template-columns: 1fr;
|
659 |
+
gap: 24px;
|
660 |
+
}
|
661 |
+
|
662 |
+
.header-right {
|
663 |
+
align-items: flex-start;
|
664 |
+
}
|
665 |
+
|
666 |
+
.cards-grid {
|
667 |
+
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
668 |
+
}
|
669 |
+
}
|
670 |
+
|
671 |
+
@media (max-width: 768px) {
|
672 |
+
.nav-container {
|
673 |
+
padding: 0 16px;
|
674 |
+
}
|
675 |
+
|
676 |
+
.nav-center {
|
677 |
+
margin: 0 16px;
|
678 |
+
}
|
679 |
+
|
680 |
+
.header-container {
|
681 |
+
padding: 0 16px;
|
682 |
+
}
|
683 |
+
|
684 |
+
.content-container {
|
685 |
+
padding: 0 16px;
|
686 |
+
}
|
687 |
+
|
688 |
+
.cards-grid {
|
689 |
+
grid-template-columns: 1fr;
|
690 |
+
}
|
691 |
+
|
692 |
+
.paper-card {
|
693 |
+
padding: 20px;
|
694 |
+
}
|
695 |
+
|
696 |
+
.filter-buttons {
|
697 |
+
flex-wrap: wrap;
|
698 |
+
}
|
699 |
+
}
|
700 |
+
|
701 |
+
@media (max-width: 480px) {
|
702 |
+
.nav-left .logo span {
|
703 |
+
display: none;
|
704 |
+
}
|
705 |
+
|
706 |
+
.nav-center {
|
707 |
+
margin: 0 8px;
|
708 |
+
}
|
709 |
+
|
710 |
+
.paper-card {
|
711 |
+
padding: 16px;
|
712 |
+
}
|
713 |
+
|
714 |
+
.card-header {
|
715 |
+
flex-direction: column;
|
716 |
+
gap: 12px;
|
717 |
+
}
|
718 |
+
|
719 |
+
.upvote-section {
|
720 |
+
flex-direction: row;
|
721 |
+
justify-content: center;
|
722 |
+
}
|
723 |
+
}
|
724 |
+
|
725 |
+
/* Paper Evaluation Page Styles */
|
726 |
+
.back-link {
|
727 |
+
display: flex;
|
728 |
+
align-items: center;
|
729 |
+
gap: 8px;
|
730 |
+
color: var(--text-secondary);
|
731 |
+
text-decoration: none;
|
732 |
+
font-weight: 500;
|
733 |
+
transition: all 0.2s ease;
|
734 |
+
}
|
735 |
+
|
736 |
+
.back-link:hover {
|
737 |
+
color: var(--text-primary);
|
738 |
+
}
|
739 |
+
|
740 |
+
.page-title {
|
741 |
+
display: flex;
|
742 |
+
align-items: center;
|
743 |
+
gap: 12px;
|
744 |
+
font-size: 18px;
|
745 |
+
font-weight: 600;
|
746 |
+
color: var(--text-primary);
|
747 |
+
}
|
748 |
+
|
749 |
+
.page-title i {
|
750 |
+
color: var(--accent-primary);
|
751 |
+
}
|
752 |
+
|
753 |
+
.paper-main {
|
754 |
+
background-color: var(--bg-secondary);
|
755 |
+
min-height: calc(100vh - 64px);
|
756 |
+
padding: 32px 0;
|
757 |
+
}
|
758 |
+
|
759 |
+
.paper-container {
|
760 |
+
max-width: 1400px;
|
761 |
+
margin: 0 auto;
|
762 |
+
padding: 0 24px;
|
763 |
+
}
|
764 |
+
|
765 |
+
.paper-header {
|
766 |
+
background-color: var(--bg-primary);
|
767 |
+
border: 1px solid var(--border-light);
|
768 |
+
border-radius: 16px;
|
769 |
+
padding: 32px;
|
770 |
+
margin-bottom: 24px;
|
771 |
+
box-shadow: var(--shadow-sm);
|
772 |
+
}
|
773 |
+
|
774 |
+
.paper-meta h1 {
|
775 |
+
font-size: 28px;
|
776 |
+
font-weight: 800;
|
777 |
+
color: var(--text-primary);
|
778 |
+
margin-bottom: 24px;
|
779 |
+
}
|
780 |
+
|
781 |
+
.meta-grid {
|
782 |
+
display: grid;
|
783 |
+
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
784 |
+
gap: 16px;
|
785 |
+
}
|
786 |
+
|
787 |
+
.meta-item {
|
788 |
+
display: flex;
|
789 |
+
flex-direction: column;
|
790 |
+
gap: 4px;
|
791 |
+
}
|
792 |
+
|
793 |
+
.meta-label {
|
794 |
+
font-size: 12px;
|
795 |
+
font-weight: 600;
|
796 |
+
color: var(--text-muted);
|
797 |
+
text-transform: uppercase;
|
798 |
+
letter-spacing: 0.5px;
|
799 |
+
}
|
800 |
+
|
801 |
+
.meta-value {
|
802 |
+
font-size: 14px;
|
803 |
+
color: var(--text-primary);
|
804 |
+
font-weight: 500;
|
805 |
+
}
|
806 |
+
|
807 |
+
.meta-value a {
|
808 |
+
color: var(--accent-primary);
|
809 |
+
text-decoration: none;
|
810 |
+
}
|
811 |
+
|
812 |
+
.meta-value a:hover {
|
813 |
+
text-decoration: underline;
|
814 |
+
}
|
815 |
+
|
816 |
+
.content-layout {
|
817 |
+
display: grid;
|
818 |
+
grid-template-columns: 1fr 320px;
|
819 |
+
gap: 24px;
|
820 |
+
align-items: start;
|
821 |
+
}
|
822 |
+
|
823 |
+
.main-content {
|
824 |
+
background-color: var(--bg-primary);
|
825 |
+
border: 1px solid var(--border-light);
|
826 |
+
border-radius: 16px;
|
827 |
+
padding: 32px;
|
828 |
+
box-shadow: var(--shadow-sm);
|
829 |
+
}
|
830 |
+
|
831 |
+
.evaluation-content {
|
832 |
+
color: var(--text-primary);
|
833 |
+
}
|
834 |
+
|
835 |
+
.evaluation-content section {
|
836 |
+
margin-bottom: 32px;
|
837 |
+
}
|
838 |
+
|
839 |
+
.evaluation-content h2 {
|
840 |
+
font-size: 24px;
|
841 |
+
font-weight: 700;
|
842 |
+
color: var(--text-primary);
|
843 |
+
margin-bottom: 16px;
|
844 |
+
padding-bottom: 8px;
|
845 |
+
border-bottom: 2px solid var(--border-light);
|
846 |
+
}
|
847 |
+
|
848 |
+
.evaluation-content h3 {
|
849 |
+
font-size: 18px;
|
850 |
+
font-weight: 600;
|
851 |
+
color: var(--text-primary);
|
852 |
+
margin: 24px 0 12px 0;
|
853 |
+
}
|
854 |
+
|
855 |
+
.evaluation-content p {
|
856 |
+
font-size: 16px;
|
857 |
+
line-height: 1.6;
|
858 |
+
color: var(--text-secondary);
|
859 |
+
margin-bottom: 16px;
|
860 |
+
}
|
861 |
+
|
862 |
+
.evaluation-content ul {
|
863 |
+
list-style: none;
|
864 |
+
padding: 0;
|
865 |
+
margin: 16px 0;
|
866 |
+
}
|
867 |
+
|
868 |
+
.evaluation-content li {
|
869 |
+
padding: 8px 0;
|
870 |
+
border-bottom: 1px solid var(--border-light);
|
871 |
+
color: var(--text-secondary);
|
872 |
+
position: relative;
|
873 |
+
padding-left: 20px;
|
874 |
+
}
|
875 |
+
|
876 |
+
.evaluation-content li:before {
|
877 |
+
content: '•';
|
878 |
+
color: var(--accent-primary);
|
879 |
+
font-weight: bold;
|
880 |
+
position: absolute;
|
881 |
+
left: 0;
|
882 |
+
}
|
883 |
+
|
884 |
+
.dimension-card {
|
885 |
+
background-color: var(--bg-secondary);
|
886 |
+
border: 1px solid var(--border-light);
|
887 |
+
border-radius: 12px;
|
888 |
+
padding: 20px;
|
889 |
+
margin-bottom: 16px;
|
890 |
+
transition: all 0.2s ease;
|
891 |
+
}
|
892 |
+
|
893 |
+
.dimension-card:hover {
|
894 |
+
border-color: var(--border-medium);
|
895 |
+
box-shadow: var(--shadow-sm);
|
896 |
+
}
|
897 |
+
|
898 |
+
.dimension-header {
|
899 |
+
display: flex;
|
900 |
+
justify-content: space-between;
|
901 |
+
align-items: center;
|
902 |
+
margin-bottom: 12px;
|
903 |
+
}
|
904 |
+
|
905 |
+
.dimension-title {
|
906 |
+
font-size: 16px;
|
907 |
+
font-weight: 600;
|
908 |
+
color: var(--text-primary);
|
909 |
+
}
|
910 |
+
|
911 |
+
.dimension-score {
|
912 |
+
display: flex;
|
913 |
+
align-items: center;
|
914 |
+
gap: 8px;
|
915 |
+
}
|
916 |
+
|
917 |
+
.score-badge {
|
918 |
+
background-color: var(--accent-primary);
|
919 |
+
color: white;
|
920 |
+
padding: 4px 12px;
|
921 |
+
border-radius: 12px;
|
922 |
+
font-size: 14px;
|
923 |
+
font-weight: 600;
|
924 |
+
}
|
925 |
+
|
926 |
+
.dimension-meta {
|
927 |
+
display: flex;
|
928 |
+
gap: 16px;
|
929 |
+
margin-bottom: 12px;
|
930 |
+
font-size: 14px;
|
931 |
+
color: var(--text-muted);
|
932 |
+
}
|
933 |
+
|
934 |
+
.dimension-meta span {
|
935 |
+
display: flex;
|
936 |
+
align-items: center;
|
937 |
+
gap: 4px;
|
938 |
+
}
|
939 |
+
|
940 |
+
.dimension-analysis {
|
941 |
+
color: var(--text-secondary);
|
942 |
+
line-height: 1.5;
|
943 |
+
}
|
944 |
+
|
945 |
+
.sidebar {
|
946 |
+
position: sticky;
|
947 |
+
top: 100px;
|
948 |
+
}
|
949 |
+
|
950 |
+
.scorecard-panel {
|
951 |
+
background-color: var(--bg-primary);
|
952 |
+
border: 1px solid var(--border-light);
|
953 |
+
border-radius: 16px;
|
954 |
+
padding: 24px;
|
955 |
+
box-shadow: var(--shadow-sm);
|
956 |
+
}
|
957 |
+
|
958 |
+
.panel-header {
|
959 |
+
display: flex;
|
960 |
+
justify-content: space-between;
|
961 |
+
align-items: center;
|
962 |
+
margin-bottom: 20px;
|
963 |
+
}
|
964 |
+
|
965 |
+
.panel-header h2 {
|
966 |
+
font-size: 18px;
|
967 |
+
font-weight: 600;
|
968 |
+
color: var(--text-primary);
|
969 |
+
display: flex;
|
970 |
+
align-items: center;
|
971 |
+
gap: 8px;
|
972 |
+
}
|
973 |
+
|
974 |
+
.overall-score {
|
975 |
+
text-align: center;
|
976 |
+
}
|
977 |
+
|
978 |
+
.score-number {
|
979 |
+
display: block;
|
980 |
+
font-size: 24px;
|
981 |
+
font-weight: 800;
|
982 |
+
color: var(--accent-primary);
|
983 |
+
}
|
984 |
+
|
985 |
+
.score-label {
|
986 |
+
font-size: 12px;
|
987 |
+
color: var(--text-muted);
|
988 |
+
text-transform: uppercase;
|
989 |
+
letter-spacing: 0.5px;
|
990 |
+
}
|
991 |
+
|
992 |
+
.radar-container {
|
993 |
+
display: flex;
|
994 |
+
justify-content: center;
|
995 |
+
margin-bottom: 20px;
|
996 |
+
}
|
997 |
+
|
998 |
+
.radar-legend h3 {
|
999 |
+
font-size: 14px;
|
1000 |
+
font-weight: 600;
|
1001 |
+
color: var(--text-primary);
|
1002 |
+
margin-bottom: 12px;
|
1003 |
+
}
|
1004 |
+
|
1005 |
+
.legend-list {
|
1006 |
+
list-style: none;
|
1007 |
+
padding: 0;
|
1008 |
+
margin: 0;
|
1009 |
+
}
|
1010 |
+
|
1011 |
+
.legend-list li {
|
1012 |
+
display: flex;
|
1013 |
+
align-items: center;
|
1014 |
+
gap: 8px;
|
1015 |
+
padding: 6px 0;
|
1016 |
+
font-size: 12px;
|
1017 |
+
color: var(--text-secondary);
|
1018 |
+
}
|
1019 |
+
|
1020 |
+
.legend-color {
|
1021 |
+
width: 12px;
|
1022 |
+
height: 12px;
|
1023 |
+
border-radius: 50%;
|
1024 |
+
flex-shrink: 0;
|
1025 |
+
}
|
1026 |
+
|
1027 |
+
.legend-abbr {
|
1028 |
+
font-weight: 600;
|
1029 |
+
min-width: 40px;
|
1030 |
+
text-align: center;
|
1031 |
+
}
|
1032 |
+
|
1033 |
+
.legend-full {
|
1034 |
+
color: var(--text-secondary);
|
1035 |
+
font-size: 11px;
|
1036 |
+
}
|
1037 |
+
|
1038 |
+
/* Responsive Design for Paper Page */
|
1039 |
+
@media (max-width: 1024px) {
|
1040 |
+
.content-layout {
|
1041 |
+
grid-template-columns: 1fr;
|
1042 |
+
gap: 16px;
|
1043 |
+
}
|
1044 |
+
|
1045 |
+
.sidebar {
|
1046 |
+
position: static;
|
1047 |
+
}
|
1048 |
+
|
1049 |
+
.scorecard-panel {
|
1050 |
+
order: -1;
|
1051 |
+
}
|
1052 |
+
}
|
1053 |
+
|
1054 |
+
@media (max-width: 768px) {
|
1055 |
+
.paper-container {
|
1056 |
+
padding: 0 16px;
|
1057 |
+
}
|
1058 |
+
|
1059 |
+
.paper-header {
|
1060 |
+
padding: 24px;
|
1061 |
+
}
|
1062 |
+
|
1063 |
+
.main-content {
|
1064 |
+
padding: 24px;
|
1065 |
+
}
|
1066 |
+
|
1067 |
+
.meta-grid {
|
1068 |
+
grid-template-columns: 1fr;
|
1069 |
+
}
|
1070 |
+
}
|
1071 |
+
|
1072 |
+
@media (max-width: 480px) {
|
1073 |
+
.paper-header {
|
1074 |
+
padding: 20px;
|
1075 |
+
}
|
1076 |
+
|
1077 |
+
.main-content {
|
1078 |
+
padding: 20px;
|
1079 |
+
}
|
1080 |
+
|
1081 |
+
.scorecard-panel {
|
1082 |
+
padding: 20px;
|
1083 |
+
}
|
1084 |
+
|
1085 |
+
.dimension-header {
|
1086 |
+
flex-direction: column;
|
1087 |
+
align-items: flex-start;
|
1088 |
+
gap: 8px;
|
1089 |
+
}
|
1090 |
+
}
|
1091 |
+
|
1092 |
+
/* Enhanced Evaluation Page Styles */
|
1093 |
+
.evaluation-section {
|
1094 |
+
margin-bottom: 48px;
|
1095 |
+
background: var(--bg-primary);
|
1096 |
+
border-radius: 12px;
|
1097 |
+
border: 1px solid var(--border-light);
|
1098 |
+
overflow: hidden;
|
1099 |
+
box-shadow: var(--shadow-sm);
|
1100 |
+
}
|
1101 |
+
|
1102 |
+
.section-header {
|
1103 |
+
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
1104 |
+
color: white;
|
1105 |
+
padding: 24px 32px;
|
1106 |
+
border-bottom: 1px solid var(--border-light);
|
1107 |
+
}
|
1108 |
+
|
1109 |
+
.section-header h2 {
|
1110 |
+
margin: 0;
|
1111 |
+
font-size: 24px;
|
1112 |
+
font-weight: 600;
|
1113 |
+
display: flex;
|
1114 |
+
align-items: center;
|
1115 |
+
gap: 12px;
|
1116 |
+
}
|
1117 |
+
|
1118 |
+
.section-header h2 i {
|
1119 |
+
font-size: 20px;
|
1120 |
+
opacity: 0.9;
|
1121 |
+
}
|
1122 |
+
|
1123 |
+
.section-content {
|
1124 |
+
padding: 32px;
|
1125 |
+
}
|
1126 |
+
|
1127 |
+
/* Summary Card */
|
1128 |
+
.summary-card {
|
1129 |
+
background: var(--bg-secondary);
|
1130 |
+
border-radius: 8px;
|
1131 |
+
padding: 24px;
|
1132 |
+
border-left: 4px solid var(--accent-primary);
|
1133 |
+
}
|
1134 |
+
|
1135 |
+
.summary-text {
|
1136 |
+
font-size: 16px;
|
1137 |
+
line-height: 1.7;
|
1138 |
+
color: var(--text-primary);
|
1139 |
+
margin: 0;
|
1140 |
+
}
|
1141 |
+
|
1142 |
+
/* Dimensions Grid */
|
1143 |
+
.dimensions-grid {
|
1144 |
+
display: grid;
|
1145 |
+
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
|
1146 |
+
gap: 24px;
|
1147 |
+
}
|
1148 |
+
|
1149 |
+
.dimension-card {
|
1150 |
+
background: var(--bg-secondary);
|
1151 |
+
border-radius: 12px;
|
1152 |
+
padding: 24px;
|
1153 |
+
border: 1px solid var(--border-light);
|
1154 |
+
transition: all 0.2s ease;
|
1155 |
+
}
|
1156 |
+
|
1157 |
+
.dimension-card:hover {
|
1158 |
+
transform: translateY(-2px);
|
1159 |
+
box-shadow: var(--shadow-md);
|
1160 |
+
border-color: var(--accent-primary);
|
1161 |
+
}
|
1162 |
+
|
1163 |
+
.dimension-header {
|
1164 |
+
display: flex;
|
1165 |
+
justify-content: space-between;
|
1166 |
+
align-items: flex-start;
|
1167 |
+
margin-bottom: 16px;
|
1168 |
+
}
|
1169 |
+
|
1170 |
+
.dimension-title {
|
1171 |
+
font-size: 18px;
|
1172 |
+
font-weight: 600;
|
1173 |
+
color: var(--text-primary);
|
1174 |
+
display: flex;
|
1175 |
+
align-items: center;
|
1176 |
+
gap: 8px;
|
1177 |
+
flex: 1;
|
1178 |
+
}
|
1179 |
+
|
1180 |
+
.dimension-title i {
|
1181 |
+
color: var(--accent-primary);
|
1182 |
+
font-size: 16px;
|
1183 |
+
}
|
1184 |
+
|
1185 |
+
.dimension-score {
|
1186 |
+
display: flex;
|
1187 |
+
gap: 8px;
|
1188 |
+
flex-shrink: 0;
|
1189 |
+
}
|
1190 |
+
|
1191 |
+
.score-badge {
|
1192 |
+
background: var(--accent-primary);
|
1193 |
+
color: white;
|
1194 |
+
padding: 4px 12px;
|
1195 |
+
border-radius: 20px;
|
1196 |
+
font-size: 14px;
|
1197 |
+
font-weight: 600;
|
1198 |
+
min-width: 32px;
|
1199 |
+
text-align: center;
|
1200 |
+
}
|
1201 |
+
|
1202 |
+
.score-badge.probability {
|
1203 |
+
background: var(--accent-secondary);
|
1204 |
+
}
|
1205 |
+
|
1206 |
+
.dimension-meta {
|
1207 |
+
margin-bottom: 16px;
|
1208 |
+
display: flex;
|
1209 |
+
flex-wrap: wrap;
|
1210 |
+
gap: 12px;
|
1211 |
+
}
|
1212 |
+
|
1213 |
+
.meta-item {
|
1214 |
+
background: var(--bg-tertiary);
|
1215 |
+
padding: 6px 12px;
|
1216 |
+
border-radius: 6px;
|
1217 |
+
font-size: 12px;
|
1218 |
+
color: var(--text-secondary);
|
1219 |
+
display: flex;
|
1220 |
+
align-items: center;
|
1221 |
+
gap: 6px;
|
1222 |
+
}
|
1223 |
+
|
1224 |
+
.dimension-analysis {
|
1225 |
+
color: var(--text-secondary);
|
1226 |
+
line-height: 1.6;
|
1227 |
+
font-size: 14px;
|
1228 |
+
}
|
1229 |
+
|
1230 |
+
/* Recommendations Grid */
|
1231 |
+
.recommendations-grid {
|
1232 |
+
display: grid;
|
1233 |
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
1234 |
+
gap: 24px;
|
1235 |
+
}
|
1236 |
+
|
1237 |
+
.recommendation-card {
|
1238 |
+
background: var(--bg-secondary);
|
1239 |
+
border-radius: 12px;
|
1240 |
+
padding: 24px;
|
1241 |
+
border: 1px solid var(--border-light);
|
1242 |
+
transition: all 0.2s ease;
|
1243 |
+
}
|
1244 |
+
|
1245 |
+
.recommendation-card:hover {
|
1246 |
+
transform: translateY(-2px);
|
1247 |
+
box-shadow: var(--shadow-md);
|
1248 |
+
border-color: var(--accent-secondary);
|
1249 |
+
}
|
1250 |
+
|
1251 |
+
.recommendation-card h3 {
|
1252 |
+
color: var(--accent-secondary);
|
1253 |
+
font-size: 18px;
|
1254 |
+
font-weight: 600;
|
1255 |
+
margin: 0 0 16px 0;
|
1256 |
+
display: flex;
|
1257 |
+
align-items: center;
|
1258 |
+
gap: 8px;
|
1259 |
+
}
|
1260 |
+
|
1261 |
+
.recommendation-list {
|
1262 |
+
list-style: none;
|
1263 |
+
padding: 0;
|
1264 |
+
margin: 0;
|
1265 |
+
}
|
1266 |
+
|
1267 |
+
.recommendation-list li {
|
1268 |
+
padding: 8px 0;
|
1269 |
+
border-bottom: 1px solid var(--border-light);
|
1270 |
+
position: relative;
|
1271 |
+
padding-left: 20px;
|
1272 |
+
}
|
1273 |
+
|
1274 |
+
.recommendation-list li:before {
|
1275 |
+
content: "→";
|
1276 |
+
color: var(--accent-secondary);
|
1277 |
+
position: absolute;
|
1278 |
+
left: 0;
|
1279 |
+
font-weight: bold;
|
1280 |
+
}
|
1281 |
+
|
1282 |
+
.recommendation-list li:last-child {
|
1283 |
+
border-bottom: none;
|
1284 |
+
}
|
1285 |
+
|
1286 |
+
/* Limitations Card */
|
1287 |
+
.limitations-card {
|
1288 |
+
background: var(--bg-secondary);
|
1289 |
+
border-radius: 12px;
|
1290 |
+
padding: 24px;
|
1291 |
+
border: 1px solid var(--border-light);
|
1292 |
+
border-left: 4px solid var(--accent-warning);
|
1293 |
+
}
|
1294 |
+
|
1295 |
+
.limitations-list {
|
1296 |
+
list-style: none;
|
1297 |
+
padding: 0;
|
1298 |
+
margin: 0;
|
1299 |
+
}
|
1300 |
+
|
1301 |
+
.limitations-list li {
|
1302 |
+
padding: 12px 0;
|
1303 |
+
border-bottom: 1px solid var(--border-light);
|
1304 |
+
position: relative;
|
1305 |
+
padding-left: 24px;
|
1306 |
+
}
|
1307 |
+
|
1308 |
+
.limitations-list li:before {
|
1309 |
+
content: "⚠";
|
1310 |
+
color: var(--accent-warning);
|
1311 |
+
position: absolute;
|
1312 |
+
left: 0;
|
1313 |
+
font-weight: bold;
|
1314 |
+
}
|
1315 |
+
|
1316 |
+
.limitations-list li:last-child {
|
1317 |
+
border-bottom: none;
|
1318 |
+
}
|
1319 |
+
|
1320 |
+
/* No Data Styling */
|
1321 |
+
.no-data {
|
1322 |
+
color: var(--text-muted);
|
1323 |
+
font-style: italic;
|
1324 |
+
text-align: center;
|
1325 |
+
padding: 24px;
|
1326 |
+
background: var(--bg-tertiary);
|
1327 |
+
border-radius: 8px;
|
1328 |
+
border: 2px dashed var(--border-medium);
|
1329 |
+
}
|
1330 |
+
|
1331 |
+
/* Radar Chart Tooltip */
|
1332 |
+
.radar-tooltip {
|
1333 |
+
position: fixed;
|
1334 |
+
display: none;
|
1335 |
+
z-index: 1000;
|
1336 |
+
pointer-events: none;
|
1337 |
+
background: var(--bg-primary);
|
1338 |
+
border: 1px solid var(--border-light);
|
1339 |
+
border-radius: 8px;
|
1340 |
+
padding: 8px 12px;
|
1341 |
+
box-shadow: var(--shadow-lg);
|
1342 |
+
backdrop-filter: blur(8px);
|
1343 |
+
animation: tooltip-fade-in 0.2s ease-out;
|
1344 |
+
}
|
1345 |
+
|
1346 |
+
.tooltip-content {
|
1347 |
+
display: flex;
|
1348 |
+
flex-direction: column;
|
1349 |
+
gap: 2px;
|
1350 |
+
}
|
1351 |
+
|
1352 |
+
.tooltip-abbr {
|
1353 |
+
font-weight: 700;
|
1354 |
+
font-size: 14px;
|
1355 |
+
line-height: 1;
|
1356 |
+
}
|
1357 |
+
|
1358 |
+
.tooltip-full {
|
1359 |
+
font-size: 11px;
|
1360 |
+
color: var(--text-secondary);
|
1361 |
+
line-height: 1;
|
1362 |
+
}
|
1363 |
+
|
1364 |
+
@keyframes tooltip-fade-in {
|
1365 |
+
from {
|
1366 |
+
opacity: 0;
|
1367 |
+
transform: translateY(4px);
|
1368 |
+
}
|
1369 |
+
to {
|
1370 |
+
opacity: 1;
|
1371 |
+
transform: translateY(0);
|
1372 |
+
}
|
1373 |
+
}
|
1374 |
+
|
1375 |
+
|
requirements.txt
ADDED
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
anthropic>=0.38.0
|
2 |
+
langgraph>=0.2.43
|
3 |
+
pydantic>=2.7.0
|
4 |
+
python-dotenv>=1.0.1
|
5 |
+
rich>=13.7.1
|
6 |
+
fastapi>=0.112.0
|
7 |
+
uvicorn[standard]>=0.30.5
|
8 |
+
httpx>=0.27.0
|
9 |
+
beautifulsoup4>=4.12.3
|
10 |
+
lxml>=5.2.2
|
11 |
+
|
server.py
ADDED
@@ -0,0 +1,731 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import re
|
3 |
+
import glob
|
4 |
+
import json
|
5 |
+
import sqlite3
|
6 |
+
from datetime import date, datetime, timedelta
|
7 |
+
from typing import Any, Dict, List, Optional
|
8 |
+
from contextlib import contextmanager
|
9 |
+
|
10 |
+
from fastapi import FastAPI, HTTPException
|
11 |
+
from fastapi.middleware.cors import CORSMiddleware
|
12 |
+
from fastapi.responses import FileResponse
|
13 |
+
from fastapi.staticfiles import StaticFiles
|
14 |
+
from dotenv import load_dotenv
|
15 |
+
import httpx
|
16 |
+
from bs4 import BeautifulSoup
|
17 |
+
|
18 |
+
|
19 |
+
# Load environment variables
|
20 |
+
load_dotenv()
|
21 |
+
|
22 |
+
# Get API key from HF Spaces secrets
|
23 |
+
def get_anthropic_api_key() -> Optional[str]:
|
24 |
+
"""Get Anthropic API key, prioritize HF Spaces secrets"""
|
25 |
+
# First try to get from HF Spaces secrets
|
26 |
+
hf_secret = os.getenv("HF_SECRET_ANTHROPIC_API_KEY")
|
27 |
+
if hf_secret:
|
28 |
+
return hf_secret
|
29 |
+
|
30 |
+
# Then try to get from environment variables
|
31 |
+
env_key = os.getenv("ANTHROPIC_API_KEY")
|
32 |
+
if env_key:
|
33 |
+
return env_key
|
34 |
+
|
35 |
+
return None
|
36 |
+
|
37 |
+
|
38 |
+
def get_project_root() -> str:
|
39 |
+
return os.path.dirname(os.path.abspath(__file__))
|
40 |
+
|
41 |
+
|
42 |
+
PROJECT_ROOT = get_project_root()
|
43 |
+
DEFAULT_WORKDIR = os.getenv("WORKDIR", os.path.join(PROJECT_ROOT, "workdir"))
|
44 |
+
DB_PATH = os.path.join(PROJECT_ROOT, "papers_cache.db")
|
45 |
+
|
46 |
+
# Database management
|
47 |
+
class PapersDatabase:
|
48 |
+
def __init__(self, db_path: str):
|
49 |
+
self.db_path = db_path
|
50 |
+
self.init_database()
|
51 |
+
|
52 |
+
def init_database(self):
|
53 |
+
"""Initialize the database with required tables"""
|
54 |
+
with self.get_connection() as conn:
|
55 |
+
cursor = conn.cursor()
|
56 |
+
|
57 |
+
# Create papers cache table
|
58 |
+
cursor.execute('''
|
59 |
+
CREATE TABLE IF NOT EXISTS papers_cache (
|
60 |
+
date_str TEXT PRIMARY KEY,
|
61 |
+
html_content TEXT NOT NULL,
|
62 |
+
parsed_cards TEXT NOT NULL,
|
63 |
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
64 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
65 |
+
)
|
66 |
+
''')
|
67 |
+
|
68 |
+
# Create latest_date table to track the most recent available date
|
69 |
+
cursor.execute('''
|
70 |
+
CREATE TABLE IF NOT EXISTS latest_date (
|
71 |
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
72 |
+
date_str TEXT NOT NULL,
|
73 |
+
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
74 |
+
)
|
75 |
+
''')
|
76 |
+
|
77 |
+
# Insert default latest_date record if it doesn't exist
|
78 |
+
cursor.execute('''
|
79 |
+
INSERT OR IGNORE INTO latest_date (id, date_str)
|
80 |
+
VALUES (1, ?)
|
81 |
+
''', (date.today().isoformat(),))
|
82 |
+
|
83 |
+
conn.commit()
|
84 |
+
|
85 |
+
@contextmanager
|
86 |
+
def get_connection(self):
|
87 |
+
"""Context manager for database connections"""
|
88 |
+
conn = sqlite3.connect(self.db_path)
|
89 |
+
conn.row_factory = sqlite3.Row # Enable dict-like access
|
90 |
+
try:
|
91 |
+
yield conn
|
92 |
+
finally:
|
93 |
+
conn.close()
|
94 |
+
|
95 |
+
def get_cached_papers(self, date_str: str) -> Optional[Dict[str, Any]]:
|
96 |
+
"""Get cached papers for a specific date"""
|
97 |
+
with self.get_connection() as conn:
|
98 |
+
cursor = conn.cursor()
|
99 |
+
cursor.execute('''
|
100 |
+
SELECT parsed_cards, created_at
|
101 |
+
FROM papers_cache
|
102 |
+
WHERE date_str = ?
|
103 |
+
''', (date_str,))
|
104 |
+
|
105 |
+
row = cursor.fetchone()
|
106 |
+
if row:
|
107 |
+
return {
|
108 |
+
'cards': json.loads(row['parsed_cards']),
|
109 |
+
'cached_at': row['created_at']
|
110 |
+
}
|
111 |
+
return None
|
112 |
+
|
113 |
+
def cache_papers(self, date_str: str, html_content: str, parsed_cards: List[Dict[str, Any]]):
|
114 |
+
"""Cache papers for a specific date"""
|
115 |
+
with self.get_connection() as conn:
|
116 |
+
cursor = conn.cursor()
|
117 |
+
cursor.execute('''
|
118 |
+
INSERT OR REPLACE INTO papers_cache
|
119 |
+
(date_str, html_content, parsed_cards, updated_at)
|
120 |
+
VALUES (?, ?, ?, CURRENT_TIMESTAMP)
|
121 |
+
''', (date_str, html_content, json.dumps(parsed_cards)))
|
122 |
+
conn.commit()
|
123 |
+
|
124 |
+
def get_latest_cached_date(self) -> Optional[str]:
|
125 |
+
"""Get the latest cached date"""
|
126 |
+
with self.get_connection() as conn:
|
127 |
+
cursor = conn.cursor()
|
128 |
+
cursor.execute('SELECT date_str FROM latest_date WHERE id = 1')
|
129 |
+
row = cursor.fetchone()
|
130 |
+
return row['date_str'] if row else None
|
131 |
+
|
132 |
+
def update_latest_date(self, date_str: str):
|
133 |
+
"""Update the latest available date"""
|
134 |
+
with self.get_connection() as conn:
|
135 |
+
cursor = conn.cursor()
|
136 |
+
cursor.execute('''
|
137 |
+
UPDATE latest_date
|
138 |
+
SET date_str = ?, updated_at = CURRENT_TIMESTAMP
|
139 |
+
WHERE id = 1
|
140 |
+
''', (date_str,))
|
141 |
+
conn.commit()
|
142 |
+
|
143 |
+
def is_cache_fresh(self, date_str: str, max_age_hours: int = 24) -> bool:
|
144 |
+
"""Check if cache is fresh (within max_age_hours)"""
|
145 |
+
with self.get_connection() as conn:
|
146 |
+
cursor = conn.cursor()
|
147 |
+
cursor.execute('''
|
148 |
+
SELECT updated_at
|
149 |
+
FROM papers_cache
|
150 |
+
WHERE date_str = ?
|
151 |
+
''', (date_str,))
|
152 |
+
|
153 |
+
row = cursor.fetchone()
|
154 |
+
if not row:
|
155 |
+
return False
|
156 |
+
|
157 |
+
cached_time = datetime.fromisoformat(row['updated_at'].replace('Z', '+00:00'))
|
158 |
+
age = datetime.now(cached_time.tzinfo) - cached_time
|
159 |
+
return age.total_seconds() < max_age_hours * 3600
|
160 |
+
|
161 |
+
def cleanup_old_cache(self, days_to_keep: int = 7):
|
162 |
+
"""Clean up old cache entries"""
|
163 |
+
cutoff_date = (datetime.now() - timedelta(days=days_to_keep)).isoformat()
|
164 |
+
with self.get_connection() as conn:
|
165 |
+
cursor = conn.cursor()
|
166 |
+
cursor.execute('''
|
167 |
+
DELETE FROM papers_cache
|
168 |
+
WHERE updated_at < ?
|
169 |
+
''', (cutoff_date,))
|
170 |
+
conn.commit()
|
171 |
+
|
172 |
+
|
173 |
+
# Initialize database
|
174 |
+
db = PapersDatabase(DB_PATH)
|
175 |
+
|
176 |
+
|
177 |
+
app = FastAPI(title="PaperIndex Web")
|
178 |
+
|
179 |
+
# Local development: allow same-origin and localhost
|
180 |
+
app.add_middleware(
|
181 |
+
CORSMiddleware,
|
182 |
+
allow_origins=["*"],
|
183 |
+
allow_credentials=True,
|
184 |
+
allow_methods=["*"],
|
185 |
+
allow_headers=["*"],
|
186 |
+
)
|
187 |
+
|
188 |
+
|
189 |
+
# --- Utility functions ---
|
190 |
+
|
191 |
+
def ensure_workdir() -> str:
|
192 |
+
os.makedirs(DEFAULT_WORKDIR, exist_ok=True)
|
193 |
+
return DEFAULT_WORKDIR
|
194 |
+
|
195 |
+
|
196 |
+
def extract_arxiv_id(url: str) -> Optional[str]:
|
197 |
+
if not url:
|
198 |
+
return None
|
199 |
+
# Matches /abs/2508.05629, /pdf/2508.05629.pdf
|
200 |
+
m = re.search(r"arxiv\.org/(abs|pdf)/([0-9]{4}\.\d{4,5})(?:\.pdf)?", url)
|
201 |
+
if m:
|
202 |
+
return m.group(2)
|
203 |
+
return None
|
204 |
+
|
205 |
+
def extract_json_data(html: str) -> Dict[str, Any]:
|
206 |
+
"""Extract JSON data from the HTML page to get GitHub stars and other metadata."""
|
207 |
+
try:
|
208 |
+
soup = BeautifulSoup(html, "lxml")
|
209 |
+
|
210 |
+
# Look for GitHub stars in the HTML structure
|
211 |
+
# Based on the user's description, GitHub stars are displayed with SVG icons
|
212 |
+
# Look for SVG elements that might represent GitHub stars
|
213 |
+
svg_elements = soup.find_all("svg")
|
214 |
+
|
215 |
+
github_stars_map = {}
|
216 |
+
|
217 |
+
for svg in svg_elements:
|
218 |
+
# Look for GitHub-related SVG (usually has specific viewBox or path)
|
219 |
+
svg_html = str(svg)
|
220 |
+
if "github" in svg_html.lower() or "256 250" in svg_html: # GitHub icon viewBox
|
221 |
+
# Look for the star count near this SVG
|
222 |
+
parent = svg.parent
|
223 |
+
if parent:
|
224 |
+
# Look for numbers that might be star counts
|
225 |
+
text_content = parent.get_text()
|
226 |
+
numbers = re.findall(r'\b(\d+)\b', text_content)
|
227 |
+
if numbers:
|
228 |
+
# The number near a GitHub SVG is likely the star count
|
229 |
+
star_count = int(numbers[0])
|
230 |
+
# Try to find the paper title or ID to associate with
|
231 |
+
# Look for the closest article or card container
|
232 |
+
article = svg.find_parent("article")
|
233 |
+
if article:
|
234 |
+
title_elem = article.find("h3")
|
235 |
+
if title_elem:
|
236 |
+
paper_title = title_elem.get_text(strip=True)
|
237 |
+
github_stars_map[paper_title] = star_count
|
238 |
+
|
239 |
+
# Also look for any elements with GitHub-related text
|
240 |
+
github_text_elements = soup.find_all(string=lambda text: text and "github" in text.lower())
|
241 |
+
for text_elem in github_text_elements:
|
242 |
+
parent = text_elem.parent
|
243 |
+
if parent:
|
244 |
+
text_content = parent.get_text()
|
245 |
+
numbers = re.findall(r'\b(\d+)\b', text_content)
|
246 |
+
if numbers:
|
247 |
+
star_count = int(numbers[0])
|
248 |
+
# Try to find the paper title
|
249 |
+
article = parent.find_parent("article")
|
250 |
+
if article:
|
251 |
+
title_elem = article.find("h3")
|
252 |
+
if title_elem:
|
253 |
+
paper_title = title_elem.get_text(strip=True)
|
254 |
+
if paper_title not in github_stars_map:
|
255 |
+
github_stars_map[paper_title] = star_count
|
256 |
+
|
257 |
+
return {"github_stars_map": github_stars_map}
|
258 |
+
|
259 |
+
except Exception as e:
|
260 |
+
print(f"Error extracting JSON data: {e}")
|
261 |
+
|
262 |
+
return {"github_stars_map": {}}
|
263 |
+
|
264 |
+
|
265 |
+
def find_eval_file_for_id(arxiv_id: str) -> Optional[str]:
|
266 |
+
workdir = ensure_workdir()
|
267 |
+
# Look for any json containing the id substring
|
268 |
+
candidates = glob.glob(os.path.join(workdir, f"**/*{arxiv_id}*.json"), recursive=True)
|
269 |
+
return candidates[0] if candidates else None
|
270 |
+
|
271 |
+
|
272 |
+
async def fetch_daily_html(target_date: str) -> tuple[str, str]:
|
273 |
+
"""Fetch daily papers HTML, with fallback to find the latest available date"""
|
274 |
+
async with httpx.AsyncClient(timeout=20, follow_redirects=False) as client:
|
275 |
+
# First try the requested date
|
276 |
+
url = f"https://huggingface.co/papers/date/{target_date}"
|
277 |
+
try:
|
278 |
+
r = await client.get(url)
|
279 |
+
|
280 |
+
# Check if we got redirected
|
281 |
+
if r.status_code in [301, 302, 303, 307, 308]:
|
282 |
+
# We got redirected, extract the actual date from the redirect location
|
283 |
+
location = r.headers.get('location', '')
|
284 |
+
print(f"Got redirect to: {location}")
|
285 |
+
|
286 |
+
# Extract date from redirect URL (e.g., /papers/date/2025-08-08)
|
287 |
+
import re
|
288 |
+
date_match = re.search(r'/papers/date/(\d{4}-\d{2}-\d{2})', location)
|
289 |
+
if date_match:
|
290 |
+
actual_date = date_match.group(1)
|
291 |
+
print(f"Redirected from {target_date} to {actual_date}")
|
292 |
+
|
293 |
+
# Fetch the actual page
|
294 |
+
actual_url = f"https://huggingface.co{location}"
|
295 |
+
r = await client.get(actual_url)
|
296 |
+
if r.status_code == 200:
|
297 |
+
return actual_date, r.text
|
298 |
+
else:
|
299 |
+
raise Exception(f"Failed to fetch redirected page: {r.status_code}")
|
300 |
+
else:
|
301 |
+
# Couldn't extract date from redirect, use fallback
|
302 |
+
raise Exception("Could not extract date from redirect")
|
303 |
+
|
304 |
+
elif r.status_code == 200:
|
305 |
+
# Direct success, check if the page actually contains the requested date
|
306 |
+
if target_date in r.text or "Daily Papers" in r.text:
|
307 |
+
return target_date, r.text
|
308 |
+
else:
|
309 |
+
# Page exists but doesn't contain expected content
|
310 |
+
raise Exception("Page exists but doesn't contain expected content")
|
311 |
+
else:
|
312 |
+
# Other error status
|
313 |
+
raise Exception(f"Status code {r.status_code}")
|
314 |
+
|
315 |
+
except Exception as e:
|
316 |
+
print(f"Failed to fetch {target_date}: {e}")
|
317 |
+
# If the requested date fails, try to find the latest available date
|
318 |
+
actual_date, html = await find_latest_available_date(client)
|
319 |
+
return actual_date, html
|
320 |
+
|
321 |
+
async def find_latest_available_date(client: httpx.AsyncClient) -> tuple[str, str]:
|
322 |
+
"""Find the latest available date by checking recent dates"""
|
323 |
+
|
324 |
+
# Start from today and go backwards up to 30 days
|
325 |
+
today = datetime.now()
|
326 |
+
for i in range(30):
|
327 |
+
check_date = today - timedelta(days=i)
|
328 |
+
date_str = check_date.strftime("%Y-%m-%d")
|
329 |
+
url = f"https://huggingface.co/papers/date/{date_str}"
|
330 |
+
|
331 |
+
try:
|
332 |
+
r = await client.get(url)
|
333 |
+
if r.status_code == 200:
|
334 |
+
# Check if the page actually has content (not just a 404 or empty page)
|
335 |
+
if "Daily Papers" in r.text and len(r.text) > 1000:
|
336 |
+
print(f"Found latest available date: {date_str}")
|
337 |
+
return date_str, r.text
|
338 |
+
except Exception:
|
339 |
+
continue
|
340 |
+
|
341 |
+
# If no date found, return a default page or raise an error
|
342 |
+
raise Exception("No available daily papers found in the last 30 days")
|
343 |
+
|
344 |
+
|
345 |
+
def parse_daily_cards(html: str) -> List[Dict[str, Any]]:
|
346 |
+
soup = BeautifulSoup(html, "lxml")
|
347 |
+
|
348 |
+
# First, extract JSON data from the page to get GitHub stars
|
349 |
+
json_data = extract_json_data(html)
|
350 |
+
|
351 |
+
# Find all article elements that contain paper cards
|
352 |
+
cards: List[Dict[str, Any]] = []
|
353 |
+
|
354 |
+
# Look for article elements with the specific class structure from Hugging Face
|
355 |
+
for article in soup.select("article.relative.flex.flex-col.overflow-hidden.rounded-xl.border"):
|
356 |
+
try:
|
357 |
+
card_data = {}
|
358 |
+
|
359 |
+
# Extract title and link
|
360 |
+
title_link = article.select_one("h3 a")
|
361 |
+
if title_link:
|
362 |
+
card_data["title"] = title_link.get_text(strip=True)
|
363 |
+
href = title_link.get("href")
|
364 |
+
if href:
|
365 |
+
if href.startswith("http"):
|
366 |
+
card_data["huggingface_url"] = href
|
367 |
+
else:
|
368 |
+
card_data["huggingface_url"] = f"https://huggingface.co{href}"
|
369 |
+
|
370 |
+
# Extract upvote count
|
371 |
+
upvote_div = article.select_one("div.shadow-alternate div.leading-none")
|
372 |
+
if upvote_div:
|
373 |
+
upvote_text = upvote_div.get_text(strip=True)
|
374 |
+
try:
|
375 |
+
card_data["upvotes"] = int(upvote_text)
|
376 |
+
except ValueError:
|
377 |
+
card_data["upvotes"] = 0
|
378 |
+
|
379 |
+
# Extract author count - look for the author count text
|
380 |
+
author_count_div = article.select_one("div.flex.truncate.text-sm")
|
381 |
+
if author_count_div:
|
382 |
+
author_text = author_count_div.get_text(strip=True)
|
383 |
+
# Extract number from "· 10 authors"
|
384 |
+
author_match = re.search(r'(\d+)\s*authors?', author_text)
|
385 |
+
if author_match:
|
386 |
+
card_data["author_count"] = int(author_match.group(1))
|
387 |
+
else:
|
388 |
+
card_data["author_count"] = 0
|
389 |
+
|
390 |
+
# Extract GitHub stars from JSON data in the page
|
391 |
+
# This will be handled later when we parse the JSON data
|
392 |
+
card_data["github_stars"] = 0 # Default value
|
393 |
+
|
394 |
+
# Extract comments count - look for comment icon and number
|
395 |
+
comment_links = article.select("a[href*='#community']")
|
396 |
+
for comment_link in comment_links:
|
397 |
+
comment_text = comment_link.get_text(strip=True)
|
398 |
+
try:
|
399 |
+
card_data["comments"] = int(comment_text)
|
400 |
+
break
|
401 |
+
except ValueError:
|
402 |
+
continue
|
403 |
+
|
404 |
+
# Extract submitter information
|
405 |
+
submitted_div = article.select_one("div.shadow-xs")
|
406 |
+
if submitted_div:
|
407 |
+
submitter_text = submitted_div.get_text(strip=True)
|
408 |
+
# Extract submitter name from "Submitted byLiang0223" (no space)
|
409 |
+
submitter_match = re.search(r'Submitted by(\S+)', submitter_text)
|
410 |
+
if submitter_match:
|
411 |
+
card_data["submitter"] = submitter_match.group(1)
|
412 |
+
|
413 |
+
# Extract arXiv ID from the URL
|
414 |
+
if card_data.get("huggingface_url"):
|
415 |
+
arxiv_id = extract_arxiv_id(card_data["huggingface_url"])
|
416 |
+
if arxiv_id:
|
417 |
+
card_data["arxiv_id"] = arxiv_id
|
418 |
+
|
419 |
+
# Try to get GitHub stars from the extracted data
|
420 |
+
# Look for GitHub stars by matching paper title
|
421 |
+
paper_title = card_data.get("title", "")
|
422 |
+
if paper_title in json_data.get("github_stars_map", {}):
|
423 |
+
card_data["github_stars"] = json_data["github_stars_map"][paper_title]
|
424 |
+
|
425 |
+
# Only add cards that have at least a title
|
426 |
+
if card_data.get("title"):
|
427 |
+
cards.append(card_data)
|
428 |
+
|
429 |
+
except Exception as e:
|
430 |
+
print(f"Error parsing card: {e}")
|
431 |
+
continue
|
432 |
+
|
433 |
+
# If the above method didn't work, fall back to the old method
|
434 |
+
if not cards:
|
435 |
+
print("Falling back to old parsing method")
|
436 |
+
for h3 in soup.select("h3"):
|
437 |
+
# Title and Hugging Face paper link (if present)
|
438 |
+
a = h3.find("a")
|
439 |
+
title = h3.get_text(strip=True)
|
440 |
+
hf_link = None
|
441 |
+
if a and a.get("href"):
|
442 |
+
href = a.get("href")
|
443 |
+
# Absolute URL to huggingface
|
444 |
+
if href.startswith("http"):
|
445 |
+
hf_link = href
|
446 |
+
else:
|
447 |
+
hf_link = f"https://huggingface.co{href}"
|
448 |
+
|
449 |
+
# Try to capture sibling info (authors, votes, etc.) as a small snippet
|
450 |
+
meta_text = None
|
451 |
+
parent = h3.parent
|
452 |
+
if parent:
|
453 |
+
# Join immediate text content following h3
|
454 |
+
collected: List[str] = []
|
455 |
+
for sib in parent.find_all(text=True, recursive=False):
|
456 |
+
t = (sib or "").strip()
|
457 |
+
if t:
|
458 |
+
collected.append(t)
|
459 |
+
if collected:
|
460 |
+
meta_text = " ".join(collected)
|
461 |
+
|
462 |
+
# Try to discover any arXiv link inside nearby anchors
|
463 |
+
arxiv_id: Optional[str] = None
|
464 |
+
container = parent if parent else h3
|
465 |
+
for link in container.find_all("a", href=True):
|
466 |
+
possible = extract_arxiv_id(link["href"])
|
467 |
+
if possible:
|
468 |
+
arxiv_id = possible
|
469 |
+
break
|
470 |
+
|
471 |
+
cards.append(
|
472 |
+
{
|
473 |
+
"title": title,
|
474 |
+
"huggingface_url": hf_link,
|
475 |
+
"meta": meta_text,
|
476 |
+
"arxiv_id": arxiv_id,
|
477 |
+
}
|
478 |
+
)
|
479 |
+
|
480 |
+
# Deduplicate by title
|
481 |
+
seen = set()
|
482 |
+
unique_cards: List[Dict[str, Any]] = []
|
483 |
+
for c in cards:
|
484 |
+
key = c.get("title") or ""
|
485 |
+
if key and key not in seen:
|
486 |
+
seen.add(key)
|
487 |
+
unique_cards.append(c)
|
488 |
+
|
489 |
+
print(f"Parsed {len(unique_cards)} cards")
|
490 |
+
return unique_cards
|
491 |
+
|
492 |
+
|
493 |
+
# --- API Routes ---
|
494 |
+
|
495 |
+
@app.get("/api/daily")
|
496 |
+
async def get_daily(date_str: Optional[str] = None) -> Dict[str, Any]:
|
497 |
+
target_date = date_str or date.today().isoformat()
|
498 |
+
|
499 |
+
# First, check if we have fresh cache for the requested date
|
500 |
+
cached_data = db.get_cached_papers(target_date)
|
501 |
+
if cached_data and db.is_cache_fresh(target_date):
|
502 |
+
print(f"Using cached data for {target_date}")
|
503 |
+
return {
|
504 |
+
"date": target_date,
|
505 |
+
"requested_date": target_date,
|
506 |
+
"cards": cached_data['cards'],
|
507 |
+
"fallback_used": False,
|
508 |
+
"cached": True,
|
509 |
+
"cached_at": cached_data['cached_at']
|
510 |
+
}
|
511 |
+
|
512 |
+
# If no cache or stale cache, try to fetch fresh data
|
513 |
+
try:
|
514 |
+
actual_date, html = await fetch_daily_html(target_date)
|
515 |
+
print(f"Fetched fresh data for {actual_date} (requested {target_date})")
|
516 |
+
|
517 |
+
# Check if we got redirected to a different date, and if that date has fresh cache
|
518 |
+
if actual_date != target_date:
|
519 |
+
cached_data = db.get_cached_papers(actual_date)
|
520 |
+
if cached_data and db.is_cache_fresh(actual_date):
|
521 |
+
print(f"Using cached data for redirected date {actual_date}")
|
522 |
+
return {
|
523 |
+
"date": actual_date,
|
524 |
+
"requested_date": target_date,
|
525 |
+
"cards": cached_data['cards'],
|
526 |
+
"fallback_used": True,
|
527 |
+
"cached": True,
|
528 |
+
"cached_at": cached_data['cached_at']
|
529 |
+
}
|
530 |
+
|
531 |
+
except Exception as e:
|
532 |
+
print(f"Failed to fetch {target_date}, trying to find latest available date")
|
533 |
+
# If the requested date fails, try to find the latest available date
|
534 |
+
async with httpx.AsyncClient(timeout=20) as client:
|
535 |
+
try:
|
536 |
+
actual_date, html = await find_latest_available_date(client)
|
537 |
+
print(f"Using fallback date: {actual_date}")
|
538 |
+
|
539 |
+
# Check if the fallback date has fresh cache
|
540 |
+
cached_data = db.get_cached_papers(actual_date)
|
541 |
+
if cached_data and db.is_cache_fresh(actual_date):
|
542 |
+
print(f"Using cached data for fallback date {actual_date}")
|
543 |
+
return {
|
544 |
+
"date": actual_date,
|
545 |
+
"requested_date": target_date,
|
546 |
+
"cards": cached_data['cards'],
|
547 |
+
"fallback_used": True,
|
548 |
+
"cached": True,
|
549 |
+
"cached_at": cached_data['cached_at']
|
550 |
+
}
|
551 |
+
|
552 |
+
except Exception as fallback_error:
|
553 |
+
print(f"Fallback also failed: {fallback_error}")
|
554 |
+
# If everything fails, return cached data if available
|
555 |
+
cached_data = db.get_cached_papers(target_date)
|
556 |
+
if cached_data:
|
557 |
+
return {
|
558 |
+
"date": target_date,
|
559 |
+
"requested_date": target_date,
|
560 |
+
"cards": cached_data['cards'],
|
561 |
+
"fallback_used": False,
|
562 |
+
"cached": True,
|
563 |
+
"cached_at": cached_data['cached_at']
|
564 |
+
}
|
565 |
+
# If no cache available, return error
|
566 |
+
raise HTTPException(status_code=503, detail="Unable to fetch papers and no cache available")
|
567 |
+
|
568 |
+
# Parse the HTML and process cards
|
569 |
+
cards = parse_daily_cards(html)
|
570 |
+
|
571 |
+
# Attempt to resolve missing arXiv ids by scraping the HF paper page
|
572 |
+
async with httpx.AsyncClient(timeout=15) as client:
|
573 |
+
async def resolve_card(card: Dict[str, Any]) -> None:
|
574 |
+
if card.get("arxiv_id") or not card.get("huggingface_url"):
|
575 |
+
return
|
576 |
+
try:
|
577 |
+
r = await client.get(card["huggingface_url"])
|
578 |
+
if r.status_code == 200:
|
579 |
+
soup = BeautifulSoup(r.text, "lxml")
|
580 |
+
for a in soup.select("a[href]"):
|
581 |
+
aid = extract_arxiv_id(a.get("href") or "")
|
582 |
+
if aid:
|
583 |
+
card["arxiv_id"] = aid
|
584 |
+
break
|
585 |
+
except Exception:
|
586 |
+
pass
|
587 |
+
|
588 |
+
# Resolve sequentially for compatibility/simplicity
|
589 |
+
for c in cards:
|
590 |
+
await resolve_card(c)
|
591 |
+
|
592 |
+
# Fallback HF link to the daily page when missing
|
593 |
+
for c in cards:
|
594 |
+
if not c.get("huggingface_url"):
|
595 |
+
c["huggingface_url"] = f"https://huggingface.co/papers/date/{actual_date}"
|
596 |
+
|
597 |
+
# Attach has_eval flag
|
598 |
+
for c in cards:
|
599 |
+
arxiv_id = c.get("arxiv_id")
|
600 |
+
c["has_eval"] = bool(arxiv_id and find_eval_file_for_id(arxiv_id))
|
601 |
+
|
602 |
+
# Cache the results
|
603 |
+
db.cache_papers(actual_date, html, cards)
|
604 |
+
db.update_latest_date(actual_date)
|
605 |
+
|
606 |
+
# Clean up old cache entries (run occasionally)
|
607 |
+
if datetime.now().hour == 2: # Run cleanup at 2 AM
|
608 |
+
db.cleanup_old_cache()
|
609 |
+
|
610 |
+
return {
|
611 |
+
"date": actual_date,
|
612 |
+
"requested_date": target_date,
|
613 |
+
"cards": cards,
|
614 |
+
"fallback_used": actual_date != target_date,
|
615 |
+
"cached": False
|
616 |
+
}
|
617 |
+
|
618 |
+
|
619 |
+
@app.get("/api/evals")
|
620 |
+
def list_evals() -> Dict[str, Any]:
|
621 |
+
workdir = ensure_workdir()
|
622 |
+
files = sorted(glob.glob(os.path.join(workdir, "**/*.json"), recursive=True))
|
623 |
+
items: List[Dict[str, Any]] = []
|
624 |
+
for f in files:
|
625 |
+
base = os.path.basename(f)
|
626 |
+
# Extract arxiv-like id from filename if present
|
627 |
+
m = re.search(r"([0-9]{4}\.\d{4,5})", base)
|
628 |
+
arxiv_id = m.group(1) if m else None
|
629 |
+
items.append({"file": f, "name": base, "arxiv_id": arxiv_id})
|
630 |
+
return {"count": len(items), "items": items}
|
631 |
+
|
632 |
+
|
633 |
+
@app.get("/api/has-eval/{paper_id}")
|
634 |
+
def has_eval(paper_id: str) -> Dict[str, bool]:
|
635 |
+
exists = find_eval_file_for_id(paper_id) is not None
|
636 |
+
return {"exists": exists}
|
637 |
+
|
638 |
+
|
639 |
+
@app.get("/api/eval/{paper_id}")
|
640 |
+
def get_eval(paper_id: str) -> Any:
|
641 |
+
path = find_eval_file_for_id(paper_id)
|
642 |
+
if not path:
|
643 |
+
raise HTTPException(status_code=404, detail="Evaluation not found")
|
644 |
+
return FileResponse(path, media_type="application/json")
|
645 |
+
|
646 |
+
|
647 |
+
@app.get("/api/cache/status")
|
648 |
+
def get_cache_status() -> Dict[str, Any]:
|
649 |
+
"""Get cache status and statistics"""
|
650 |
+
with db.get_connection() as conn:
|
651 |
+
cursor = conn.cursor()
|
652 |
+
|
653 |
+
# Get total cached dates
|
654 |
+
cursor.execute('SELECT COUNT(*) as count FROM papers_cache')
|
655 |
+
total_cached = cursor.fetchone()['count']
|
656 |
+
|
657 |
+
# Get latest cached date
|
658 |
+
cursor.execute('SELECT date_str, updated_at FROM latest_date WHERE id = 1')
|
659 |
+
latest_info = cursor.fetchone()
|
660 |
+
|
661 |
+
# Get cache age distribution
|
662 |
+
cursor.execute('''
|
663 |
+
SELECT
|
664 |
+
CASE
|
665 |
+
WHEN updated_at > datetime('now', '-1 hour') THEN '1 hour'
|
666 |
+
WHEN updated_at > datetime('now', '-24 hours') THEN '24 hours'
|
667 |
+
WHEN updated_at > datetime('now', '-7 days') THEN '7 days'
|
668 |
+
ELSE 'older'
|
669 |
+
END as age_group,
|
670 |
+
COUNT(*) as count
|
671 |
+
FROM papers_cache
|
672 |
+
GROUP BY age_group
|
673 |
+
''')
|
674 |
+
age_distribution = {row['age_group']: row['count'] for row in cursor.fetchall()}
|
675 |
+
|
676 |
+
return {
|
677 |
+
"total_cached_dates": total_cached,
|
678 |
+
"latest_cached_date": latest_info['date_str'] if latest_info else None,
|
679 |
+
"latest_updated": latest_info['updated_at'] if latest_info else None,
|
680 |
+
"age_distribution": age_distribution
|
681 |
+
}
|
682 |
+
|
683 |
+
|
684 |
+
@app.post("/api/cache/clear")
|
685 |
+
def clear_cache() -> Dict[str, str]:
|
686 |
+
"""Clear all cached data"""
|
687 |
+
with db.get_connection() as conn:
|
688 |
+
cursor = conn.cursor()
|
689 |
+
cursor.execute('DELETE FROM papers_cache')
|
690 |
+
conn.commit()
|
691 |
+
return {"message": "Cache cleared successfully"}
|
692 |
+
|
693 |
+
|
694 |
+
@app.post("/api/cache/refresh/{date_str}")
|
695 |
+
async def refresh_cache(date_str: str) -> Dict[str, Any]:
|
696 |
+
"""Force refresh cache for a specific date"""
|
697 |
+
try:
|
698 |
+
# Force fetch fresh data
|
699 |
+
html = await fetch_daily_html(date_str)
|
700 |
+
cards = parse_daily_cards(html)
|
701 |
+
|
702 |
+
# Cache the results
|
703 |
+
db.cache_papers(date_str, html, cards)
|
704 |
+
|
705 |
+
return {
|
706 |
+
"message": f"Cache refreshed for {date_str}",
|
707 |
+
"cards_count": len(cards)
|
708 |
+
}
|
709 |
+
except Exception as e:
|
710 |
+
raise HTTPException(status_code=500, detail=f"Failed to refresh cache: {str(e)}")
|
711 |
+
|
712 |
+
|
713 |
+
@app.get("/styles.css")
|
714 |
+
async def get_styles():
|
715 |
+
"""Serve CSS with no-cache headers to prevent caching issues during development"""
|
716 |
+
response = FileResponse("frontend/styles.css", media_type="text/css")
|
717 |
+
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
|
718 |
+
response.headers["Pragma"] = "no-cache"
|
719 |
+
response.headers["Expires"] = "0"
|
720 |
+
return response
|
721 |
+
|
722 |
+
# --- Static Frontend ---
|
723 |
+
|
724 |
+
FRONTEND_DIR = os.path.join(PROJECT_ROOT, "frontend")
|
725 |
+
os.makedirs(FRONTEND_DIR, exist_ok=True)
|
726 |
+
app.mount("/", StaticFiles(directory=FRONTEND_DIR, html=True), name="static")
|
727 |
+
|
728 |
+
# Initialize database
|
729 |
+
db = PapersDatabase(DB_PATH)
|
730 |
+
|
731 |
+
|