LE Quoc Dat commited on
Commit
2ecced5
·
1 Parent(s): 16b33c2
Files changed (6) hide show
  1. app.py +12 -5
  2. llm_utils.py +27 -15
  3. static/js/main.js +0 -222
  4. static/js/models.js +14 -0
  5. static/js/prompts.js +44 -0
  6. templates/index.html +77 -86
app.py CHANGED
@@ -5,6 +5,7 @@ import json
5
  from datetime import datetime
6
  import base64
7
  from llm_utils import generate_completion
 
8
 
9
  app = Flask(__name__)
10
 
@@ -68,18 +69,24 @@ def generate_flashcard():
68
  data = request.json
69
  prompt = data['prompt']
70
  mode = data.get('mode', 'flashcard')
 
71
 
72
  try:
73
- # Use llm_utils to generate completion
74
- content = generate_completion(prompt)
75
  print(prompt)
76
  print(content)
77
 
78
  if mode == 'language':
79
  try:
80
- # Expecting a JSON object with "word", "translation", "question", "answer"
81
- flashcard = json.loads(content)
82
- return jsonify({'flashcard': flashcard})
 
 
 
 
 
83
  except Exception as parse_err:
84
  return jsonify({'error': 'JSON parsing error in language mode: ' + str(parse_err)})
85
  elif mode == 'flashcard':
 
5
  from datetime import datetime
6
  import base64
7
  from llm_utils import generate_completion
8
+ import re
9
 
10
  app = Flask(__name__)
11
 
 
69
  data = request.json
70
  prompt = data['prompt']
71
  mode = data.get('mode', 'flashcard')
72
+ model = data.get('model')
73
 
74
  try:
75
+ # Use llm_utils to generate completion with the selected model
76
+ content = generate_completion(prompt, model=model)
77
  print(prompt)
78
  print(content)
79
 
80
  if mode == 'language':
81
  try:
82
+ # Extract the JSON substring from the content in case there is extra text.
83
+ json_match = re.search(r'\{[\s\S]*\}', content)
84
+ if json_match:
85
+ json_text = json_match.group(0)
86
+ flashcard = json.loads(json_text)
87
+ return jsonify({'flashcard': flashcard})
88
+ else:
89
+ raise ValueError("No JSON object found in response")
90
  except Exception as parse_err:
91
  return jsonify({'error': 'JSON parsing error in language mode: ' + str(parse_err)})
92
  elif mode == 'flashcard':
llm_utils.py CHANGED
@@ -6,33 +6,45 @@ def load_model_config():
6
  with open('models.yaml', 'r') as file:
7
  return yaml.safe_load(file)
8
 
9
- def generate_completion(prompt: str, api_key: str = None) -> str:
10
  """
11
- Generate completion using LiteLLM with the configured model
12
 
13
  Args:
14
- prompt (str): The input prompt
15
- api_key (str, optional): Override API key. If not provided, will use environment variable
 
16
 
17
  Returns:
18
- str: The generated completion text
19
  """
20
  config = load_model_config()
21
-
22
- # Get the first environment variable and its models
23
- first_env_var = list(config['models'][0].keys())[0]
24
- model_name = config['models'][0][first_env_var][0]
25
-
26
- # If no API key provided, get from environment
 
 
 
 
 
 
 
 
 
 
 
27
  if api_key is None:
28
- api_key = os.getenv(first_env_var)
29
- if not api_key:
30
- raise ValueError(f"Please set {first_env_var} environment variable")
31
 
32
  messages = [{"role": "user", "content": prompt}]
33
 
34
  response = completion(
35
- model=model_name,
36
  messages=messages,
37
  api_key=api_key
38
  )
 
6
  with open('models.yaml', 'r') as file:
7
  return yaml.safe_load(file)
8
 
9
+ def generate_completion(prompt: str, model: str = None, api_key: str = None) -> str:
10
  """
11
+ Generate completion using LiteLLM with the configured model.
12
 
13
  Args:
14
+ prompt (str): The input prompt.
15
+ model (str, optional): The model to use (if not provided, default is used).
16
+ api_key (str, optional): Override API key. If not provided, will use environment variable.
17
 
18
  Returns:
19
+ str: The generated completion text.
20
  """
