DVampire commited on
Commit
21fd477
·
1 Parent(s): 360e958
Files changed (16) hide show
  1. .gitignore +63 -0
  2. DEPLOYMENT.md +107 -0
  3. Dockerfile +0 -0
  4. README.md +146 -11
  5. agents/__init__.py +2 -0
  6. agents/evaluator.py +162 -0
  7. agents/prompt.py +397 -0
  8. app.py +13 -0
  9. cli.py +65 -0
  10. frontend/index.html +88 -0
  11. frontend/main.js +459 -0
  12. frontend/paper.html +88 -0
  13. frontend/paper.js +578 -0
  14. frontend/styles.css +1375 -0
  15. requirements.txt +11 -0
  16. 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
- title: Paperindex
3
- emoji: 📈
4
- colorFrom: pink
5
- colorTo: blue
6
- sdk: docker
7
- pinned: false
8
- license: mit
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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) => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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
+