LE Quoc Dat commited on
Commit
417ad57
1 Parent(s): 9601828
Files changed (9) hide show
  1. .gitignore +16 -0
  2. Dockerfile +20 -0
  3. LICENSE +21 -0
  4. README.md +71 -11
  5. app.py +136 -0
  6. requirements.txt +3 -0
  7. static/css/styles.css +500 -0
  8. static/favicon.ico +0 -0
  9. 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
- title: Pdf Flashcards Autogen
3
- emoji: 🏃
4
- colorFrom: blue
5
- colorTo: purple
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
+ # 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">&times;</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>