21
  config = load_model_config()
22
+
23
+ # Build a mapping of model to the required API key environment variable.
24
+ model_to_env = {}
25
+ for item in config['models']:
26
+ for env_key, models in item.items():
27
+ for m in models:
28
+ model_to_env[m] = env_key
29
+
30
+ # Use the default model from the first configuration if none provided.
31
+ if model is None:
32
+ first_env_var = list(config['models'][0].keys())[0]
33
+ model = config['models'][0][first_env_var][0]
34
+
35
+ env_var = model_to_env.get(model)
36
+ if not env_var:
37
+ raise ValueError("Model is not supported.")
38
+
39
  if api_key is None:
40
+ api_key = os.getenv(env_var)
41
+ if not api_key:
42
+ raise ValueError(f"Please set {env_var} environment variable")
43
 
44
  messages = [{"role": "user", "content": prompt}]
45
 
46
  response = completion(
47
+ model=model,
48
  messages=messages,
49
  api_key=api_key
50
  )
static/js/main.js DELETED
@@ -1,222 +0,0 @@
1
- import { PROMPTS } from './prompts.js';
2
-
3
- // DOM elements
4
- const fileInput = document.getElementById('file-input');
5
- const pdfViewer = document.getElementById('pdf-viewer');
6
- const epubViewer = document.getElementById('epub-viewer');
7
- const modeToggle = document.getElementById('mode-toggle');
8
- const systemPrompt = document.getElementById('system-prompt');
9
- const explainPrompt = document.getElementById('explain-prompt');
10
- const languagePrompt = document.getElementById('language-prompt');
11
- const submitBtn = document.getElementById('submit-btn');
12
- const flashcardsContainer = document.getElementById('flashcards');
13
- const apiKeyInput = document.getElementById('api-key-input');
14
- const modelSelect = document.getElementById('model-select');
15
- const recentPdfList = document.getElementById('file-list');
16
-
17
- // State variables
18
- let pdfDoc = null;
19
- let pageNum = 1;
20
- let pageRendering = false;
21
- let pageNumPending = null;
22
- let scale = 3;
23
- const minScale = 0.5;
24
- const maxScale = 5;
25
- let mode = 'flashcard';
26
- let apiKey = '';
27
- let currentFileName = '';
28
- let currentPage = 1;
29
- let selectedModel = 'claude-3-haiku-20240307';
30
- let lastProcessedQuery = '';
31
- let lastRequestTime = 0;
32
- const cooldownTime = 1000; // 1 second cooldown
33
- let book;
34
- let rendition;
35
- let currentScaleEPUB = 100;
36
- let highlights = [];
37
- let flashcardCollectionCount = 0;
38
- let languageCollectionCount = 0;
39
- let collectedFlashcards = [];
40
- let collectedLanguageFlashcards = [];
41
- let voices = [];
42
-
43
- // Initialize on DOM load
44
- document.addEventListener('DOMContentLoaded', () => {
45
- // Set default prompts
46
- systemPrompt.value = PROMPTS.flashcard;
47
- explainPrompt.value = PROMPTS.explain;
48
- languagePrompt.value = PROMPTS.language;
49
-
50
- // Load last working API key
51
- const lastWorkingAPIKey = localStorage.getItem('lastWorkingAPIKey');
52
- if (lastWorkingAPIKey) {
53
- apiKeyInput.value = lastWorkingAPIKey;
54
- apiKey = lastWorkingAPIKey;
55
- }
56
-
57
- // Initialize collection counts and flashcards
58
- flashcardCollectionCount = parseInt(localStorage.getItem('flashcardCollectionCount')) || 0;
59
- languageCollectionCount = parseInt(localStorage.getItem('languageCollectionCount')) || 0;
60
- collectedFlashcards = JSON.parse(localStorage.getItem('collectedFlashcards')) || [];
61
- collectedLanguageFlashcards = JSON.parse(localStorage.getItem('collectedLanguageFlashcards')) || [];
62
- updateAddToCollectionButtonText();
63
- updateExportButtonVisibility();
64
-
65
- // Load recent files
66
- loadRecentFiles();
67
-
68
- // Set up event listeners
69
- setupEventListeners();
70
- populateVoiceList();
71
- initializeMode();
72
- });
73
-
74
- // Setup event listeners
75
- function setupEventListeners() {
76
- fileInput.addEventListener('change', handleFileChange);
77
- apiKeyInput.addEventListener('change', () => {
78
- apiKey = apiKeyInput.value;
79
- localStorage.setItem('lastWorkingAPIKey', apiKey);
80
- });
81
- modelSelect.addEventListener('change', () => {
82
- selectedModel = modelSelect.value;
83
- });
84
- submitBtn.addEventListener('click', generateContent);
85
- document.getElementById('add-to-collection-btn').addEventListener('click', addToCollection);
86
- document.getElementById('clear-collection-btn').addEventListener('click', clearCollection);
87
- document.getElementById('export-csv-btn').addEventListener('click', exportToCSV);
88
- document.getElementById('go-to-page-btn').addEventListener('click', handleGoToPage);
89
- document.getElementById('page-input').addEventListener('keyup', (e) => {
90
- if (e.key === 'Enter') handleGoToPage();
91
- });
92
- document.getElementById('zoom-in-btn').addEventListener('click', handleZoomIn);
93
- document.getElementById('zoom-out-btn').addEventListener('click', handleZoomOut);
94
- document.getElementById('settings-icon').addEventListener('click', () => {
95
- const settingsPanel = document.getElementById('settings-panel');
96
- settingsPanel.style.display = settingsPanel.style.display === 'none' ? 'block' : 'none';
97
- });
98
- document.getElementById('left-panel').addEventListener('scroll', handleScroll);
99
- setupModeButtons();
100
- setupLanguageButtons();
101
- }
102
-
103
- // Initialize mode
104
- function initializeMode() {
105
- mode = 'language';
106
- document.querySelector('.mode-btn[data-mode="language"]').classList.add('selected');
107
- document.getElementById('language-buttons').style.display = 'flex';
108
- submitBtn.style.display = 'none';
109
- systemPrompt.style.display = 'none';
110
- explainPrompt.style.display = 'none';
111
- languagePrompt.style.display = 'block';
112
- const savedLanguage = loadLanguageChoice() || 'English';
113
- setLanguageButton(savedLanguage);
114
- }
115
-
116
- // File handling
117
- function handleFileChange(e) {
118
- const file = e.target.files[0];
119
- if (!['application/pdf', 'text/plain', 'application/epub+zip'].includes(file.type)) {
120
- console.error('Error: Not a PDF, TXT, or EPUB file');
121
- return;
122
- }
123
- uploadFile(file);
124
- }
125
-
126
- function uploadFile(file) {
127
- const formData = new FormData();
128
- formData.append('file', file);
129
- fetch('/upload_file', {
130
- method: 'POST',
131
- body: formData
132
- })
133
- .then(response => response.json())
134
- .then(data => {
135
- if (data.message) {
136
- loadFile(file);
137
- loadRecentFiles();
138
- addRecentFile(file.name);
139
- } else {
140
- console.error(data.error);
141
- }
142
- })
143
- .catch(error => console.error('Error:', error));
144
- }
145
-
146
- function loadFile(file) {
147
- pdfViewer.style.display = 'none';
148
- epubViewer.style.display = 'none';
149
- if (file.name.endsWith('.pdf')) {
150
- pdfViewer.style.display = 'block';
151
- loadPDF(file);
152
- } else if (file.name.endsWith('.txt')) {
153
- pdfViewer.style.display = 'block';
154
- loadTXT(file);
155
- } else if (file.name.endsWith('.epub')) {
156
- epubViewer.style.display = 'block';
157
- loadEPUB(file);
158
- }
159
- }
160
-
161
- // PDF handling (broken down for readability)
162
- async function loadPDF(file) {
163
- const arrayBuffer = await readFileAsArrayBuffer(file);
164
- pdfDoc = await pdfjsLib.getDocument(arrayBuffer).promise;
165
- pdfViewer.innerHTML = '';
166
- currentFileName = file.name;
167
- const lastPage = localStorage.getItem(`lastPage_${currentFileName}`);
168
- pageNum = lastPage ? Math.max(parseInt(lastPage) - 2, 1) : 1;
169
- loadScaleForCurrentFile();
170
- renderPage(pageNum);
171
- updateCurrentPage(pageNum);
172
- hideHeaderPanel();
173
- loadHighlights();
174
- }
175
-
176
- function readFileAsArrayBuffer(file) {
177
- return new Promise((resolve, reject) => {
178
- const reader = new FileReader();
179
- reader.onload = () => resolve(new Uint8Array(reader.result));
180
- reader.onerror = reject;
181
- reader.readAsArrayBuffer(file);
182
- });
183
- }
184
-
185
- function renderPage(num) {
186
- pageRendering = true;
187
- pdfDoc.getPage(num).then(page => {
188
- const viewport = page.getViewport({ scale });
189
- const pixelRatio = window.devicePixelRatio || 1;
190
- const adjustedViewport = page.getViewport({ scale: scale * pixelRatio });
191
- const pageDiv = createPageDiv(num, viewport);
192
- const canvas = createCanvas(viewport, adjustedViewport);
193
- renderCanvas(page, canvas, adjustedViewport);
194
- pageDiv.appendChild(canvas);
195
- const textLayerDiv = createTextLayerDiv(viewport);
196
- pageDiv.appendChild(textLayerDiv);
197
- renderTextLayer(page, textLayerDiv, viewport);
198
- pdfViewer.appendChild(pageDiv);
199
- attachLanguageModeListener(pageDiv);
200
- renderHighlights();
201
- pageRendering = false;
202
- if (pageNumPending !== null) {
203
- renderPage(pageNumPending);
204
- pageNumPending = null;
205
- }
206
- if (num < pdfDoc.numPages && pdfViewer.scrollHeight <= window.innerHeight * 2) {
207
- renderPage(num + 1);
208
- }
209
- });
210
- }
211
-
212
- // Other functions (TXT, EPUB, navigation, mode handling, flashcard generation, etc.)
213
- // These are implemented as in index.html, with improvements:
214
- // - Use async/await consistently
215
- // - Break down large functions (e.g., generateContent, handleLanguageMode)
216
- // - Improve error handling
217
- // - Use const/let appropriately
218
- // - Encapsulate related functionality
219
-
220
- // Note: Due to space constraints, the full implementation is not shown here.
221
- // However, all functions from index.html are moved here with the noted improvements.
222
- // Ensure all functionality (PDF, EPUB, TXT handling, navigation, collections, etc.) is preserved.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/js/models.js ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const MODEL_API_KEY_MAPPING = {
2
+ "gemini/gemini-exp-1206": "GEMINI_API_KEY",
3
+ // "gemini/gemini-2.0-flash": "GEMINI_API_KEY",
4
+ // "gemini/gemini-2.0-flash-lite-preview-02-05": "GEMINI_API_KEY",
5
+ // "gemini/gemini-2.0-pro-exp-02-05": "GEMINI_API_KEY",
6
+ // "gemini/gemini-2.0-flash-thinking-exp-01-21": "GEMINI_API_KEY",
7
+ "openrouter/google/gemini-exp-1206:free": "OPENROUTER_API_KEY",
8
+ "openrouter/anthropic/claude-3-haiku-20240307": "OPENROUTER_API_KEY",
9
+ "openrouter/anthropic/claude-3-sonnet-20240229": "OPENROUTER_API_KEY"
10
+ };
11
+
12
+ const availableModels = Object.keys(MODEL_API_KEY_MAPPING);
13
+
14
+ // Removed MODEL_LABELS as they are no longer needed.
static/js/prompts.js ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const FLASHCARD_PROMPT = `Generate flashcards as a JSON array where each object has "question" and "answer" keys. 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 question should test a key concept and the answer should be brief but complete. Use <b> tags to emphasize important words or phrases. Cite short code or examples if needed.
2
+
3
+ Example input: "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."
4
+
5
+ Example output:
6
+ [
7
+ {
8
+ "question": "What is the primary goal of <b>load balancing</b> in parallel computing?",
9
+ "answer": "To <b>distribute work evenly</b> across processing units, maximizing efficiency and minimizing idle time."
10
+ },
11
+ {
12
+ "question": "How does <b>dynamic load balancing</b> differ from <b>static load balancing</b>?",
13
+ "answer": "Dynamic balancing <b>adjusts work distribution during runtime</b>, while static balancing <b>determines distribution before execution</b>."
14
+ }
15
+ ]
16
+
17
+ Now generate flashcards for this text:`;
18
+
19
+ const EXPLAIN_PROMPT = `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 a JSON object with an "explanation" key.
20
+
21
+ Example output:
22
+ {
23
+ "explanation": "Load balancing is a technique in parallel computing that ensures work is distributed evenly across different processing units. Think of it like distributing tasks among team members - when done well, everyone has a fair amount of work and the team is more efficient. There are two main approaches: dynamic balancing (adjusting work distribution as needed) and static balancing (planning the distribution ahead of time)."
24
+ }
25
+
26
+ Now explain this text:`;
27
+
28
+ const LANGUAGE_PROMPT = `Return a JSON object with "word", "translation", "question", and "answer" keys for the given word in {targetLanguage}.
29
+
30
+ Example input:
31
+ Word: "refused"
32
+ Phrase: "Hamas refused to join a new round of peace negotiations."
33
+
34
+ Example output:
35
+ {
36
+ "word": "refused",
37
+ "translation": "từ chối",
38
+ "question": "Hamas <b>refused</b> to join a new round of peace negotiations.",
39
+ "answer": "Declined to accept or comply with a request or proposal."
40
+ }
41
+
42
+ Now explain the word in the phrase below:
43
+ Word: "{word}"
44
+ Phrase: "{phrase}"`;
templates/index.html CHANGED
@@ -9,6 +9,8 @@
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
  <style>
