Spaces:
Runtime error
Runtime error
LE Quoc Dat
commited on
Commit
·
417ad57
1
Parent(s):
9601828
update
Browse files- .gitignore +16 -0
- Dockerfile +20 -0
- LICENSE +21 -0
- README.md +71 -11
- app.py +136 -0
- requirements.txt +3 -0
- static/css/styles.css +500 -0
- static/favicon.ico +0 -0
- templates/index.html +1341 -0
.gitignore
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
**/*.swp
|
2 |
+
**/*.un~
|
3 |
+
|
4 |
+
**/*.swo
|
5 |
+
**/*.swn
|
6 |
+
|
7 |
+
# Python cache files
|
8 |
+
**/__pycache__/
|
9 |
+
*.pyc
|
10 |
+
.aider*
|
11 |
+
|
12 |
+
|
13 |
+
uploads/
|
14 |
+
TODO.txt
|
15 |
+
|
16 |
+
**/*Zone.Identifier
|
Dockerfile
ADDED
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Use an official Python runtime as a parent image
|
2 |
+
FROM python:3.9-slim
|
3 |
+
|
4 |
+
# Set the working directory in the container
|
5 |
+
WORKDIR /app
|
6 |
+
|
7 |
+
# Copy the current directory contents into the container at /app
|
8 |
+
COPY . /app
|
9 |
+
|
10 |
+
# Install any needed packages specified in requirements.txt
|
11 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
12 |
+
|
13 |
+
# Make port 7860 available to the world outside this container
|
14 |
+
EXPOSE 7860
|
15 |
+
|
16 |
+
# Define environment variable
|
17 |
+
ENV NAME World
|
18 |
+
|
19 |
+
# Run app.py when the container launches
|
20 |
+
CMD ["python", "app.py"]
|
LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
MIT License
|
2 |
+
|
3 |
+
Copyright (c) 2024 LE Quoc Dat
|
4 |
+
|
5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6 |
+
of this software and associated documentation files (the "Software"), to deal
|
7 |
+
in the Software without restriction, including without limitation the rights
|
8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9 |
+
copies of the Software, and to permit persons to whom the Software is
|
10 |
+
furnished to do so, subject to the following conditions:
|
11 |
+
|
12 |
+
The above copyright notice and this permission notice shall be included in all
|
13 |
+
copies or substantial portions of the Software.
|
14 |
+
|
15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21 |
+
SOFTWARE.
|
README.md
CHANGED
@@ -1,11 +1,71 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# PDF Flashcard Generator: AI Study Companion
|
2 |
+
|
3 |
+
Unlock the power of your PDFs with AI-driven learning! This web application transforms your documents into interactive flashcards and explanations using Claude AI, perfect for importing into **ANKI**. Ideal for students, researchers, and lifelong learners looking to supercharge their study sessions and spaced repetition practice.
|
4 |
+
|
5 |
+
![image](https://github.com/user-attachments/assets/c82dc51e-588e-4d14-b399-34c6784d5d99)
|
6 |
+
|
7 |
+
## Key Features:
|
8 |
+
- 📚 Upload and view PDFs directly in your browser
|
9 |
+
- 🤖 Generate flashcards and explanations with Claude AI
|
10 |
+
- 🖍️ Highlight important text for focused learning
|
11 |
+
- 💾 Save and export your flashcard collections to **ANKI**-compatible format
|
12 |
+
- 📱 Responsive design for desktop and mobile use
|
13 |
+
- 🔄 Seamless integration with **ANKI** for optimized spaced repetition
|
14 |
+
|
15 |
+
Dive into your documents, emerge with knowledge at your fingertips, and supercharge your **ANKI** decks!
|
16 |
+
|
17 |
+
## Getting Started
|
18 |
+
|
19 |
+
### Prerequisites
|
20 |
+
|
21 |
+
- Python 3.7+
|
22 |
+
- Flask
|
23 |
+
- Anthropic API key
|
24 |
+
|
25 |
+
### Installation
|
26 |
+
|
27 |
+
1. Clone the repository:
|
28 |
+
```
|
29 |
+
git clone https://github.com/quocdat-le-insacvl/pdf-flashcards-autogen.git
|
30 |
+
cd pdf-flashcards-autogen
|
31 |
+
```
|
32 |
+
|
33 |
+
2. Install the required packages:
|
34 |
+
```
|
35 |
+
pip install -r requirements.txt
|
36 |
+
```
|
37 |
+
|
38 |
+
3. Set up your Anthropic API key:
|
39 |
+
- Sign up for an API key at [https://www.anthropic.com](https://www.anthropic.com)
|
40 |
+
- Add your API key to the application when prompted
|
41 |
+
|
42 |
+
### Running the Application
|
43 |
+
|
44 |
+
1. Start the Flask server:
|
45 |
+
```
|
46 |
+
python app.py
|
47 |
+
```
|
48 |
+
|
49 |
+
2. Open your web browser and navigate to `http://localhost:5000`
|
50 |
+
|
51 |
+
## Usage
|
52 |
+
|
53 |
+
1. Upload a PDF file using the file input at the top of the page
|
54 |
+
2. Navigate through the PDF using the page controls or by scrolling
|
55 |
+
3. Select text in the PDF viewer
|
56 |
+
4. Click "Generate Flashcard" to create flashcards from the selected text
|
57 |
+
5. View, remove, or export generated flashcards
|
58 |
+
6. Use the highlight mode to mark important text in the PDF
|
59 |
+
|
60 |
+
## Contributing
|
61 |
+
|
62 |
+
Contributions are welcome! Please feel free to submit a Pull Request. For discussing improvements or new features, we encourage you to open an Issue first to facilitate community discussion.
|
63 |
+
|
64 |
+
## License
|
65 |
+
|
66 |
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
67 |
+
|
68 |
+
## Acknowledgments
|
69 |
+
|
70 |
+
- [PDF.js](https://mozilla.github.io/pdf.js/) for PDF rendering
|
71 |
+
- [Anthropic](https://www.anthropic.com) for the Claude AI API
|
app.py
ADDED
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from flask import Flask, request, jsonify, render_template, make_response, send_from_directory
|
2 |
+
import anthropic
|
3 |
+
import os
|
4 |
+
import json
|
5 |
+
from datetime import datetime
|
6 |
+
import base64
|
7 |
+
|
8 |
+
app = Flask(__name__)
|
9 |
+
app.config['UPLOAD_FOLDER'] = 'uploads'
|
10 |
+
|
11 |
+
@app.route('/favicon.ico')
|
12 |
+
def favicon():
|
13 |
+
return send_from_directory(os.path.join(app.root_path, 'static'),
|
14 |
+
'favicon.ico', mimetype='image/vnd.microsoft.icon')
|
15 |
+
@app.route('/')
|
16 |
+
def index():
|
17 |
+
recent_files = get_recent_files()
|
18 |
+
response = make_response(render_template('index.html', recent_files=recent_files))
|
19 |
+
return response
|
20 |
+
|
21 |
+
def get_recent_files():
|
22 |
+
if not os.path.exists(app.config['UPLOAD_FOLDER']):
|
23 |
+
os.makedirs(app.config['UPLOAD_FOLDER'])
|
24 |
+
files = os.listdir(app.config['UPLOAD_FOLDER'])
|
25 |
+
valid_files = [f for f in files if f.lower().endswith(('.pdf', '.txt'))]
|
26 |
+
valid_files.sort(key=lambda x: os.path.getmtime(os.path.join(app.config['UPLOAD_FOLDER'], x)), reverse=True)
|
27 |
+
return [{'filename': file, 'date': datetime.fromtimestamp(os.path.getmtime(os.path.join(app.config['UPLOAD_FOLDER'], file))).isoformat()} for file in valid_files[:5]]
|
28 |
+
|
29 |
+
@app.route('/get_recent_files')
|
30 |
+
def get_recent_files_route():
|
31 |
+
return jsonify(get_recent_files())
|
32 |
+
|
33 |
+
@app.route('/upload_file', methods=['POST'])
|
34 |
+
def upload_file():
|
35 |
+
if 'file' not in request.files:
|
36 |
+
return jsonify({'error': 'No file part'}), 400
|
37 |
+
file = request.files['file']
|
38 |
+
if file.filename == '':
|
39 |
+
return jsonify({'error': 'No selected file'}), 400
|
40 |
+
if file and (file.filename.lower().endswith(('.pdf', '.txt', '.epub'))):
|
41 |
+
filename = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
|
42 |
+
file.save(filename)
|
43 |
+
return jsonify({'message': 'File uploaded successfully', 'filename': file.filename}), 200
|
44 |
+
return jsonify({'error': 'Invalid file type. Please upload a PDF, TXT, or EPUB file.'}), 400
|
45 |
+
|
46 |
+
@app.route('/get_epub_content/<path:filename>')
|
47 |
+
def get_epub_content(filename):
|
48 |
+
file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename)
|
49 |
+
if os.path.exists(file_path) and filename.endswith('.epub'):
|
50 |
+
with open(file_path, 'rb') as file:
|
51 |
+
epub_content = base64.b64encode(file.read()).decode('utf-8')
|
52 |
+
return jsonify({'epub_content': epub_content})
|
53 |
+
return jsonify({'error': 'File not found or not an EPUB'}), 404
|
54 |
+
|
55 |
+
@app.route('/open_pdf/<path:filename>')
|
56 |
+
def open_pdf(filename):
|
57 |
+
return send_from_directory(app.config['UPLOAD_FOLDER'], filename)
|
58 |
+
|
59 |
+
@app.route('/generate_flashcard', methods=['POST'])
|
60 |
+
def generate_flashcard():
|
61 |
+
data = request.json
|
62 |
+
prompt = data['prompt']
|
63 |
+
api_key = request.headers.get('X-API-Key')
|
64 |
+
mode = data.get('mode', 'flashcard')
|
65 |
+
|
66 |
+
client = anthropic.Anthropic(api_key=api_key)
|
67 |
+
|
68 |
+
try:
|
69 |
+
model = data.get('model', "claude-3-5-sonnet-20240620")
|
70 |
+
message = client.messages.create(
|
71 |
+
model=model,
|
72 |
+
max_tokens=1024,
|
73 |
+
messages=[
|
74 |
+
{"role": "user", "content": prompt}
|
75 |
+
]
|
76 |
+
)
|
77 |
+
|
78 |
+
content = message.content[0].text
|
79 |
+
print(prompt)
|
80 |
+
print(content)
|
81 |
+
|
82 |
+
if mode == 'language':
|
83 |
+
# For Language mode, parse the content and return in the custom format
|
84 |
+
lines = content.split('\n')
|
85 |
+
word = ''
|
86 |
+
translation = ''
|
87 |
+
answer = ''
|
88 |
+
for line in lines:
|
89 |
+
if line.startswith('T:'):
|
90 |
+
translation = line[2:].strip()
|
91 |
+
elif line.startswith('Q:'):
|
92 |
+
word = line[2:].split('<b>')[1].split('</b>')[0].strip()
|
93 |
+
question = line[2:].strip()
|
94 |
+
elif line.startswith('A:'):
|
95 |
+
answer = line[2:].strip()
|
96 |
+
|
97 |
+
flashcard = {
|
98 |
+
'word': word,
|
99 |
+
'question': question,
|
100 |
+
'translation': translation,
|
101 |
+
'answer': answer
|
102 |
+
}
|
103 |
+
response = make_response(jsonify({'flashcard': flashcard}))
|
104 |
+
elif mode == 'flashcard' or 'flashcard' in prompt.lower():
|
105 |
+
flashcards = []
|
106 |
+
current_question = ''
|
107 |
+
current_answer = ''
|
108 |
+
|
109 |
+
for line in content.split('\n'):
|
110 |
+
if line.startswith('Q:'):
|
111 |
+
if current_question and current_answer:
|
112 |
+
flashcards.append({'question': current_question, 'answer': current_answer})
|
113 |
+
current_question = line[2:].strip()
|
114 |
+
current_answer = ''
|
115 |
+
elif line.startswith('A:'):
|
116 |
+
current_answer = line[2:].strip()
|
117 |
+
|
118 |
+
if current_question and current_answer:
|
119 |
+
flashcards.append({'question': current_question, 'answer': current_answer})
|
120 |
+
|
121 |
+
response = make_response(jsonify({'flashcards': flashcards}))
|
122 |
+
elif mode == 'explain' or 'explain' in prompt.lower():
|
123 |
+
# For Explain mode, return the entire content as the explanation
|
124 |
+
response = make_response(jsonify({'explanation': content}))
|
125 |
+
else:
|
126 |
+
response = make_response(jsonify({'error': 'Invalid mode'}))
|
127 |
+
|
128 |
+
# Set cookie with the API key
|
129 |
+
response.set_cookie('last_working_api_key', api_key, secure=True, httponly=True, samesite='Strict')
|
130 |
+
|
131 |
+
return response
|
132 |
+
except Exception as e:
|
133 |
+
return jsonify({'error': str(e)}), 500
|
134 |
+
|
135 |
+
if __name__ == '__main__':
|
136 |
+
app.run(debug=True)
|
requirements.txt
ADDED
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
1 |
+
flask_cors
|
2 |
+
flask
|
3 |
+
anthropic
|
static/css/styles.css
ADDED
@@ -0,0 +1,500 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
:root {
|
2 |
+
--primary-color: #3498db;
|
3 |
+
--secondary-color: #2c3e50;
|
4 |
+
--background-color: #ecf0f1;
|
5 |
+
--text-color: #34495e;
|
6 |
+
--highlight-color: #e74c3c;
|
7 |
+
--button-min-width: 100px;
|
8 |
+
--button-height: 36px;
|
9 |
+
--button-font-size: 14px;
|
10 |
+
}
|
11 |
+
|
12 |
+
body {
|
13 |
+
font-family: 'Roboto', Arial, sans-serif;
|
14 |
+
margin: 0;
|
15 |
+
padding: 0;
|
16 |
+
display: flex;
|
17 |
+
flex-direction: column;
|
18 |
+
height: 100vh;
|
19 |
+
overflow: hidden;
|
20 |
+
background-color: var(--background-color);
|
21 |
+
color: var(--text-color);
|
22 |
+
}
|
23 |
+
|
24 |
+
#top-bar {
|
25 |
+
display: flex;
|
26 |
+
justify-content: space-between;
|
27 |
+
align-items: center;
|
28 |
+
padding: 15px;
|
29 |
+
background-color: rgba(52, 152, 219, 0.4);
|
30 |
+
color: white;
|
31 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
32 |
+
backdrop-filter: blur(5px);
|
33 |
+
}
|
34 |
+
|
35 |
+
#file-input {
|
36 |
+
color: transparent;
|
37 |
+
}
|
38 |
+
|
39 |
+
#file-input::before {
|
40 |
+
content: 'Choose PDF';
|
41 |
+
display: inline-block;
|
42 |
+
min-width: var(--button-min-width);
|
43 |
+
height: var(--button-height);
|
44 |
+
padding: 0 15px;
|
45 |
+
font-size: var(--button-font-size);
|
46 |
+
background: var(--secondary-color);
|
47 |
+
color: white;
|
48 |
+
border-radius: 3px;
|
49 |
+
cursor: pointer;
|
50 |
+
display: inline-flex;
|
51 |
+
align-items: center;
|
52 |
+
justify-content: center;
|
53 |
+
}
|
54 |
+
|
55 |
+
#page-navigation {
|
56 |
+
display: flex;
|
57 |
+
align-items: center;
|
58 |
+
}
|
59 |
+
|
60 |
+
#current-page {
|
61 |
+
margin-right: 15px;
|
62 |
+
font-weight: bold;
|
63 |
+
}
|
64 |
+
|
65 |
+
#page-input {
|
66 |
+
width: 60px;
|
67 |
+
margin-right: 10px;
|
68 |
+
padding: 5px;
|
69 |
+
border: none;
|
70 |
+
border-radius: 3px;
|
71 |
+
}
|
72 |
+
|
73 |
+
#left-panel {
|
74 |
+
flex-grow: 1;
|
75 |
+
width: 70%;
|
76 |
+
overflow-y: auto;
|
77 |
+
padding: 20px;
|
78 |
+
box-sizing: border-box;
|
79 |
+
height: 100vh;
|
80 |
+
}
|
81 |
+
|
82 |
+
#right-panel {
|
83 |
+
transform: scale(1);
|
84 |
+
transform-origin: top right;
|
85 |
+
width: 30%;
|
86 |
+
height: 100vh;
|
87 |
+
position: fixed;
|
88 |
+
right: 0;
|
89 |
+
top: 0;
|
90 |
+
padding: 20px;
|
91 |
+
box-sizing: border-box;
|
92 |
+
overflow-y: auto;
|
93 |
+
background-color: white;
|
94 |
+
box-shadow: -2px 0 5px rgba(0, 0, 0, 0.1);
|
95 |
+
}
|
96 |
+
|
97 |
+
#file-input,
|
98 |
+
#mode-toggle,
|
99 |
+
#top-controls {
|
100 |
+
display: flex;
|
101 |
+
justify-content: space-between;
|
102 |
+
align-items: center;
|
103 |
+
margin-bottom: 15px;
|
104 |
+
}
|
105 |
+
|
106 |
+
#settings-icon {
|
107 |
+
cursor: pointer;
|
108 |
+
font-size: 24px;
|
109 |
+
line-height: 1;
|
110 |
+
}
|
111 |
+
|
112 |
+
.mode-btn {
|
113 |
+
flex: 1;
|
114 |
+
padding: 10px;
|
115 |
+
border: 1px solid var(--secondary-color);
|
116 |
+
background-color: white;
|
117 |
+
color: var(--secondary-color);
|
118 |
+
cursor: pointer;
|
119 |
+
transition: all 0.3s ease;
|
120 |
+
}
|
121 |
+
|
122 |
+
.mode-btn:not(:last-child) {
|
123 |
+
margin-right: 10px;
|
124 |
+
}
|
125 |
+
|
126 |
+
.mode-btn.selected {
|
127 |
+
background-color: var(--secondary-color);
|
128 |
+
color: white;
|
129 |
+
transform: scale(1.05);
|
130 |
+
}
|
131 |
+
|
132 |
+
.mode-btn:hover:not(.selected) {
|
133 |
+
background-color: var(--background-color);
|
134 |
+
}
|
135 |
+
|
136 |
+
#page-navigation {
|
137 |
+
display: flex;
|
138 |
+
align-items: center;
|
139 |
+
}
|
140 |
+
|
141 |
+
#page-input {
|
142 |
+
width: 60px;
|
143 |
+
margin-right: 10px;
|
144 |
+
padding: 5px;
|
145 |
+
border: 1px solid #ddd;
|
146 |
+
border-radius: 3px;
|
147 |
+
}
|
148 |
+
|
149 |
+
#settings-panel {
|
150 |
+
margin-top: 15px;
|
151 |
+
}
|
152 |
+
|
153 |
+
#api-key-input,
|
154 |
+
#model-select {
|
155 |
+
margin-bottom: 15px;
|
156 |
+
width: 100%;
|
157 |
+
padding: 8px;
|
158 |
+
border: 1px solid #ddd;
|
159 |
+
border-radius: 3px;
|
160 |
+
}
|
161 |
+
|
162 |
+
#pdf-viewer {
|
163 |
+
border: 1px solid #ddd;
|
164 |
+
background-color: white;
|
165 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
166 |
+
}
|
167 |
+
|
168 |
+
.page {
|
169 |
+
position: relative;
|
170 |
+
margin-bottom: 20px;
|
171 |
+
}
|
172 |
+
|
173 |
+
.text-layer {
|
174 |
+
position: absolute;
|
175 |
+
left: 0;
|
176 |
+
top: 0;
|
177 |
+
right: 0;
|
178 |
+
bottom: 0;
|
179 |
+
overflow: hidden;
|
180 |
+
opacity: 0.7;
|
181 |
+
line-height: 1.0;
|
182 |
+
}
|
183 |
+
|
184 |
+
.text-layer > span {
|
185 |
+
color: transparent;
|
186 |
+
position: absolute;
|
187 |
+
white-space: pre;
|
188 |
+
cursor: text;
|
189 |
+
transform-origin: 0% 0%;
|
190 |
+
}
|
191 |
+
|
192 |
+
::selection {
|
193 |
+
background: rgba(52, 152, 219, 0.3);
|
194 |
+
}
|
195 |
+
|
196 |
+
.highlight {
|
197 |
+
background-color: rgba(255, 255, 0, 0.4);
|
198 |
+
}
|
199 |
+
|
200 |
+
#system-prompt, #explain-prompt, #language-prompt {
|
201 |
+
width: 100%;
|
202 |
+
height: 150px;
|
203 |
+
margin-bottom: 15px;
|
204 |
+
padding: 10px;
|
205 |
+
border: 1px solid #ddd;
|
206 |
+
border-radius: 3px;
|
207 |
+
resize: vertical;
|
208 |
+
}
|
209 |
+
|
210 |
+
#explain-prompt, #language-prompt {
|
211 |
+
display: none;
|
212 |
+
}
|
213 |
+
|
214 |
+
#flashcards {
|
215 |
+
border: 1px solid #ddd;
|
216 |
+
padding: 15px;
|
217 |
+
margin-top: 15px;
|
218 |
+
background-color: white;
|
219 |
+
border-radius: 3px;
|
220 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
221 |
+
}
|
222 |
+
|
223 |
+
.flashcard {
|
224 |
+
margin-bottom: 15px;
|
225 |
+
padding: 15px;
|
226 |
+
border: 1px solid #ddd;
|
227 |
+
background-color: white;
|
228 |
+
border-radius: 3px;
|
229 |
+
transition: box-shadow 0.3s ease;
|
230 |
+
font-size: 16px;
|
231 |
+
}
|
232 |
+
|
233 |
+
.flashcard:hover {
|
234 |
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
235 |
+
}
|
236 |
+
|
237 |
+
.explanation-content {
|
238 |
+
white-space: pre-wrap;
|
239 |
+
text-align: left;
|
240 |
+
margin-bottom: 15px;
|
241 |
+
}
|
242 |
+
|
243 |
+
.explanation-content br {
|
244 |
+
display: block;
|
245 |
+
margin-bottom: 5px;
|
246 |
+
}
|
247 |
+
|
248 |
+
.remove-btn {
|
249 |
+
float: right;
|
250 |
+
background-color: var(--highlight-color);
|
251 |
+
color: white;
|
252 |
+
border: none;
|
253 |
+
padding: 5px 10px;
|
254 |
+
border-radius: 3px;
|
255 |
+
cursor: pointer;
|
256 |
+
transition: background-color 0.3s ease;
|
257 |
+
}
|
258 |
+
|
259 |
+
.remove-btn:hover {
|
260 |
+
background-color: #c0392b;
|
261 |
+
}
|
262 |
+
|
263 |
+
#recent-pdfs {
|
264 |
+
margin-top: 20px;
|
265 |
+
}
|
266 |
+
|
267 |
+
#recent-pdfs h3 {
|
268 |
+
margin-bottom: 10px;
|
269 |
+
color: var(--secondary-color);
|
270 |
+
}
|
271 |
+
|
272 |
+
#recent-pdfs ul {
|
273 |
+
padding-left: 20px;
|
274 |
+
list-style-type: none;
|
275 |
+
}
|
276 |
+
|
277 |
+
#recent-pdfs li {
|
278 |
+
margin-bottom: 5px;
|
279 |
+
}
|
280 |
+
|
281 |
+
#recent-pdfs a {
|
282 |
+
color: var(--primary-color);
|
283 |
+
text-decoration: none;
|
284 |
+
transition: color 0.3s ease;
|
285 |
+
}
|
286 |
+
|
287 |
+
#recent-pdfs a:hover {
|
288 |
+
color: #2980b9;
|
289 |
+
}
|
290 |
+
|
291 |
+
/* Modal styles */
|
292 |
+
.modal {
|
293 |
+
display: none;
|
294 |
+
position: fixed;
|
295 |
+
z-index: 1000;
|
296 |
+
left: 0;
|
297 |
+
top: 0;
|
298 |
+
width: 100%;
|
299 |
+
height: 100%;
|
300 |
+
overflow: auto;
|
301 |
+
background-color: rgba(0,0,0,0.4);
|
302 |
+
}
|
303 |
+
|
304 |
+
.modal-content {
|
305 |
+
background-color: #fefefe;
|
306 |
+
margin: 5% auto;
|
307 |
+
padding: 20px;
|
308 |
+
border: 1px solid #888;
|
309 |
+
width: 80%;
|
310 |
+
max-width: 800px;
|
311 |
+
max-height: 80vh;
|
312 |
+
overflow-y: auto;
|
313 |
+
}
|
314 |
+
|
315 |
+
.close {
|
316 |
+
color: #aaa;
|
317 |
+
float: right;
|
318 |
+
font-size: 28px;
|
319 |
+
font-weight: bold;
|
320 |
+
}
|
321 |
+
|
322 |
+
.close:hover,
|
323 |
+
.close:focus {
|
324 |
+
color: black;
|
325 |
+
text-decoration: none;
|
326 |
+
cursor: pointer;
|
327 |
+
}
|
328 |
+
|
329 |
+
/* Markdown styles */
|
330 |
+
#explanationModalContent {
|
331 |
+
font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Helvetica,Arial,sans-serif,Apple Color Emoji,Segoe UI Emoji;
|
332 |
+
font-size: 16px;
|
333 |
+
line-height: 1.8;
|
334 |
+
word-wrap: break-word;
|
335 |
+
}
|
336 |
+
|
337 |
+
#explanationModalContent h1,
|
338 |
+
#explanationModalContent h2,
|
339 |
+
#explanationModalContent h3,
|
340 |
+
#explanationModalContent h4,
|
341 |
+
#explanationModalContent h5,
|
342 |
+
#explanationModalContent h6 {
|
343 |
+
margin-top: 24px;
|
344 |
+
margin-bottom: 16px;
|
345 |
+
font-weight: 600;
|
346 |
+
line-height: 1.25;
|
347 |
+
}
|
348 |
+
|
349 |
+
#explanationModalContent h1 { font-size: 32px; }
|
350 |
+
#explanationModalContent h2 { font-size: 24px; }
|
351 |
+
#explanationModalContent h3 { font-size: 20px; }
|
352 |
+
#explanationModalContent h4 { font-size: 16px; }
|
353 |
+
#explanationModalContent h5 { font-size: 14px; }
|
354 |
+
#explanationModalContent h6 { font-size: 13px; }
|
355 |
+
|
356 |
+
#explanationModalContent p {
|
357 |
+
margin-top: 0;
|
358 |
+
margin-bottom: 16px;
|
359 |
+
}
|
360 |
+
|
361 |
+
#explanationModalContent code {
|
362 |
+
padding: 0.2em 0.4em;
|
363 |
+
margin: 0;
|
364 |
+
font-size: 14px;
|
365 |
+
background-color: rgba(27,31,35,0.05);
|
366 |
+
border-radius: 3px;
|
367 |
+
}
|
368 |
+
|
369 |
+
#explanationModalContent pre {
|
370 |
+
padding: 16px;
|
371 |
+
overflow: auto;
|
372 |
+
font-size: 85%;
|
373 |
+
line-height: 1.45;
|
374 |
+
background-color: #f6f8fa;
|
375 |
+
border-radius: 3px;
|
376 |
+
}
|
377 |
+
|
378 |
+
#explanationModalContent ul,
|
379 |
+
#explanationModalContent ol {
|
380 |
+
padding-left: 2em;
|
381 |
+
margin-top: 0;
|
382 |
+
margin-bottom: 16px;
|
383 |
+
}
|
384 |
+
|
385 |
+
#explanationModalContent img {
|
386 |
+
max-width: 100%;
|
387 |
+
box-sizing: content-box;
|
388 |
+
background-color: #fff;
|
389 |
+
}
|
390 |
+
|
391 |
+
#explanationModalContent blockquote {
|
392 |
+
padding: 0 1em;
|
393 |
+
color: #6a737d;
|
394 |
+
border-left: 0.25em solid #dfe2e5;
|
395 |
+
margin: 0 0 16px 0;
|
396 |
+
}
|
397 |
+
|
398 |
+
/* Button styles */
|
399 |
+
.mode-btn,
|
400 |
+
#go-to-page-btn,
|
401 |
+
#submit-btn,
|
402 |
+
#add-to-collection-btn,
|
403 |
+
#clear-collection-btn,
|
404 |
+
#export-csv-btn {
|
405 |
+
min-width: var(--button-min-width);
|
406 |
+
height: var(--button-height);
|
407 |
+
padding: 0 15px;
|
408 |
+
font-size: var(--button-font-size);
|
409 |
+
white-space: nowrap;
|
410 |
+
overflow: hidden;
|
411 |
+
text-overflow: ellipsis;
|
412 |
+
display: inline-flex;
|
413 |
+
align-items: center;
|
414 |
+
justify-content: center;
|
415 |
+
}
|
416 |
+
|
417 |
+
#go-to-page-btn,
|
418 |
+
#zoom-in-btn,
|
419 |
+
#zoom-out-btn {
|
420 |
+
background-color: var(--secondary-color);
|
421 |
+
color: white;
|
422 |
+
border: none;
|
423 |
+
border-radius: 3px;
|
424 |
+
cursor: pointer;
|
425 |
+
transition: background-color 0.3s ease;
|
426 |
+
margin-right: 5px;
|
427 |
+
}
|
428 |
+
|
429 |
+
#go-to-page-btn:hover,
|
430 |
+
#zoom-in-btn:hover,
|
431 |
+
#zoom-out-btn:hover {
|
432 |
+
background-color: #34495e;
|
433 |
+
}
|
434 |
+
|
435 |
+
#zoom-in-btn,
|
436 |
+
#zoom-out-btn {
|
437 |
+
width: 30px;
|
438 |
+
height: 30px;
|
439 |
+
font-size: 18px;
|
440 |
+
line-height: 1;
|
441 |
+
padding: 0;
|
442 |
+
}
|
443 |
+
|
444 |
+
#submit-btn {
|
445 |
+
width: 100%;
|
446 |
+
margin-bottom: 15px;
|
447 |
+
background-color: var(--primary-color);
|
448 |
+
color: white;
|
449 |
+
border: none;
|
450 |
+
border-radius: 3px;
|
451 |
+
cursor: pointer;
|
452 |
+
transition: background-color 0.3s ease;
|
453 |
+
}
|
454 |
+
|
455 |
+
#submit-btn:hover {
|
456 |
+
background-color: #2980b9;
|
457 |
+
}
|
458 |
+
|
459 |
+
#add-to-collection-btn,
|
460 |
+
#clear-collection-btn,
|
461 |
+
#export-csv-btn {
|
462 |
+
width: 100%;
|
463 |
+
margin-bottom: 10px;
|
464 |
+
background-color: var(--secondary-color);
|
465 |
+
color: white;
|
466 |
+
border: none;
|
467 |
+
border-radius: 3px;
|
468 |
+
cursor: pointer;
|
469 |
+
transition: background-color 0.3s ease;
|
470 |
+
}
|
471 |
+
|
472 |
+
#add-to-collection-btn:hover,
|
473 |
+
#clear-collection-btn:hover,
|
474 |
+
#export-csv-btn:hover {
|
475 |
+
background-color: #34495e;
|
476 |
+
}
|
477 |
+
|
478 |
+
/* Media query for small screens and high zoom levels */
|
479 |
+
@media screen and (max-width: 768px), screen and (min-resolution: 2dppx) {
|
480 |
+
#top-bar {
|
481 |
+
flex-wrap: wrap;
|
482 |
+
}
|
483 |
+
|
484 |
+
#file-input,
|
485 |
+
#mode-toggle,
|
486 |
+
#page-navigation {
|
487 |
+
width: 100%;
|
488 |
+
margin-bottom: 10px;
|
489 |
+
}
|
490 |
+
|
491 |
+
#right-panel {
|
492 |
+
width: 100%;
|
493 |
+
position: static;
|
494 |
+
height: auto;
|
495 |
+
}
|
496 |
+
|
497 |
+
#left-panel {
|
498 |
+
width: 100%;
|
499 |
+
}
|
500 |
+
}
|
static/favicon.ico
ADDED
templates/index.html
ADDED
@@ -0,0 +1,1341 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
|
4 |
+
<head>
|
5 |
+
<meta charset="UTF-8">
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7 |
+
<title>Document Viewer with Flashcard Generation</title>
|
8 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.min.js"></script>
|
9 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/jszip/3.1.5/jszip.min.js"></script>
|
10 |
+
<script src="https://cdn.jsdelivr.net/npm/epubjs/dist/epub.min.js"></script>
|
11 |
+
<link rel="stylesheet" href="/static/css/styles.css">
|
12 |
+
</head>
|
13 |
+
|
14 |
+
<body>
|
15 |
+
<div id="top-bar">
|
16 |
+
<input type="file" id="file-input" accept=".pdf,.txt,.epub">
|
17 |
+
<span id="current-page">Page: 1</span>
|
18 |
+
</div>
|
19 |
+
<div id="left-panel">
|
20 |
+
<div id="pdf-viewer"></div>
|
21 |
+
<div id="epub-viewer"></div>
|
22 |
+
</div>
|
23 |
+
<div id="right-panel">
|
24 |
+
<div id="top-controls">
|
25 |
+
<div id="settings-icon">⚙️</div>
|
26 |
+
<div id="page-navigation">
|
27 |
+
<button id="zoom-out-btn">-</button>
|
28 |
+
<button id="zoom-in-btn">+</button>
|
29 |
+
<input type="number" id="page-input" min="1" placeholder="Go to page">
|
30 |
+
<button id="go-to-page-btn">Go</button>
|
31 |
+
</div>
|
32 |
+
</div>
|
33 |
+
<div id="settings-panel" style="display: none;">
|
34 |
+
<input type="password" id="api-key-input" placeholder="Enter Claude API Key">
|
35 |
+
<select id="model-select">
|
36 |
+
<option value="claude-3-5-sonnet-20240620">Claude 3.5 Sonnet</option>
|
37 |
+
<option value="claude-3-haiku-20240307">Claude 3 Haiku</option>
|
38 |
+
</select>
|
39 |
+
<textarea id="system-prompt" placeholder="Enter system prompt for flashcard generation">Generate concise flashcards based on the following text. The number of flashcards should be proportional to the text's length and complexity, with a minimum of 1 and a maximum of 10. Each flashcard should have a question (Q:) that tests a key concept and an answer (A:) that is brief but complete. Ensure that the flashcards cover different aspects of the text when possible. Use <b> tags to emphasize important words or phrases in both questions and answers. Cite the short code or example to the question if needed.
|
40 |
+
|
41 |
+
Example:
|
42 |
+
Text: "In parallel computing, load balancing refers to the practice of distributing computational work evenly across multiple processing units. This is crucial for maximizing efficiency and minimizing idle time. Dynamic load balancing adjusts the distribution of work during runtime, while static load balancing determines the distribution before execution begins."
|
43 |
+
Q: What is the primary goal of <b>load balancing</b> in parallel computing?
|
44 |
+
A: To <b>distribute work evenly</b> across processing units, maximizing efficiency and minimizing idle time.
|
45 |
+
Q: How does <b>dynamic load balancing</b> differ from <b>static load balancing</b>?
|
46 |
+
A: Dynamic balancing <b>adjusts work distribution during runtime</b>, while static balancing <b>determines distribution before execution</b>.
|
47 |
+
|
48 |
+
That was example, now generate flashcards for this text:
|
49 |
+
</textarea>
|
50 |
+
<textarea id="explain-prompt" placeholder="Enter system prompt for explanation" style="display: none;">Explain the following text in simple terms, focusing on the main concepts and their relationships. Use clear and concise language, and break down complex ideas into easily understandable parts. If there are any technical terms, provide brief explanations for them. Return your explanation in markdown format.
|
51 |
+
|
52 |
+
Now explain this text:</textarea>
|
53 |
+
<textarea id="language-prompt" placeholder="Enter system prompt for language mode">Explain the word in the phrase in {targetLanguage} using this format:
|
54 |
+
|
55 |
+
T: [Translation of the word in Vietnamese]
|
56 |
+
Q: [Original phrase with the target word in <b> tags, or craft an example with ONLY the target word in <b> tags if no phrase is provided. The Q must contain the word in <b> tags.]
|
57 |
+
A: [Short explanation of the word's meaning in the context]
|
58 |
+
|
59 |
+
Example:
|
60 |
+
Word: "refused"
|
61 |
+
Phrase: "Hamas refused to join a new round of peace negotiations."
|
62 |
+
T: từ chối
|
63 |
+
Q: "Hamas <b>refused</b> to join a new round of peace negotiations."
|
64 |
+
A: Declined to accept or comply with a request or proposal.
|
65 |
+
|
66 |
+
Example when no phrase is provided or it's unclear:
|
67 |
+
Word: "analogues"
|
68 |
+
Phrase: ""
|
69 |
+
T: tương tự
|
70 |
+
Q: "Scientists often use animal <b>analogues</b> to study human diseases."
|
71 |
+
A: Things or concepts that are similar or comparable to something else, often used in scientific contexts.
|
72 |
+
|
73 |
+
Now explain the word in the phrase below:
|
74 |
+
Word: "{word}"
|
75 |
+
Phrase: "{phrase}"</textarea>
|
76 |
+
</div>
|
77 |
+
<div id="mode-toggle">
|
78 |
+
<button class="mode-btn selected" data-mode="flashcard">Flashcard</button>
|
79 |
+
<button class="mode-btn" data-mode="explain">Explain</button>
|
80 |
+
<button class="mode-btn" data-mode="language">Language</button>
|
81 |
+
</div>
|
82 |
+
<div id="language-buttons" style="display: none; margin-top: 10px;">
|
83 |
+
<button class="mode-btn" data-language="English">English</button>
|
84 |
+
<button class="mode-btn" data-language="French">French</button>
|
85 |
+
</div>
|
86 |
+
<button id="submit-btn" style="display: block;">Generate</button>
|
87 |
+
<div id="flashcards"></div>
|
88 |
+
<div id="collection">
|
89 |
+
<button id="add-to-collection-btn">Add to Collection (0)</button>
|
90 |
+
<button id="clear-collection-btn">Clear Collection</button>
|
91 |
+
</div>
|
92 |
+
<button id="export-csv-btn" style="display: none;">Export Flashcards to CSV</button>
|
93 |
+
<div id="recent-files">
|
94 |
+
<h3>Recent Files</h3>
|
95 |
+
<ul id="file-list"></ul>
|
96 |
+
</div>
|
97 |
+
<div id="highlight-instruction" style="font-size: 0.7em; color: #666; position: absolute; bottom: 5px; right: 5px;">Use Alt+Select to highlight text</div>
|
98 |
+
</div>
|
99 |
+
|
100 |
+
<!-- Explanation Modal -->
|
101 |
+
<div id="explanationModal" class="modal">
|
102 |
+
<div class="modal-content">
|
103 |
+
<span class="close">×</span>
|
104 |
+
<div id="explanationModalContent"></div>
|
105 |
+
</div>
|
106 |
+
</div>
|
107 |
+
|
108 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"></script>
|
109 |
+
<script>
|
110 |
+
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.worker.min.js';
|
111 |
+
|
112 |
+
const fileInput = document.getElementById('file-input');
|
113 |
+
const pdfViewer = document.getElementById('pdf-viewer');
|
114 |
+
const modeToggle = document.getElementById('mode-toggle');
|
115 |
+
const systemPrompt = document.getElementById('system-prompt');
|
116 |
+
const submitBtn = document.getElementById('submit-btn');
|
117 |
+
const flashcardsContainer = document.getElementById('flashcards');
|
118 |
+
const apiKeyInput = document.getElementById('api-key-input');
|
119 |
+
const modelSelect = document.getElementById('model-select');
|
120 |
+
const recentPdfList = document.getElementById('recent-pdf-list');
|
121 |
+
|
122 |
+
let pdfDoc = null;
|
123 |
+
let pageNum = 1;
|
124 |
+
let pageRendering = false;
|
125 |
+
let pageNumPending = null;
|
126 |
+
let scale = 3;
|
127 |
+
const minScale = 0.5;
|
128 |
+
const maxScale = 5;
|
129 |
+
let mode = 'flashcard';
|
130 |
+
let apiKey = '';
|
131 |
+
let currentFileName = '';
|
132 |
+
let currentPage = 1;
|
133 |
+
let selectedModel = 'claude-3-haiku-20240307';
|
134 |
+
let lastProcessedQuery = '';
|
135 |
+
let lastRequestTime = 0;
|
136 |
+
const cooldownTime = 1000; // 1 second cooldown
|
137 |
+
|
138 |
+
function renderPage(num) {
|
139 |
+
pageRendering = true;
|
140 |
+
pdfDoc.getPage(num).then(function (page) {
|
141 |
+
const viewport = page.getViewport({ scale: scale });
|
142 |
+
const pixelRatio = window.devicePixelRatio || 1;
|
143 |
+
const adjustedViewport = page.getViewport({ scale: scale * pixelRatio });
|
144 |
+
|
145 |
+
const pageDiv = document.createElement('div');
|
146 |
+
pageDiv.className = 'page';
|
147 |
+
pageDiv.dataset.pageNumber = num;
|
148 |
+
pageDiv.style.width = `${viewport.width}px`;
|
149 |
+
pageDiv.style.height = `${viewport.height}px`;
|
150 |
+
|
151 |
+
const canvas = document.createElement('canvas');
|
152 |
+
const ctx = canvas.getContext('2d');
|
153 |
+
canvas.height = adjustedViewport.height;
|
154 |
+
canvas.width = adjustedViewport.width;
|
155 |
+
canvas.style.width = `${viewport.width}px`;
|
156 |
+
canvas.style.height = `${viewport.height}px`;
|
157 |
+
|
158 |
+
const renderContext = {
|
159 |
+
canvasContext: ctx,
|
160 |
+
viewport: adjustedViewport,
|
161 |
+
enableWebGL: true,
|
162 |
+
renderInteractiveForms: true,
|
163 |
+
};
|
164 |
+
|
165 |
+
const renderTask = page.render(renderContext);
|
166 |
+
|
167 |
+
renderTask.promise.then(function () {
|
168 |
+
pageRendering = false;
|
169 |
+
if (pageNumPending !== null) {
|
170 |
+
renderPage(pageNumPending);
|
171 |
+
pageNumPending = null;
|
172 |
+
}
|
173 |
+
});
|
174 |
+
|
175 |
+
pageDiv.appendChild(canvas);
|
176 |
+
|
177 |
+
// Text layer
|
178 |
+
const textLayerDiv = document.createElement('div');
|
179 |
+
textLayerDiv.className = 'text-layer';
|
180 |
+
textLayerDiv.style.width = `${viewport.width}px`;
|
181 |
+
textLayerDiv.style.height = `${viewport.height}px`;
|
182 |
+
pageDiv.appendChild(textLayerDiv);
|
183 |
+
|
184 |
+
page.getTextContent().then(function (textContent) {
|
185 |
+
pdfjsLib.renderTextLayer({
|
186 |
+
textContent: textContent,
|
187 |
+
container: textLayerDiv,
|
188 |
+
viewport: viewport,
|
189 |
+
textDivs: []
|
190 |
+
});
|
191 |
+
});
|
192 |
+
|
193 |
+
pdfViewer.appendChild(pageDiv);
|
194 |
+
|
195 |
+
// Attach language mode listener to the new page
|
196 |
+
attachLanguageModeListener(pageDiv);
|
197 |
+
|
198 |
+
// Render highlights for this page
|
199 |
+
renderHighlights();
|
200 |
+
|
201 |
+
// Check if we need to load more pages
|
202 |
+
if (num < pdfDoc.numPages && pdfViewer.scrollHeight <= window.innerHeight * 2) {
|
203 |
+
renderPage(num + 1);
|
204 |
+
}
|
205 |
+
});
|
206 |
+
}
|
207 |
+
|
208 |
+
function loadFile(file) {
|
209 |
+
if (file.name.endsWith('.pdf')) {
|
210 |
+
loadPDF(file);
|
211 |
+
} else if (file.name.endsWith('.txt')) {
|
212 |
+
loadTXT(file);
|
213 |
+
}
|
214 |
+
}
|
215 |
+
|
216 |
+
function loadPDF(file) {
|
217 |
+
const fileReader = new FileReader();
|
218 |
+
fileReader.onload = function () {
|
219 |
+
const typedarray = new Uint8Array(this.result);
|
220 |
+
|
221 |
+
pdfjsLib.getDocument(typedarray).promise.then(function (pdf) {
|
222 |
+
pdfDoc = pdf;
|
223 |
+
pdfViewer.innerHTML = '';
|
224 |
+
currentFileName = file.name;
|
225 |
+
const lastPage = localStorage.getItem(`lastPage_${currentFileName}`);
|
226 |
+
pageNum = lastPage ? Math.max(parseInt(lastPage) - 2, 1) : 1;
|
227 |
+
loadScaleForCurrentFile();
|
228 |
+
renderPage(pageNum);
|
229 |
+
updateCurrentPage(pageNum);
|
230 |
+
hideHeaderPanel();
|
231 |
+
loadHighlights();
|
232 |
+
});
|
233 |
+
};
|
234 |
+
fileReader.readAsArrayBuffer(file);
|
235 |
+
}
|
236 |
+
|
237 |
+
function loadTXT(file) {
|
238 |
+
const fileReader = new FileReader();
|
239 |
+
fileReader.onload = function () {
|
240 |
+
const content = this.result;
|
241 |
+
pdfViewer.innerHTML = '';
|
242 |
+
currentFileName = file.name;
|
243 |
+
const textContainer = document.createElement('div');
|
244 |
+
textContainer.className = 'text-content';
|
245 |
+
textContainer.textContent = content;
|
246 |
+
pdfViewer.appendChild(textContainer);
|
247 |
+
hideHeaderPanel();
|
248 |
+
|
249 |
+
// Add event listeners for language mode
|
250 |
+
attachLanguageModeListener(textContainer);
|
251 |
+
};
|
252 |
+
fileReader.readAsText(file);
|
253 |
+
}
|
254 |
+
|
255 |
+
function hideHeaderPanel() {
|
256 |
+
document.getElementById('top-bar').style.display = 'none';
|
257 |
+
}
|
258 |
+
|
259 |
+
function goToPage(num) {
|
260 |
+
if (num >= 1 && num <= pdfDoc.numPages) {
|
261 |
+
pageNum = num;
|
262 |
+
pdfViewer.innerHTML = '';
|
263 |
+
renderPage(pageNum);
|
264 |
+
updateCurrentPage(pageNum);
|
265 |
+
localStorage.setItem(`lastPage_${currentFileName}`, pageNum);
|
266 |
+
} else {
|
267 |
+
alert('Invalid page number');
|
268 |
+
}
|
269 |
+
}
|
270 |
+
|
271 |
+
function updateCurrentPage(num) {
|
272 |
+
if (num !== currentPage) {
|
273 |
+
currentPage = num;
|
274 |
+
document.getElementById('current-page').textContent = `Page: ${num}`;
|
275 |
+
document.getElementById('page-input').value = num;
|
276 |
+
localStorage.setItem(`lastPage_${currentFileName}`, num);
|
277 |
+
}
|
278 |
+
}
|
279 |
+
|
280 |
+
// Infinite scrolling with page tracking
|
281 |
+
document.getElementById('left-panel').addEventListener('scroll', function () {
|
282 |
+
if (this.scrollTop + this.clientHeight >= this.scrollHeight - 500) {
|
283 |
+
if (pageNum < pdfDoc.numPages) {
|
284 |
+
pageNum++;
|
285 |
+
renderPage(pageNum);
|
286 |
+
}
|
287 |
+
}
|
288 |
+
|
289 |
+
// Update current page based on scroll position
|
290 |
+
const pages = document.querySelectorAll('.page');
|
291 |
+
for (let i = 0; i < pages.length; i++) {
|
292 |
+
const page = pages[i];
|
293 |
+
const rect = page.getBoundingClientRect();
|
294 |
+
if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
|
295 |
+
const newPageNum = parseInt(page.dataset.pageNumber);
|
296 |
+
updateCurrentPage(newPageNum);
|
297 |
+
break;
|
298 |
+
}
|
299 |
+
}
|
300 |
+
});
|
301 |
+
|
302 |
+
function handleLanguageMode(event, targetLanguage) {
|
303 |
+
if (mode !== 'language') return;
|
304 |
+
|
305 |
+
event.preventDefault();
|
306 |
+
const selection = window.getSelection();
|
307 |
+
if (selection.rangeCount > 0) {
|
308 |
+
const range = selection.getRangeAt(0);
|
309 |
+
const selectedText = selection.toString().trim();
|
310 |
+
if (selectedText) {
|
311 |
+
const phrase = getPhrase(range);
|
312 |
+
const currentTime = Date.now();
|
313 |
+
if (phrase !== lastProcessedQuery && currentTime - lastRequestTime >= cooldownTime) {
|
314 |
+
lastProcessedQuery = phrase;
|
315 |
+
lastRequestTime = currentTime;
|
316 |
+
speakWord(selectedText);
|
317 |
+
generateLanguageFlashcard(selectedText, phrase, targetLanguage);
|
318 |
+
}
|
319 |
+
|
320 |
+
}
|
321 |
+
}
|
322 |
+
}
|
323 |
+
|
324 |
+
let voices = [];
|
325 |
+
|
326 |
+
function populateVoiceList() {
|
327 |
+
voices = speechSynthesis.getVoices();
|
328 |
+
}
|
329 |
+
|
330 |
+
populateVoiceList();
|
331 |
+
if (speechSynthesis.onvoiceschanged !== undefined) {
|
332 |
+
speechSynthesis.onvoiceschanged = populateVoiceList;
|
333 |
+
}
|
334 |
+
|
335 |
+
function speakWord(word) {
|
336 |
+
console.log('Attempting to speak word:', word);
|
337 |
+
|
338 |
+
const utterance = new SpeechSynthesisUtterance(word);
|
339 |
+
utterance.rate = 0.8; // Slightly slower rate for clarity
|
340 |
+
|
341 |
+
let englishVoice;
|
342 |
+
if (voices.length > 1) {
|
343 |
+
englishVoice = voices[2];
|
344 |
+
console.log('Using second voice in the list:', englishVoice.name);
|
345 |
+
} else {
|
346 |
+
englishVoice = voices.find(voice => voice.name === "Microsoft Zira Desktop - English (United States)") ||
|
347 |
+
voices.find(voice => /en/i.test(voice.lang));
|
348 |
+
if (englishVoice) {
|
349 |
+
console.log('Using voice:', englishVoice.name);
|
350 |
+
} else {
|
351 |
+
console.log('No suitable English voice found. Using default voice.');
|
352 |
+
}
|
353 |
+
}
|
354 |
+
|
355 |
+
if (englishVoice) {
|
356 |
+
utterance.voice = englishVoice;
|
357 |
+
}
|
358 |
+
|
359 |
+
try {
|
360 |
+
speechSynthesis.speak(utterance);
|
361 |
+
} catch (error) {
|
362 |
+
console.error('Error initiating speech:', error);
|
363 |
+
}
|
364 |
+
}
|
365 |
+
|
366 |
+
function getPhrase(range) {
|
367 |
+
const sentenceStart = /[.!?]\s+[A-Z]|^[A-Z]/;
|
368 |
+
const sentenceEnd = /[.!?](?=\s|$)/;
|
369 |
+
|
370 |
+
let startNode = range.startContainer;
|
371 |
+
let endNode = range.endContainer;
|
372 |
+
let startOffset = range.startOffset;
|
373 |
+
let endOffset = range.endOffset;
|
374 |
+
|
375 |
+
// Expand to sentence boundaries
|
376 |
+
while (startNode && startNode.textContent && !sentenceStart.test(startNode.textContent.slice(0, startOffset))) {
|
377 |
+
if (startNode.previousSibling) {
|
378 |
+
startNode = startNode.previousSibling;
|
379 |
+
startOffset = startNode.textContent ? startNode.textContent.length : 0;
|
380 |
+
} else if (startNode.parentNode && startNode.parentNode.previousSibling) {
|
381 |
+
startNode = startNode.parentNode.previousSibling.lastChild;
|
382 |
+
startOffset = startNode && startNode.textContent ? startNode.textContent.length : 0;
|
383 |
+
} else {
|
384 |
+
break;
|
385 |
+
}
|
386 |
+
}
|
387 |
+
|
388 |
+
while (endNode && endNode.textContent && !sentenceEnd.test(endNode.textContent.slice(endOffset))) {
|
389 |
+
if (endNode.nextSibling) {
|
390 |
+
endNode = endNode.nextSibling;
|
391 |
+
endOffset = 0;
|
392 |
+
} else if (endNode.parentNode && endNode.parentNode.nextSibling) {
|
393 |
+
endNode = endNode.parentNode.nextSibling.firstChild;
|
394 |
+
endOffset = 0;
|
395 |
+
} else {
|
396 |
+
break;
|
397 |
+
}
|
398 |
+
}
|
399 |
+
|
400 |
+
// Check if we have valid start and end nodes
|
401 |
+
if (startNode && startNode.nodeType === Node.TEXT_NODE &&
|
402 |
+
endNode && endNode.nodeType === Node.TEXT_NODE &&
|
403 |
+
startNode.textContent && endNode.textContent) {
|
404 |
+
const phraseRange = document.createRange();
|
405 |
+
phraseRange.setStart(startNode, startOffset);
|
406 |
+
phraseRange.setEnd(endNode, endOffset);
|
407 |
+
return phraseRange.toString().trim();
|
408 |
+
} else {
|
409 |
+
// If we don't have valid nodes, return the original selection
|
410 |
+
return range.toString().trim();
|
411 |
+
}
|
412 |
+
}
|
413 |
+
|
414 |
+
function getFullSentence(text, word) {
|
415 |
+
const sentenceRegex = /[^.!?]+[.!?]+\s*/g;
|
416 |
+
const sentences = text.match(sentenceRegex) || [text];
|
417 |
+
|
418 |
+
const matchingSentences = sentences.filter(sentence =>
|
419 |
+
new RegExp(`\\b${word}\\b`, 'i').test(sentence)
|
420 |
+
);
|
421 |
+
|
422 |
+
if (matchingSentences.length === 0) {
|
423 |
+
const wordIndex = text.indexOf(word);
|
424 |
+
if (wordIndex !== -1) {
|
425 |
+
const start = Math.max(0, wordIndex - 30);
|
426 |
+
const end = Math.min(text.length, wordIndex + word.length + 30);
|
427 |
+
return text.slice(start, end);
|
428 |
+
}
|
429 |
+
return text;
|
430 |
+
} else if (matchingSentences.length === 1) {
|
431 |
+
// If only one matching sentence, return it
|
432 |
+
return matchingSentences[0].trim();
|
433 |
+
} else {
|
434 |
+
// If multiple matching sentences, return them joined
|
435 |
+
return matchingSentences.join(' ').trim();
|
436 |
+
}
|
437 |
+
}
|
438 |
+
|
439 |
+
async function generateLanguageFlashcard(word, phrase, targetLanguage) {
|
440 |
+
if (!apiKey) {
|
441 |
+
alert('Please enter your Claude API key first.');
|
442 |
+
return;
|
443 |
+
}
|
444 |
+
|
445 |
+
const prompt = document.getElementById('language-prompt').value
|
446 |
+
.replace('{word}', word)
|
447 |
+
.replace('{phrase}', phrase)
|
448 |
+
.replace('{targetLanguage}', targetLanguage);
|
449 |
+
|
450 |
+
try {
|
451 |
+
const response = await callClaudeAPI(prompt);
|
452 |
+
if (response.flashcard) {
|
453 |
+
const flashcard = response.flashcard;
|
454 |
+
const formattedFlashcard = {
|
455 |
+
question: flashcard.question,
|
456 |
+
answer: flashcard.answer,
|
457 |
+
word: flashcard.word,
|
458 |
+
translation: flashcard.translation
|
459 |
+
};
|
460 |
+
console.log(formattedFlashcard);
|
461 |
+
displayLanguageFlashcard(formattedFlashcard);
|
462 |
+
} else {
|
463 |
+
throw new Error('Invalid response from API');
|
464 |
+
}
|
465 |
+
} catch (error) {
|
466 |
+
console.error('Error calling Claude API:', error);
|
467 |
+
alert('Failed to generate language flashcard. Please check your API key and try again.');
|
468 |
+
}
|
469 |
+
}
|
470 |
+
|
471 |
+
async function generateContent() {
|
472 |
+
if (!apiKey) {
|
473 |
+
alert('Please enter your Claude API key first.');
|
474 |
+
return;
|
475 |
+
}
|
476 |
+
|
477 |
+
const selection = window.getSelection();
|
478 |
+
if (selection.rangeCount > 0 && selection.toString().trim() !== '') {
|
479 |
+
const selectedText = selection.toString();
|
480 |
+
let prompt;
|
481 |
+
|
482 |
+
if (mode === 'flashcard') {
|
483 |
+
prompt = `${systemPrompt.value}\n\n${selectedText}`;
|
484 |
+
} else if (mode === 'explain') {
|
485 |
+
const explainPromptValue = document.getElementById('explain-prompt').value;
|
486 |
+
prompt = `${explainPromptValue}\n\n${selectedText}`;
|
487 |
+
} else {
|
488 |
+
return;
|
489 |
+
}
|
490 |
+
|
491 |
+
// Disable the button, change its color, and show notification
|
492 |
+
submitBtn.disabled = true;
|
493 |
+
submitBtn.style.backgroundColor = '#808080'; // Change to gray
|
494 |
+
const notification = document.createElement('div');
|
495 |
+
notification.textContent = 'Generating...';
|
496 |
+
notification.style.position = 'fixed';
|
497 |
+
notification.style.top = '20px';
|
498 |
+
notification.style.right = '20px';
|
499 |
+
notification.style.padding = '10px';
|
500 |
+
notification.style.backgroundColor = 'rgba(0, 128, 0, 0.7)'; // Change to green
|
501 |
+
notification.style.color = 'white';
|
502 |
+
notification.style.borderRadius = '5px';
|
503 |
+
notification.style.zIndex = '1000';
|
504 |
+
document.body.appendChild(notification);
|
505 |
+
|
506 |
+
try {
|
507 |
+
const response = await callClaudeAPI(prompt);
|
508 |
+
if (mode === 'flashcard' && response.flashcards) {
|
509 |
+
displayFlashcards(response.flashcards, true);
|
510 |
+
} else if (mode === 'explain' && response.explanation) {
|
511 |
+
displayExplanation(response.explanation);
|
512 |
+
} else {
|
513 |
+
throw new Error('Invalid response from API');
|
514 |
+
}
|
515 |
+
} catch (error) {
|
516 |
+
console.error('Error calling Claude API:', error);
|
517 |
+
alert(`Failed to generate ${mode === 'flashcard' ? 'flashcards' : 'explanation'}. Please check your API key and try again.`);
|
518 |
+
} finally {
|
519 |
+
// Remove notification, re-enable button, and restore its color after 3 seconds
|
520 |
+
setTimeout(() => {
|
521 |
+
document.body.removeChild(notification);
|
522 |
+
submitBtn.disabled = false;
|
523 |
+
submitBtn.style.backgroundColor = ''; // Restore original color
|
524 |
+
}, 3000);
|
525 |
+
}
|
526 |
+
} else {
|
527 |
+
alert(`Please select some text from the PDF to generate ${mode === 'flashcard' ? 'flashcards' : 'an explanation'}.`);
|
528 |
+
}
|
529 |
+
}
|
530 |
+
|
531 |
+
function displayExplanation(explanation) {
|
532 |
+
// Display in right panel
|
533 |
+
const explanationElement = document.createElement('div');
|
534 |
+
explanationElement.className = 'explanation';
|
535 |
+
explanationElement.innerHTML = `
|
536 |
+
<h3>Explanation</h3>
|
537 |
+
<div class="explanation-content">${explanation}</div>
|
538 |
+
<button class="remove-btn">Remove</button>
|
539 |
+
`;
|
540 |
+
explanationElement.querySelector('.remove-btn').addEventListener('click', function () {
|
541 |
+
explanationElement.remove();
|
542 |
+
});
|
543 |
+
flashcardsContainer.appendChild(explanationElement);
|
544 |
+
|
545 |
+
// Display in modal
|
546 |
+
const modal = document.getElementById('explanationModal');
|
547 |
+
const modalContent = document.getElementById('explanationModalContent');
|
548 |
+
const closeBtn = document.getElementsByClassName('close')[0];
|
549 |
+
|
550 |
+
// Convert markdown to HTML
|
551 |
+
const converter = new showdown.Converter();
|
552 |
+
const htmlContent = converter.makeHtml(explanation);
|
553 |
+
|
554 |
+
modalContent.innerHTML = htmlContent;
|
555 |
+
modal.style.display = 'block';
|
556 |
+
|
557 |
+
closeBtn.onclick = function () {
|
558 |
+
modal.style.display = 'none';
|
559 |
+
}
|
560 |
+
|
561 |
+
window.onclick = function (event) {
|
562 |
+
if (event.target == modal) {
|
563 |
+
modal.style.display = 'none';
|
564 |
+
}
|
565 |
+
}
|
566 |
+
}
|
567 |
+
|
568 |
+
async function callClaudeAPI(prompt) {
|
569 |
+
const response = await fetch('/generate_flashcard', {
|
570 |
+
method: 'POST',
|
571 |
+
headers: {
|
572 |
+
'Content-Type': 'application/json',
|
573 |
+
'X-API-Key': apiKey
|
574 |
+
},
|
575 |
+
body: JSON.stringify({
|
576 |
+
prompt: prompt,
|
577 |
+
model: selectedModel,
|
578 |
+
mode: mode // Add the current mode to the request
|
579 |
+
})
|
580 |
+
});
|
581 |
+
|
582 |
+
if (!response.ok) {
|
583 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
584 |
+
}
|
585 |
+
|
586 |
+
return await response.json();
|
587 |
+
}
|
588 |
+
|
589 |
+
modelSelect.addEventListener('change', function () {
|
590 |
+
selectedModel = this.value;
|
591 |
+
});
|
592 |
+
|
593 |
+
function displayFlashcards(flashcards, append = false) {
|
594 |
+
if (!append) {
|
595 |
+
flashcardsContainer.innerHTML = ''; // Clear existing flashcards only if not appending
|
596 |
+
}
|
597 |
+
flashcards.forEach(flashcard => {
|
598 |
+
const flashcardElement = document.createElement('div');
|
599 |
+
flashcardElement.className = 'flashcard';
|
600 |
+
flashcardElement.innerHTML = `
|
601 |
+
<strong>Q: ${flashcard.question}</strong><br>
|
602 |
+
A: ${flashcard.answer}
|
603 |
+
<button class="remove-btn">Remove</button>
|
604 |
+
`;
|
605 |
+
flashcardElement.querySelector('.remove-btn').addEventListener('click', function () {
|
606 |
+
flashcardElement.remove();
|
607 |
+
updateExportButtonVisibility();
|
608 |
+
});
|
609 |
+
flashcardsContainer.appendChild(flashcardElement);
|
610 |
+
});
|
611 |
+
updateExportButtonVisibility();
|
612 |
+
}
|
613 |
+
|
614 |
+
function displayLanguageFlashcard(flashcard) {
|
615 |
+
const flashcardElement = document.createElement('div');
|
616 |
+
flashcardElement.className = 'flashcard language-flashcard';
|
617 |
+
flashcardElement.dataset.question = flashcard.question;
|
618 |
+
flashcardElement.dataset.word = flashcard.word;
|
619 |
+
flashcardElement.dataset.translation = flashcard.translation;
|
620 |
+
flashcardElement.dataset.answer = flashcard.answer;
|
621 |
+
flashcardElement.innerHTML = `
|
622 |
+
<div style="font-size: 1.2em; margin-bottom: 10px;"><b>${flashcard.word}</b>: ${flashcard.translation}</div>
|
623 |
+
<div>- ${flashcard.answer}</div>
|
624 |
+
<button class="remove-btn">Remove</button>
|
625 |
+
`;
|
626 |
+
flashcardElement.querySelector('.remove-btn').addEventListener('click', function () {
|
627 |
+
flashcardElement.remove();
|
628 |
+
updateExportButtonVisibility();
|
629 |
+
});
|
630 |
+
flashcardsContainer.appendChild(flashcardElement);
|
631 |
+
updateExportButtonVisibility();
|
632 |
+
}
|
633 |
+
|
634 |
+
let flashcardCollectionCount = 0;
|
635 |
+
let languageCollectionCount = 0;
|
636 |
+
let collectedFlashcards = [];
|
637 |
+
let collectedLanguageFlashcards = [];
|
638 |
+
|
639 |
+
function addToCollection() {
|
640 |
+
const newFlashcards = Array.from(document.querySelectorAll('.flashcard:not(.in-collection)')).map(flashcard => {
|
641 |
+
if (flashcard.classList.contains('language-flashcard')) {
|
642 |
+
const word = flashcard.dataset.word;
|
643 |
+
const translation = flashcard.dataset.translation;
|
644 |
+
const answer = flashcard.dataset.answer;
|
645 |
+
const question = flashcard.dataset.question;
|
646 |
+
return {
|
647 |
+
word: word,
|
648 |
+
phrase: question,
|
649 |
+
translationAnswer: `${translation.trim()}\n${answer.trim()}`
|
650 |
+
};
|
651 |
+
} else {
|
652 |
+
const question = flashcard.querySelector('strong').textContent.slice(3);
|
653 |
+
const answer = flashcard.innerHTML.split('<br>')[1].split('<button')[0].trim().slice(3);
|
654 |
+
return {
|
655 |
+
phrase: question,
|
656 |
+
translationAnswer: answer
|
657 |
+
};
|
658 |
+
}
|
659 |
+
});
|
660 |
+
|
661 |
+
if (mode === 'language') {
|
662 |
+
collectedLanguageFlashcards = collectedLanguageFlashcards.concat(newFlashcards);
|
663 |
+
updateCollectionCount(newFlashcards.length, 'language');
|
664 |
+
} else {
|
665 |
+
collectedFlashcards = collectedFlashcards.concat(newFlashcards);
|
666 |
+
updateCollectionCount(newFlashcards.length, 'flashcard');
|
667 |
+
}
|
668 |
+
clearDisplayedFlashcards();
|
669 |
+
updateExportButtonVisibility();
|
670 |
+
}
|
671 |
+
|
672 |
+
function clearDisplayedFlashcards() {
|
673 |
+
flashcardsContainer.innerHTML = '';
|
674 |
+
}
|
675 |
+
|
676 |
+
function updateCollectionCount(change, collectionType) {
|
677 |
+
if (collectionType === 'language') {
|
678 |
+
languageCollectionCount += change;
|
679 |
+
localStorage.setItem('languageCollectionCount', languageCollectionCount);
|
680 |
+
localStorage.setItem('collectedLanguageFlashcards', JSON.stringify(collectedLanguageFlashcards));
|
681 |
+
} else {
|
682 |
+
flashcardCollectionCount += change;
|
683 |
+
localStorage.setItem('flashcardCollectionCount', flashcardCollectionCount);
|
684 |
+
localStorage.setItem('collectedFlashcards', JSON.stringify(collectedFlashcards));
|
685 |
+
}
|
686 |
+
updateAddToCollectionButtonText();
|
687 |
+
}
|
688 |
+
|
689 |
+
function updateAddToCollectionButtonText() {
|
690 |
+
const addToCollectionBtn = document.getElementById('add-to-collection-btn');
|
691 |
+
const count = mode === 'language' ? languageCollectionCount : flashcardCollectionCount;
|
692 |
+
addToCollectionBtn.textContent = `Add to Collection (${count})`;
|
693 |
+
}
|
694 |
+
|
695 |
+
// Initialize collection counts and flashcards from localStorage
|
696 |
+
flashcardCollectionCount = parseInt(localStorage.getItem('flashcardCollectionCount')) || 0;
|
697 |
+
languageCollectionCount = parseInt(localStorage.getItem('languageCollectionCount')) || 0;
|
698 |
+
collectedFlashcards = JSON.parse(localStorage.getItem('collectedFlashcards')) || [];
|
699 |
+
collectedLanguageFlashcards = JSON.parse(localStorage.getItem('collectedLanguageFlashcards')) || [];
|
700 |
+
updateAddToCollectionButtonText();
|
701 |
+
|
702 |
+
document.getElementById('add-to-collection-btn').addEventListener('click', addToCollection);
|
703 |
+
|
704 |
+
function updateExportButtonVisibility() {
|
705 |
+
const exportButton = document.getElementById('export-csv-btn');
|
706 |
+
const currentCollection = mode === 'language' ? collectedLanguageFlashcards : collectedFlashcards;
|
707 |
+
exportButton.style.display = currentCollection.length > 0 ? 'block' : 'none';
|
708 |
+
}
|
709 |
+
|
710 |
+
function exportToCSV() {
|
711 |
+
let csvContent = "data:text/csv;charset=utf-8,";
|
712 |
+
const currentCollection = mode === 'language' ? collectedLanguageFlashcards : collectedFlashcards;
|
713 |
+
const removeQuotes = str => str.replace(/"/g, '');
|
714 |
+
|
715 |
+
if (mode === 'language') {
|
716 |
+
currentCollection.forEach(({ phrase, translationAnswer }) => {
|
717 |
+
const [translation, answer] = translationAnswer.split('\n');
|
718 |
+
csvContent += `${removeQuotes(phrase)};- ${removeQuotes(translation)}<br>- ${removeQuotes(answer)}\n`;
|
719 |
+
});
|
720 |
+
} else {
|
721 |
+
currentCollection.forEach(({ phrase, translationAnswer }) => {
|
722 |
+
csvContent += `${removeQuotes(phrase)};${removeQuotes(translationAnswer)}\n`;
|
723 |
+
});
|
724 |
+
}
|
725 |
+
|
726 |
+
const encodedUri = encodeURI(csvContent);
|
727 |
+
const link = document.createElement("a");
|
728 |
+
link.setAttribute("href", encodedUri);
|
729 |
+
link.setAttribute("download", `${mode}_flashcards.csv`);
|
730 |
+
document.body.appendChild(link);
|
731 |
+
link.click();
|
732 |
+
document.body.removeChild(link);
|
733 |
+
}
|
734 |
+
|
735 |
+
document.getElementById('export-csv-btn').addEventListener('click', exportToCSV);
|
736 |
+
|
737 |
+
function clearCollection() {
|
738 |
+
if (confirm('Are you sure you want to clear the entire collection? This action cannot be undone.')) {
|
739 |
+
if (mode === 'language') {
|
740 |
+
collectedLanguageFlashcards = [];
|
741 |
+
languageCollectionCount = 0;
|
742 |
+
localStorage.removeItem('collectedLanguageFlashcards');
|
743 |
+
localStorage.removeItem('languageCollectionCount');
|
744 |
+
} else {
|
745 |
+
collectedFlashcards = [];
|
746 |
+
flashcardCollectionCount = 0;
|
747 |
+
localStorage.removeItem('collectedFlashcards');
|
748 |
+
localStorage.removeItem('flashcardCollectionCount');
|
749 |
+
}
|
750 |
+
updateCollectionCount(0, mode);
|
751 |
+
updateExportButtonVisibility();
|
752 |
+
}
|
753 |
+
}
|
754 |
+
|
755 |
+
document.getElementById('clear-collection-btn').addEventListener('click', clearCollection);
|
756 |
+
|
757 |
+
// Initialize export button visibility
|
758 |
+
updateExportButtonVisibility();
|
759 |
+
|
760 |
+
function addRecentFile(filename) {
|
761 |
+
let recentFiles = JSON.parse(localStorage.getItem('recentFiles')) || [];
|
762 |
+
recentFiles = recentFiles.filter(file => file.filename !== filename);
|
763 |
+
recentFiles.unshift({ filename: filename, date: new Date().toISOString() });
|
764 |
+
recentFiles = recentFiles.slice(0, 5); // Keep only the 5 most recent
|
765 |
+
localStorage.setItem('recentFiles', JSON.stringify(recentFiles));
|
766 |
+
loadRecentFiles();
|
767 |
+
}
|
768 |
+
|
769 |
+
function updateRecentPDFsList() {
|
770 |
+
const recentPDFs = JSON.parse(localStorage.getItem('recentPDFs')) || [];
|
771 |
+
recentPdfList.innerHTML = '';
|
772 |
+
recentPDFs.forEach(pdf => {
|
773 |
+
const li = document.createElement('li');
|
774 |
+
li.textContent = `${pdf.filename} (${new Date(pdf.date).toLocaleDateString()})`;
|
775 |
+
recentPdfList.appendChild(li);
|
776 |
+
});
|
777 |
+
}
|
778 |
+
|
779 |
+
fileInput.addEventListener('change', function (e) {
|
780 |
+
const file = e.target.files[0];
|
781 |
+
if (file.type !== 'application/pdf' && file.type !== 'text/plain' && file.type !== 'application/epub+zip') {
|
782 |
+
console.error('Error: Not a PDF, TXT, or EPUB file');
|
783 |
+
return;
|
784 |
+
}
|
785 |
+
loadFile(file);
|
786 |
+
addRecentFile(file.name);
|
787 |
+
this.nextElementSibling.textContent = file.name;
|
788 |
+
});
|
789 |
+
|
790 |
+
// Add a span next to the file input to display the selected file name
|
791 |
+
const fileNameDisplay = document.createElement('span');
|
792 |
+
fileNameDisplay.style.marginLeft = '10px';
|
793 |
+
fileInput.parentNode.insertBefore(fileNameDisplay, fileInput.nextSibling);
|
794 |
+
|
795 |
+
function handleGoToPage() {
|
796 |
+
const pageInput = document.getElementById('page-input');
|
797 |
+
const pageNumber = parseInt(pageInput.value);
|
798 |
+
goToPage(pageNumber);
|
799 |
+
}
|
800 |
+
|
801 |
+
document.getElementById('go-to-page-btn').addEventListener('click', handleGoToPage);
|
802 |
+
|
803 |
+
document.getElementById('page-input').addEventListener('keyup', function (event) {
|
804 |
+
if (event.key === 'Enter') {
|
805 |
+
handleGoToPage();
|
806 |
+
}
|
807 |
+
});
|
808 |
+
|
809 |
+
function calculateZoomStep(currentScale) {
|
810 |
+
return Math.max(0.1, Math.min(0.25, currentScale * 0.1));
|
811 |
+
}
|
812 |
+
|
813 |
+
document.getElementById('zoom-in-btn').addEventListener('click', function() {
|
814 |
+
if (scale < maxScale) {
|
815 |
+
const step = calculateZoomStep(scale);
|
816 |
+
scale = Math.min(maxScale, scale + step);
|
817 |
+
reRenderPDF();
|
818 |
+
saveScaleForCurrentFile();
|
819 |
+
}
|
820 |
+
});
|
821 |
+
|
822 |
+
document.getElementById('zoom-out-btn').addEventListener('click', function() {
|
823 |
+
if (scale > minScale) {
|
824 |
+
const step = calculateZoomStep(scale);
|
825 |
+
scale = Math.max(minScale, scale - step);
|
826 |
+
reRenderPDF();
|
827 |
+
saveScaleForCurrentFile();
|
828 |
+
}
|
829 |
+
});
|
830 |
+
|
831 |
+
function reRenderPDF() {
|
832 |
+
pdfViewer.innerHTML = '';
|
833 |
+
renderPage(pageNum);
|
834 |
+
}
|
835 |
+
|
836 |
+
function saveScaleForCurrentFile() {
|
837 |
+
if (currentFileName) {
|
838 |
+
localStorage.setItem(`scale_${currentFileName}`, scale);
|
839 |
+
}
|
840 |
+
}
|
841 |
+
|
842 |
+
function loadScaleForCurrentFile() {
|
843 |
+
if (currentFileName) {
|
844 |
+
const savedScale = localStorage.getItem(`scale_${currentFileName}`);
|
845 |
+
if (savedScale) {
|
846 |
+
scale = parseFloat(savedScale);
|
847 |
+
}
|
848 |
+
}
|
849 |
+
}
|
850 |
+
|
851 |
+
const modeButtons = document.querySelectorAll('.mode-btn');
|
852 |
+
modeButtons.forEach(button => {
|
853 |
+
button.addEventListener('click', function () {
|
854 |
+
modeButtons.forEach(btn => btn.classList.remove('selected'));
|
855 |
+
this.classList.add('selected');
|
856 |
+
mode = this.dataset.mode;
|
857 |
+
pdfViewer.style.cursor = mode === 'language' ? 'text' : 'default';
|
858 |
+
document.getElementById('language-buttons').style.display = mode === 'language' ? 'flex' : 'none';
|
859 |
+
systemPrompt.style.display = mode === 'flashcard' ? 'block' : 'none';
|
860 |
+
document.getElementById('explain-prompt').style.display = mode === 'explain' ? 'block' : 'none';
|
861 |
+
document.getElementById('language-prompt').style.display = mode === 'language' ? 'block' : 'none';
|
862 |
+
submitBtn.style.display = mode === 'language' ? 'none' : 'block';
|
863 |
+
submitBtn.textContent = mode === 'flashcard' ? 'Generate Flashcards' : 'Generate Explanation';
|
864 |
+
|
865 |
+
if (mode === 'language') {
|
866 |
+
const savedLanguage = loadLanguageChoice();
|
867 |
+
setLanguageButton(savedLanguage);
|
868 |
+
}
|
869 |
+
|
870 |
+
// Update Add to Collection button and export button visibility
|
871 |
+
updateAddToCollectionButtonText();
|
872 |
+
updateExportButtonVisibility();
|
873 |
+
});
|
874 |
+
});
|
875 |
+
|
876 |
+
const languageButtons = document.querySelectorAll('#language-buttons .mode-btn');
|
877 |
+
languageButtons.forEach(button => {
|
878 |
+
button.addEventListener('click', function (event) {
|
879 |
+
event.preventDefault();
|
880 |
+
languageButtons.forEach(btn => btn.classList.remove('selected'));
|
881 |
+
this.classList.add('selected');
|
882 |
+
const targetLanguage = this.dataset.language;
|
883 |
+
saveLanguageChoice(targetLanguage);
|
884 |
+
// Ensure the Language mode button remains selected
|
885 |
+
document.querySelector('.mode-btn[data-mode="language"]').classList.add('selected');
|
886 |
+
// Keep language buttons visible and Generate button hidden
|
887 |
+
document.getElementById('language-buttons').style.display = 'flex';
|
888 |
+
submitBtn.style.display = 'none';
|
889 |
+
// Set the mode to 'language'
|
890 |
+
mode = 'language';
|
891 |
+
});
|
892 |
+
});
|
893 |
+
|
894 |
+
let highlights = [];
|
895 |
+
|
896 |
+
function attachLanguageModeListener(container) {
|
897 |
+
container.addEventListener('mouseup', function (event) {
|
898 |
+
if (event.altKey) {
|
899 |
+
const selection = window.getSelection();
|
900 |
+
if (selection.rangeCount > 0) {
|
901 |
+
const range = selection.getRangeAt(0);
|
902 |
+
const selectedText = selection.toString().trim();
|
903 |
+
|
904 |
+
console.log(selectedText);
|
905 |
+
|
906 |
+
if (selectedText !== '') {
|
907 |
+
console.log(range, container);
|
908 |
+
const highlight = createHighlight(range, container);
|
909 |
+
highlights.push(highlight);
|
910 |
+
saveHighlights();
|
911 |
+
}
|
912 |
+
}
|
913 |
+
}
|
914 |
+
});
|
915 |
+
|
916 |
+
container.addEventListener('dblclick', function (event) {
|
917 |
+
if (mode === 'language') {
|
918 |
+
const selection = window.getSelection();
|
919 |
+
const range = selection.getRangeAt(0);
|
920 |
+
const word = selection.toString().trim();
|
921 |
+
|
922 |
+
if (word !== '' && word.length < 20) {
|
923 |
+
// Highlight the selected word
|
924 |
+
const span = document.createElement('span');
|
925 |
+
span.style.backgroundColor = 'rgba(255, 255, 0, 0.5)';
|
926 |
+
span.textContent = word;
|
927 |
+
range.deleteContents();
|
928 |
+
range.insertNode(span);
|
929 |
+
|
930 |
+
const selectedLanguageButton = document.querySelector('#language-buttons .mode-btn.selected');
|
931 |
+
if (selectedLanguageButton) {
|
932 |
+
const targetLanguage = selectedLanguageButton.dataset.language;
|
933 |
+
const phrase = getPhrase(range, word);
|
934 |
+
generateLanguageFlashcard(word, phrase, targetLanguage);
|
935 |
+
speakWord(word);
|
936 |
+
} else {
|
937 |
+
console.error('No language selected');
|
938 |
+
}
|
939 |
+
}
|
940 |
+
}
|
941 |
+
});
|
942 |
+
}
|
943 |
+
|
944 |
+
function createHighlight(range, pageDiv) {
|
945 |
+
const highlight = document.createElement('div');
|
946 |
+
highlight.className = 'highlight';
|
947 |
+
highlight.style.position = 'absolute';
|
948 |
+
highlight.style.backgroundColor = 'rgba(255, 255, 0, 0.3)';
|
949 |
+
highlight.style.pointerEvents = 'none';
|
950 |
+
|
951 |
+
const rect = range.getBoundingClientRect();
|
952 |
+
const pageBounds = pageDiv.getBoundingClientRect();
|
953 |
+
|
954 |
+
highlight.style.left = (rect.left - pageBounds.left) + 'px';
|
955 |
+
highlight.style.top = (rect.top - pageBounds.top) + 'px';
|
956 |
+
highlight.style.width = rect.width + 'px';
|
957 |
+
highlight.style.height = rect.height + 'px';
|
958 |
+
|
959 |
+
pageDiv.appendChild(highlight);
|
960 |
+
|
961 |
+
return {
|
962 |
+
element: highlight,
|
963 |
+
pageNumber: parseInt(pageDiv.dataset.pageNumber),
|
964 |
+
rect: {
|
965 |
+
left: rect.left - pageBounds.left,
|
966 |
+
top: rect.top - pageBounds.top,
|
967 |
+
width: rect.width,
|
968 |
+
height: rect.height
|
969 |
+
}
|
970 |
+
};
|
971 |
+
}
|
972 |
+
|
973 |
+
function saveHighlights() {
|
974 |
+
localStorage.setItem('pdfHighlights', JSON.stringify(highlights));
|
975 |
+
}
|
976 |
+
|
977 |
+
function loadHighlights() {
|
978 |
+
const savedHighlights = JSON.parse(localStorage.getItem('pdfHighlights')) || [];
|
979 |
+
highlights = savedHighlights;
|
980 |
+
renderHighlights();
|
981 |
+
}
|
982 |
+
|
983 |
+
function renderHighlights() {
|
984 |
+
highlights.forEach(highlight => {
|
985 |
+
const pageDiv = document.querySelector(`.page[data-page-number="${highlight.pageNumber}"]`);
|
986 |
+
if (pageDiv) {
|
987 |
+
const newHighlight = document.createElement('div');
|
988 |
+
newHighlight.className = 'highlight';
|
989 |
+
newHighlight.style.position = 'absolute';
|
990 |
+
newHighlight.style.backgroundColor = 'rgba(255, 255, 0, 0.3)';
|
991 |
+
newHighlight.style.pointerEvents = 'none';
|
992 |
+
|
993 |
+
const pageBounds = pageDiv.getBoundingClientRect();
|
994 |
+
const scale = parseFloat(pageDiv.style.width) / pageBounds.width;
|
995 |
+
|
996 |
+
highlight.rects.forEach(rect => {
|
997 |
+
const highlightRect = document.createElement('div');
|
998 |
+
highlightRect.style.position = 'absolute';
|
999 |
+
highlightRect.style.left = (rect.left * scale) + 'px';
|
1000 |
+
highlightRect.style.top = (rect.top * scale) + 'px';
|
1001 |
+
highlightRect.style.width = (rect.width * scale) + 'px';
|
1002 |
+
highlightRect.style.height = (rect.height * scale) + 'px';
|
1003 |
+
highlightRect.style.backgroundColor = 'inherit';
|
1004 |
+
newHighlight.appendChild(highlightRect);
|
1005 |
+
});
|
1006 |
+
|
1007 |
+
pageDiv.appendChild(newHighlight);
|
1008 |
+
}
|
1009 |
+
});
|
1010 |
+
}
|
1011 |
+
|
1012 |
+
function getPhrase(range, word) {
|
1013 |
+
let startNode = range.startContainer;
|
1014 |
+
let endNode = range.endContainer;
|
1015 |
+
let startOffset = Math.max(0, range.startOffset - 50);
|
1016 |
+
let endOffset = Math.min(endNode.length, range.endOffset + 50);
|
1017 |
+
|
1018 |
+
// Extract the phrase
|
1019 |
+
let phrase = '';
|
1020 |
+
let currentNode = startNode;
|
1021 |
+
while (currentNode) {
|
1022 |
+
if (currentNode.nodeType === Node.TEXT_NODE) {
|
1023 |
+
const text = currentNode.textContent;
|
1024 |
+
const start = currentNode === startNode ? startOffset : 0;
|
1025 |
+
const end = currentNode === endNode ? endOffset : text.length;
|
1026 |
+
phrase += text.slice(start, end);
|
1027 |
+
}
|
1028 |
+
if (currentNode === endNode) break;
|
1029 |
+
currentNode = currentNode.nextSibling;
|
1030 |
+
}
|
1031 |
+
|
1032 |
+
// Ensure the word is bolded in the phrase
|
1033 |
+
const wordRegex = new RegExp(`\\b${word}\\b`, 'gi');
|
1034 |
+
phrase = phrase.replace(wordRegex, `<b>$&</b>`);
|
1035 |
+
|
1036 |
+
return phrase.trim();
|
1037 |
+
}
|
1038 |
+
|
1039 |
+
function saveLanguageChoice(language) {
|
1040 |
+
localStorage.setItem('selectedLanguage', language);
|
1041 |
+
}
|
1042 |
+
|
1043 |
+
function loadLanguageChoice() {
|
1044 |
+
return localStorage.getItem('selectedLanguage') || 'English';
|
1045 |
+
}
|
1046 |
+
|
1047 |
+
function setLanguageButton(language) {
|
1048 |
+
const languageButton = document.querySelector(`#language-buttons .mode-btn[data-language="${language}"]`);
|
1049 |
+
if (languageButton) {
|
1050 |
+
languageButtons.forEach(btn => btn.classList.remove('selected'));
|
1051 |
+
languageButton.classList.add('selected');
|
1052 |
+
}
|
1053 |
+
}
|
1054 |
+
|
1055 |
+
submitBtn.addEventListener('click', generateContent);
|
1056 |
+
|
1057 |
+
apiKeyInput.addEventListener('change', function () {
|
1058 |
+
apiKey = this.value;
|
1059 |
+
localStorage.setItem('lastWorkingAPIKey', apiKey);
|
1060 |
+
});
|
1061 |
+
|
1062 |
+
// Load last working API key
|
1063 |
+
const lastWorkingAPIKey = localStorage.getItem('lastWorkingAPIKey');
|
1064 |
+
if (lastWorkingAPIKey) {
|
1065 |
+
apiKeyInput.value = lastWorkingAPIKey;
|
1066 |
+
apiKey = lastWorkingAPIKey;
|
1067 |
+
}
|
1068 |
+
|
1069 |
+
// Infinite scrolling
|
1070 |
+
document.getElementById('left-panel').addEventListener('scroll', function () {
|
1071 |
+
if (this.scrollTop + this.clientHeight >= this.scrollHeight - 500) {
|
1072 |
+
if (pageNum < pdfDoc.numPages) {
|
1073 |
+
pageNum++;
|
1074 |
+
renderPage(pageNum);
|
1075 |
+
}
|
1076 |
+
}
|
1077 |
+
});
|
1078 |
+
|
1079 |
+
function loadRecentFiles() {
|
1080 |
+
fetch('/get_recent_files')
|
1081 |
+
.then(response => response.json())
|
1082 |
+
.then(recentFiles => {
|
1083 |
+
const fileList = document.getElementById('file-list');
|
1084 |
+
fileList.innerHTML = '';
|
1085 |
+
recentFiles.forEach(file => {
|
1086 |
+
const li = document.createElement('li');
|
1087 |
+
const a = document.createElement('a');
|
1088 |
+
a.href = '#';
|
1089 |
+
a.textContent = `${file.filename} (${new Date(file.date).toLocaleDateString()})`;
|
1090 |
+
a.addEventListener('click', function (e) {
|
1091 |
+
e.preventDefault();
|
1092 |
+
fetch(`/open_pdf/${file.filename}`)
|
1093 |
+
.then(response => response.blob())
|
1094 |
+
.then(blob => {
|
1095 |
+
const fileType = file.filename.toLowerCase().endsWith('.pdf') ? 'application/pdf' : 'text/plain';
|
1096 |
+
const newFile = new File([blob], file.filename, { type: fileType });
|
1097 |
+
loadFile(newFile);
|
1098 |
+
})
|
1099 |
+
.catch(error => console.error('Error:', error));
|
1100 |
+
});
|
1101 |
+
li.appendChild(a);
|
1102 |
+
fileList.appendChild(li);
|
1103 |
+
});
|
1104 |
+
})
|
1105 |
+
.catch(error => console.error('Error loading recent files:', error));
|
1106 |
+
}
|
1107 |
+
|
1108 |
+
// Call loadRecentFiles when the page loads
|
1109 |
+
window.addEventListener('load', loadRecentFiles);
|
1110 |
+
|
1111 |
+
// Update recent files list after uploading a new file
|
1112 |
+
function uploadFile(file) {
|
1113 |
+
const formData = new FormData();
|
1114 |
+
formData.append('file', file);
|
1115 |
+
|
1116 |
+
fetch('/upload_pdf', {
|
1117 |
+
method: 'POST',
|
1118 |
+
body: formData
|
1119 |
+
})
|
1120 |
+
.then(response => response.json())
|
1121 |
+
.then(data => {
|
1122 |
+
if (data.message) {
|
1123 |
+
console.log(data.message);
|
1124 |
+
loadFile(file);
|
1125 |
+
loadRecentFiles(); // Reload the recent files list
|
1126 |
+
} else {
|
1127 |
+
console.error(data.error);
|
1128 |
+
}
|
1129 |
+
})
|
1130 |
+
.catch(error => {
|
1131 |
+
console.error('Error:', error);
|
1132 |
+
});
|
1133 |
+
}
|
1134 |
+
|
1135 |
+
// Update loadFile function to reload recent files list
|
1136 |
+
let book;
|
1137 |
+
let rendition;
|
1138 |
+
let currentScale = 100;
|
1139 |
+
|
1140 |
+
function loadFile(file) {
|
1141 |
+
const pdfViewer = document.getElementById('pdf-viewer');
|
1142 |
+
const epubViewer = document.getElementById('epub-viewer');
|
1143 |
+
|
1144 |
+
// Hide both viewers initially
|
1145 |
+
pdfViewer.style.display = 'none';
|
1146 |
+
epubViewer.style.display = 'none';
|
1147 |
+
|
1148 |
+
if (file.name.endsWith('.pdf')) {
|
1149 |
+
pdfViewer.style.display = 'block';
|
1150 |
+
loadPDF(file);
|
1151 |
+
} else if (file.name.endsWith('.txt')) {
|
1152 |
+
pdfViewer.style.display = 'block'; // Assuming TXT files use the PDF viewer
|
1153 |
+
loadTXT(file);
|
1154 |
+
} else if (file.name.endsWith('.epub')) {
|
1155 |
+
epubViewer.style.display = 'block';
|
1156 |
+
loadEPUB(file);
|
1157 |
+
}
|
1158 |
+
}
|
1159 |
+
|
1160 |
+
function loadEPUB(file) {
|
1161 |
+
console.log('loadEPUB function called with file:', file.name);
|
1162 |
+
|
1163 |
+
const epubContainer = document.getElementById('epub-viewer');
|
1164 |
+
if (!epubContainer) {
|
1165 |
+
console.error('EPUB viewer container not found');
|
1166 |
+
return;
|
1167 |
+
}
|
1168 |
+
|
1169 |
+
epubContainer.innerHTML = ''; // Clear previous content
|
1170 |
+
epubContainer.style.display = 'block';
|
1171 |
+
|
1172 |
+
const reader = new FileReader();
|
1173 |
+
|
1174 |
+
reader.onload = function(e) {
|
1175 |
+
console.log('FileReader onload event fired');
|
1176 |
+
const arrayBuffer = e.target.result;
|
1177 |
+
|
1178 |
+
try {
|
1179 |
+
book = ePub(arrayBuffer);
|
1180 |
+
console.log('EPUB book object created:', book);
|
1181 |
+
|
1182 |
+
book.ready.then(() => {
|
1183 |
+
console.log('EPUB book is ready');
|
1184 |
+
|
1185 |
+
rendition = book.renderTo('epub-viewer', {
|
1186 |
+
width: '100%',
|
1187 |
+
height: '100%',
|
1188 |
+
spread: 'always',
|
1189 |
+
sandbox: 'allow-scripts'
|
1190 |
+
});
|
1191 |
+
|
1192 |
+
console.log('Rendition object created:', rendition);
|
1193 |
+
|
1194 |
+
rendition.display().then(() => {
|
1195 |
+
console.log('EPUB content displayed');
|
1196 |
+
setupNavigation();
|
1197 |
+
}).catch(error => {
|
1198 |
+
console.error('Error displaying EPUB content:', error);
|
1199 |
+
epubContainer.innerHTML = 'Error displaying EPUB content. Please check console for details.';
|
1200 |
+
});
|
1201 |
+
|
1202 |
+
if (document.getElementById('pdf-viewer')) {
|
1203 |
+
document.getElementById('pdf-viewer').style.display = 'none';
|
1204 |
+
}
|
1205 |
+
|
1206 |
+
}).catch(error => {
|
1207 |
+
console.error('Error in book.ready:', error);
|
1208 |
+
epubContainer.innerHTML = 'Error preparing EPUB. Please check console for details.';
|
1209 |
+
});
|
1210 |
+
} catch (error) {
|
1211 |
+
console.error('Error creating EPUB book object:', error);
|
1212 |
+
epubContainer.innerHTML = 'Error loading EPUB. Please check console for details.';
|
1213 |
+
}
|
1214 |
+
};
|
1215 |
+
|
1216 |
+
reader.onerror = function(e) {
|
1217 |
+
console.error('Error reading file:', e);
|
1218 |
+
epubContainer.innerHTML = 'Error reading file. Please try again.';
|
1219 |
+
};
|
1220 |
+
|
1221 |
+
reader.readAsArrayBuffer(file);
|
1222 |
+
}
|
1223 |
+
|
1224 |
+
function setupNavigation() {
|
1225 |
+
const prevBtn = document.getElementById('prev-btn');
|
1226 |
+
const nextBtn = document.getElementById('next-btn');
|
1227 |
+
const zoomInBtn = document.getElementById('zoom-in-btn');
|
1228 |
+
const zoomOutBtn = document.getElementById('zoom-out-btn');
|
1229 |
+
|
1230 |
+
if (prevBtn) prevBtn.onclick = prevPage;
|
1231 |
+
if (nextBtn) nextBtn.onclick = nextPage;
|
1232 |
+
if (zoomInBtn) zoomInBtn.onclick = zoomIn;
|
1233 |
+
if (zoomOutBtn) zoomOutBtn.onclick = zoomOut;
|
1234 |
+
|
1235 |
+
// Enable keyboard navigation
|
1236 |
+
document.addEventListener('keydown', handleKeyPress);
|
1237 |
+
}
|
1238 |
+
|
1239 |
+
function prevPage() {
|
1240 |
+
if (rendition) rendition.prev();
|
1241 |
+
}
|
1242 |
+
|
1243 |
+
function nextPage() {
|
1244 |
+
if (rendition) rendition.next();
|
1245 |
+
}
|
1246 |
+
|
1247 |
+
function zoomIn() {
|
1248 |
+
if (rendition) {
|
1249 |
+
currentScale += 10;
|
1250 |
+
setZoom();
|
1251 |
+
}
|
1252 |
+
}
|
1253 |
+
|
1254 |
+
function zoomOut() {
|
1255 |
+
if (rendition) {
|
1256 |
+
currentScale -= 10;
|
1257 |
+
if (currentScale < 50) currentScale = 50; // Prevent zooming out too much
|
1258 |
+
setZoom();
|
1259 |
+
}
|
1260 |
+
}
|
1261 |
+
|
1262 |
+
function setZoom() {
|
1263 |
+
if (rendition) {
|
1264 |
+
rendition.themes.fontSize(`${currentScale}%`);
|
1265 |
+
}
|
1266 |
+
}
|
1267 |
+
|
1268 |
+
function handleKeyPress(e) {
|
1269 |
+
switch(e.key) {
|
1270 |
+
case "ArrowLeft":
|
1271 |
+
prevPage();
|
1272 |
+
break;
|
1273 |
+
case "ArrowRight":
|
1274 |
+
nextPage();
|
1275 |
+
break;
|
1276 |
+
}
|
1277 |
+
}
|
1278 |
+
|
1279 |
+
// Save current page before unloading
|
1280 |
+
window.addEventListener('beforeunload', function () {
|
1281 |
+
if (currentFileName) {
|
1282 |
+
localStorage.setItem(`lastPage_${currentFileName}`, pageNum);
|
1283 |
+
}
|
1284 |
+
});
|
1285 |
+
|
1286 |
+
// Initialize recent PDFs list
|
1287 |
+
window.onload = function () {
|
1288 |
+
loadRecentFiles();
|
1289 |
+
|
1290 |
+
// Add event listener for settings icon
|
1291 |
+
document.getElementById('settings-icon').addEventListener('click', function () {
|
1292 |
+
const settingsPanel = document.getElementById('settings-panel');
|
1293 |
+
settingsPanel.style.display = settingsPanel.style.display === 'none' ? 'block' : 'none';
|
1294 |
+
});
|
1295 |
+
|
1296 |
+
// Set default language to English if not already set
|
1297 |
+
if (!localStorage.getItem('selectedLanguage')) {
|
1298 |
+
saveLanguageChoice('English');
|
1299 |
+
}
|
1300 |
+
|
1301 |
+
// Load and set the saved language choice
|
1302 |
+
const savedLanguage = loadLanguageChoice();
|
1303 |
+
setLanguageButton(savedLanguage);
|
1304 |
+
};
|
1305 |
+
|
1306 |
+
fileInput.addEventListener('change', function (e) {
|
1307 |
+
const file = e.target.files[0];
|
1308 |
+
if (file.type !== 'application/pdf' && file.type !== 'text/plain' && file.type !== 'application/epub+zip') {
|
1309 |
+
console.error('Error: Not a PDF, TXT, or EPUB file');
|
1310 |
+
return;
|
1311 |
+
}
|
1312 |
+
uploadFile(file);
|
1313 |
+
});
|
1314 |
+
|
1315 |
+
function uploadFile(file) {
|
1316 |
+
const formData = new FormData();
|
1317 |
+
formData.append('file', file);
|
1318 |
+
|
1319 |
+
fetch('/upload_file', {
|
1320 |
+
method: 'POST',
|
1321 |
+
body: formData
|
1322 |
+
})
|
1323 |
+
.then(response => response.json())
|
1324 |
+
.then(data => {
|
1325 |
+
if (data.message) {
|
1326 |
+
console.log(data.message);
|
1327 |
+
loadFile(file);
|
1328 |
+
loadRecentFiles();
|
1329 |
+
addRecentFile(file.name);
|
1330 |
+
} else {
|
1331 |
+
console.error(data.error);
|
1332 |
+
}
|
1333 |
+
})
|
1334 |
+
.catch(error => {
|
1335 |
+
console.error('Error:', error);
|
1336 |
+
});
|
1337 |
+
}
|
1338 |
+
</script>
|
1339 |
+
</body>
|
1340 |
+
|
1341 |
+
</html>
|