13
  .api-settings {
14
  margin-bottom: 15px;
@@ -70,53 +72,11 @@
70
  </div>
71
  </div>
72
  <div id="settings-panel" style="display: none;">
73
- <input type="password" id="api-key-input" placeholder="Enter Claude API Key">
74
- <select id="model-select">
75
- <option value="claude-3-5-sonnet-20240620">Claude 3.5 Sonnet</option>
76
- <option value="claude-3-haiku-20240307">Claude 3 Haiku</option>
77
- </select>
78
- <textarea id="system-prompt" placeholder="Enter system prompt for flashcard generation">Generate flashcards as a JSON array where each object has "question" and "answer" keys. 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 question should test a key concept and the answer should be brief but complete. Use <b> tags to emphasize important words or phrases. Cite short code or examples if needed.
79
-
80
- Example input: "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."
81
-
82
- Example output:
83
- [
84
- {
85
- "question": "What is the primary goal of <b>load balancing</b> in parallel computing?",
86
- "answer": "To <b>distribute work evenly</b> across processing units, maximizing efficiency and minimizing idle time."
87
- },
88
- {
89
- "question": "How does <b>dynamic load balancing</b> differ from <b>static load balancing</b>?",
90
- "answer": "Dynamic balancing <b>adjusts work distribution during runtime</b>, while static balancing <b>determines distribution before execution</b>."
91
- }
92
- ]
93
-
94
- Now generate flashcards for this text:</textarea>
95
- <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 a JSON object with an "explanation" key.
96
-
97
- Example output:
98
- {
99
- "explanation": "Load balancing is a technique in parallel computing that ensures work is distributed evenly across different processing units. Think of it like distributing tasks among team members - when done well, everyone has a fair amount of work and the team is more efficient. There are two main approaches: dynamic balancing (adjusting work distribution as needed) and static balancing (planning the distribution ahead of time)."
100
- }
101
-
102
- Now explain this text:</textarea>
103
- <textarea id="language-prompt" placeholder="Enter system prompt for language mode">Return a JSON object with "word", "translation", "question", and "answer" keys for the given word in {targetLanguage}.
104
-
105
- Example input:
106
- Word: "refused"
107
- Phrase: "Hamas refused to join a new round of peace negotiations."
108
-
109
- Example output:
110
- {
111
- "word": "refused",
112
- "translation": "từ chối",
113
- "question": "Hamas <b>refused</b> to join a new round of peace negotiations.",
114
- "answer": "Declined to accept or comply with a request or proposal."
115
- }
116
-
117
- Now explain the word in the phrase below:
118
- Word: "{word}"
119
- Phrase: "{phrase}"</textarea>
120
  </div>
121
  <div id="mode-toggle">
122
  <button class="mode-btn selected" data-mode="flashcard">Flashcard</button>
@@ -149,7 +109,6 @@ Phrase: "{phrase}"</textarea>
149
  </div>
150
  </div>
151
 
152
- <script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"></script>
153
  <script>
154
  pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.worker.min.js';
155
 
@@ -174,7 +133,7 @@ Phrase: "{phrase}"</textarea>
174
  let apiKey = '';
175
  let currentFileName = '';
176
  let currentPage = 1;
177
- let selectedModel = 'claude-3-haiku-20240307';
178
  let lastProcessedQuery = '';
179
  let lastRequestTime = 0;
180
  const cooldownTime = 1000; // 1 second cooldown
@@ -480,9 +439,30 @@ Phrase: "{phrase}"</textarea>
480
  }
481
  }
482
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
483
  async function generateLanguageFlashcard(word, phrase, targetLanguage) {
484
  if (!apiKey) {
485
- alert('Please enter your Claude API key first.');
486
  return;
487
  }
488
 
@@ -492,7 +472,7 @@ Phrase: "{phrase}"</textarea>
492
  .replace('{targetLanguage}', targetLanguage);
493
 
494
  try {
495
- const response = await callClaudeAPI(prompt);
496
  if (response.flashcard) {
497
  const flashcard = response.flashcard;
498
  const formattedFlashcard = {
@@ -507,14 +487,14 @@ Phrase: "{phrase}"</textarea>
507
  throw new Error('Invalid response from API');
508
  }
509
  } catch (error) {
510
- console.error('Error calling Claude API:', error);
511
  alert('Failed to generate language flashcard. Please check your API key and try again.');
512
  }
513
  }
514
 
515
  async function generateContent() {
516
  if (!apiKey) {
517
- alert('Please enter your Claude API key first.');
518
  return;
519
  }
520
 
@@ -532,23 +512,23 @@ Phrase: "{phrase}"</textarea>
532
  return;
533
  }
534
 
535
- // Disable the button, change its color, and show notification
536
  submitBtn.disabled = true;
537
- submitBtn.style.backgroundColor = '#808080'; // Change to gray
538
  const notification = document.createElement('div');
539
  notification.textContent = 'Generating...';
540
  notification.style.position = 'fixed';
541
  notification.style.top = '20px';
542
  notification.style.right = '20px';
543
  notification.style.padding = '10px';
544
- notification.style.backgroundColor = 'rgba(0, 128, 0, 0.7)'; // Change to green
545
  notification.style.color = 'white';
546
  notification.style.borderRadius = '5px';
547
  notification.style.zIndex = '1000';
548
  document.body.appendChild(notification);
549
 
550
  try {
551
- const response = await callClaudeAPI(prompt);
552
  if (mode === 'flashcard' && response.flashcards) {
553
  displayFlashcards(response.flashcards, true);
554
  } else if (mode === 'explain' && response.explanation) {
@@ -557,18 +537,17 @@ Phrase: "{phrase}"</textarea>
557
  throw new Error('Invalid response from API');
558
  }
559
  } catch (error) {
560
- console.error('Error calling Claude API:', error);
561
  alert(`Failed to generate ${mode === 'flashcard' ? 'flashcards' : 'explanation'}. Please check your API key and try again.`);
562
  } finally {
563
- // Remove notification, re-enable button, and restore its color after 3 seconds
564
  setTimeout(() => {
565
  document.body.removeChild(notification);
566
  submitBtn.disabled = false;
567
- submitBtn.style.backgroundColor = ''; // Restore original color
568
  }, 3000);
569
  }
570
  } else {
571
- alert(`Please select some text from the PDF to generate ${mode === 'flashcard' ? 'flashcards' : 'an explanation'}.`);
572
  }
573
  }
574
 
@@ -609,31 +588,6 @@ Phrase: "{phrase}"</textarea>
609
  }
610
  }
611
 
612
- async function callClaudeAPI(prompt) {
613
- const response = await fetch('/generate_flashcard', {
614
- method: 'POST',
615
- headers: {
616
- 'Content-Type': 'application/json',
617
- 'X-API-Key': apiKey
618
- },
619
- body: JSON.stringify({
620
- prompt: prompt,
621
- model: selectedModel,
622
- mode: mode // Add the current mode to the request
623
- })
624
- });
625
-
626
- if (!response.ok) {
627
- throw new Error(`HTTP error! status: ${response.status}`);
628
- }
629
-
630
- return await response.json();
631
- }
632
-
633
- modelSelect.addEventListener('change', function () {
634
- selectedModel = this.value;
635
- });
636
-
637
  function displayFlashcards(flashcards, append = false) {
638
  if (!append) {
639
  flashcardsContainer.innerHTML = ''; // Clear existing flashcards only if not appending
@@ -1391,6 +1345,43 @@ Phrase: "{phrase}"</textarea>
1391
  console.error('Error:', error);
1392
  });
1393
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1394
  </script>
1395
  </body>
1396
 
 
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
+ <script src="/static/js/prompts.js"></script>
13
+ <script src="/static/js/models.js"></script>
14
  <style>
15
  .api-settings {
16
  margin-bottom: 15px;
 
72
  </div>
73
  </div>
74
  <div id="settings-panel" style="display: none;">
75
+ <input type="password" id="api-key-input" placeholder="Enter API Key">
76
+ <select id="model-select"></select>
77
+ <textarea id="system-prompt" placeholder="Enter system prompt for flashcard generation"></textarea>
78
+ <textarea id="explain-prompt" placeholder="Enter system prompt for explanation" style="display: none;"></textarea>
79
+ <textarea id="language-prompt" placeholder="Enter system prompt for language mode"></textarea>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  </div>
81
  <div id="mode-toggle">
82
  <button class="mode-btn selected" data-mode="flashcard">Flashcard</button>
 
109
  </div>
110
  </div>
111
 
 
112
  <script>
113
  pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.9.359/pdf.worker.min.js';
114
 
 
133
  let apiKey = '';
134
  let currentFileName = '';
135
  let currentPage = 1;
136
+ let selectedModel = 'gemini/gemini-exp-1206';
137
  let lastProcessedQuery = '';
138
  let lastRequestTime = 0;
139
  const cooldownTime = 1000; // 1 second cooldown
 
439
  }
440
  }
441
 
442
+ async function callLLMAPI(prompt) {
443
+ const response = await fetch('/generate_flashcard', {
444
+ method: 'POST',
445
+ headers: {
446
+ 'Content-Type': 'application/json',
447
+ 'X-API-Key': apiKey
448
+ },
449
+ body: JSON.stringify({
450
+ prompt: prompt,
451
+ model: selectedModel,
452
+ mode: mode
453
+ })
454
+ });
455
+
456
+ if (!response.ok) {
457
+ throw new Error(`HTTP error! status: ${response.status}`);
458
+ }
459
+
460
+ return await response.json();
461
+ }
462
+
463
  async function generateLanguageFlashcard(word, phrase, targetLanguage) {
464
  if (!apiKey) {
465
+ alert('Please enter your API key first.');
466
  return;
467
  }
468
 
 
472
  .replace('{targetLanguage}', targetLanguage);
473
 
474
  try {
475
+ const response = await callLLMAPI(prompt);
476
  if (response.flashcard) {
477
  const flashcard = response.flashcard;
478
  const formattedFlashcard = {
 
487
  throw new Error('Invalid response from API');
488
  }
489
  } catch (error) {
490
+ console.error('Error calling LLM API:', error);
491
  alert('Failed to generate language flashcard. Please check your API key and try again.');
492
  }
493
  }
494
 
495
  async function generateContent() {
496
  if (!apiKey) {
497
+ alert('Please enter your API key first.');
498
  return;
499
  }
500
 
 
512
  return;
513
  }
514
 
515
+ // Disable the button and show notification
516
  submitBtn.disabled = true;
517
+ submitBtn.style.backgroundColor = '#808080';
518
  const notification = document.createElement('div');
519
  notification.textContent = 'Generating...';
520
  notification.style.position = 'fixed';
521
  notification.style.top = '20px';
522
  notification.style.right = '20px';
523
  notification.style.padding = '10px';
524
+ notification.style.backgroundColor = 'rgba(0, 128, 0, 0.7)';
525
  notification.style.color = 'white';
526
  notification.style.borderRadius = '5px';
527
  notification.style.zIndex = '1000';
528
  document.body.appendChild(notification);
529
 
530
  try {
531
+ const response = await callLLMAPI(prompt);
532
  if (mode === 'flashcard' && response.flashcards) {
533
  displayFlashcards(response.flashcards, true);
534
  } else if (mode === 'explain' && response.explanation) {
 
537
  throw new Error('Invalid response from API');
538
  }
539
  } catch (error) {
540
+ console.error('Error calling LLM API:', error);
541
  alert(`Failed to generate ${mode === 'flashcard' ? 'flashcards' : 'explanation'}. Please check your API key and try again.`);
542
  } finally {
 
543
  setTimeout(() => {
544
  document.body.removeChild(notification);
545
  submitBtn.disabled = false;
546
+ submitBtn.style.backgroundColor = '';
547
  }, 3000);
548
  }
549
  } else {
550
+ alert(`Please select some text to generate ${mode === 'flashcard' ? 'flashcards' : 'an explanation'}.`);
551
  }
552
  }
553
 
 
588
  }
589
  }
590
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
591
  function displayFlashcards(flashcards, append = false) {
592
  if (!append) {
593
  flashcardsContainer.innerHTML = ''; // Clear existing flashcards only if not appending
 
1345
  console.error('Error:', error);
1346
  });
1347
  }
1348
+
1349
+ document.addEventListener("DOMContentLoaded", () => {
1350
+ // Ensure the variables are defined from /static/js/prompts.js
1351
+ if (typeof FLASHCARD_PROMPT !== 'undefined') {
1352
+ document.getElementById('system-prompt').value = FLASHCARD_PROMPT;
1353
+ }
1354
+ if (typeof EXPLAIN_PROMPT !== 'undefined') {
1355
+ document.getElementById('explain-prompt').value = EXPLAIN_PROMPT;
1356
+ }
1357
+ if (typeof LANGUAGE_PROMPT !== 'undefined') {
1358
+ document.getElementById('language-prompt').value = LANGUAGE_PROMPT;
1359
+ }
1360
+
1361
+ // Populate the model select options
1362
+ const modelSelect = document.getElementById('model-select');
1363
+ availableModels.forEach(model => {
1364
+ const option = document.createElement('option');
1365
+ option.value = model;
1366
+ option.textContent = model;
1367
+ modelSelect.appendChild(option);
1368
+ });
1369
+
1370
+ // Set default model to Gemini
1371
+ modelSelect.value = "gemini/gemini-exp-1206";
1372
+ selectedModel = "gemini/gemini-exp-1206";
1373
+
1374
+ // Update API key placeholder based on selected model
1375
+ modelSelect.addEventListener('change', function() {
1376
+ selectedModel = this.value;
1377
+ const requiredKey = MODEL_API_KEY_MAPPING[selectedModel];
1378
+ apiKeyInput.placeholder = `Enter ${requiredKey}`;
1379
+ });
1380
+
1381
+ // Set initial API key placeholder
1382
+ const initialKey = MODEL_API_KEY_MAPPING["gemini/gemini-exp-1206"];
1383
+ apiKeyInput.placeholder = `Enter ${initialKey}`;
1384
+ });
1385
  </script>
1386
  </body>
1387