brickfrog commited on
Commit
a6cf941
·
verified ·
1 Parent(s): 8de601f

Upload folder using huggingface_hub

Browse files
Files changed (5) hide show
  1. .gitignore +2 -3
  2. app.py +453 -364
  3. pyproject.toml +7 -6
  4. requirements.txt +60 -5
  5. uv.lock +0 -0
.gitignore CHANGED
@@ -167,7 +167,6 @@ cython_debug/
167
  flagged
168
  *.csv
169
 
170
-
171
- uv.lock
172
  *.apkg
173
- *.csv
 
 
167
  flagged
168
  *.csv
169
 
 
 
170
  *.apkg
171
+ *.csv
172
+ .history
app.py CHANGED
@@ -7,7 +7,12 @@ import logging
7
  from logging.handlers import RotatingFileHandler
8
  import sys
9
  from functools import lru_cache
10
- from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type
 
 
 
 
 
11
  import hashlib
12
  import genanki
13
  import random
@@ -45,6 +50,7 @@ class Card(BaseModel):
45
  front: CardFront
46
  back: CardBack
47
  metadata: Optional[dict] = None
 
48
 
49
 
50
  class CardList(BaseModel):
@@ -77,22 +83,20 @@ class LearningSequence(BaseModel):
77
 
78
  def setup_logging():
79
  """Configure logging to both file and console"""
80
- logger = logging.getLogger('ankigen')
81
  logger.setLevel(logging.DEBUG)
82
 
83
  # Create formatters
84
  detailed_formatter = logging.Formatter(
85
- '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
86
- )
87
- simple_formatter = logging.Formatter(
88
- '%(levelname)s: %(message)s'
89
  )
 
90
 
91
  # File handler (detailed logging)
92
  file_handler = RotatingFileHandler(
93
- 'ankigen.log',
94
- maxBytes=1024*1024, # 1MB
95
- backupCount=5
96
  )
97
  file_handler.setLevel(logging.DEBUG)
98
  file_handler.setFormatter(detailed_formatter)
@@ -116,15 +120,18 @@ logger = setup_logging()
116
  # Replace the caching implementation with a proper cache dictionary
117
  _response_cache = {} # Global cache dictionary
118
 
 
119
  @lru_cache(maxsize=100)
120
  def get_cached_response(cache_key: str):
121
  """Get response from cache"""
122
  return _response_cache.get(cache_key)
123
 
 
124
  def set_cached_response(cache_key: str, response):
125
  """Set response in cache"""
126
  _response_cache[cache_key] = response
127
 
 
128
  def create_cache_key(prompt: str, model: str) -> str:
129
  """Create a unique cache key for the API request"""
130
  return hashlib.md5(f"{model}:{prompt}".encode()).hexdigest()
@@ -137,7 +144,7 @@ def create_cache_key(prompt: str, model: str) -> str:
137
  retry=retry_if_exception_type(Exception),
138
  before_sleep=lambda retry_state: logger.warning(
139
  f"Retrying API call (attempt {retry_state.attempt_number})"
140
- )
141
  )
142
  def structured_output_completion(
143
  client, model, response_format, system_prompt, user_prompt
@@ -145,17 +152,17 @@ def structured_output_completion(
145
  """Make API call with retry logic and caching"""
146
  cache_key = create_cache_key(f"{system_prompt}:{user_prompt}", model)
147
  cached_response = get_cached_response(cache_key)
148
-
149
  if cached_response is not None:
150
  logger.info("Using cached response")
151
  return cached_response
152
 
153
  try:
154
  logger.debug(f"Making API call with model {model}")
155
-
156
  # Add JSON instruction to system prompt
157
  system_prompt = f"{system_prompt}\nProvide your response as a JSON object matching the specified schema."
158
-
159
  completion = client.chat.completions.create(
160
  model=model,
161
  messages=[
@@ -163,7 +170,7 @@ def structured_output_completion(
163
  {"role": "user", "content": user_prompt.strip()},
164
  ],
165
  response_format={"type": "json_object"},
166
- temperature=0.7
167
  )
168
 
169
  if not hasattr(completion, "choices") or not completion.choices:
@@ -177,7 +184,7 @@ def structured_output_completion(
177
 
178
  # Parse the JSON response
179
  result = json.loads(first_choice.message.content)
180
-
181
  # Cache the successful response
182
  set_cached_response(cache_key, result)
183
  return result
@@ -188,25 +195,33 @@ def structured_output_completion(
188
 
189
 
190
  def generate_cards_batch(
191
- client,
192
- model,
193
- topic,
194
- num_cards,
195
- system_prompt,
196
- batch_size=3
197
  ):
198
- """Generate a batch of cards for a topic"""
 
 
 
 
 
 
 
 
 
 
 
199
  cards_prompt = f"""
200
  Generate {num_cards} flashcards for the topic: {topic}
 
201
  Return your response as a JSON object with the following structure:
202
  {{
203
  "cards": [
204
  {{
 
205
  "front": {{
206
- "question": "question text"
207
  }},
208
  "back": {{
209
- "answer": "concise answer",
210
  "explanation": "detailed explanation",
211
  "example": "practical example"
212
  }},
@@ -217,18 +232,17 @@ def generate_cards_batch(
217
  "difficulty": "beginner/intermediate/advanced"
218
  }}
219
  }}
 
220
  ]
221
  }}
222
  """
223
 
224
  try:
225
- logger.info(f"Generated learning sequence for {topic}")
 
 
226
  response = structured_output_completion(
227
- client,
228
- model,
229
- {"type": "json_object"},
230
- system_prompt,
231
- cards_prompt
232
  )
233
 
234
  if not response or "cards" not in response:
@@ -238,62 +252,83 @@ def generate_cards_batch(
238
  # Convert the JSON response into Card objects
239
  cards = []
240
  for card_data in response["cards"]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  card = Card(
 
242
  front=CardFront(**card_data["front"]),
243
  back=CardBack(**card_data["back"]),
244
- metadata=card_data.get("metadata", {})
245
  )
246
  cards.append(card)
247
 
248
  return cards
249
 
250
  except Exception as e:
251
- logger.error(f"Failed to generate cards batch: {str(e)}")
 
 
252
  raise
253
 
254
 
255
  # Add near the top with other constants
256
  AVAILABLE_MODELS = [
257
  {
258
- "value": "gpt-4o-mini", # Default model
259
- "label": "gpt-4o Mini (Fastest)",
260
- "description": "Balanced speed and quality"
261
  },
262
  {
263
- "value": "gpt-4o",
264
- "label": "gpt-4o (Better Quality)",
265
- "description": "Higher quality, slower generation"
266
  },
267
- {
268
- "value": "o1",
269
- "label": "o1 (Best Quality)",
270
- "description": "Highest quality, longest generation time"
271
- }
272
  ]
273
 
274
  GENERATION_MODES = [
275
  {
276
  "value": "subject",
277
  "label": "Single Subject",
278
- "description": "Generate cards for a specific topic"
279
  },
280
  {
281
  "value": "path",
282
  "label": "Learning Path",
283
- "description": "Break down a job description or learning goal into subjects"
284
- }
285
  ]
286
 
 
287
  def generate_cards(
288
  api_key_input,
289
  subject,
290
- model_name="gpt-4o-mini",
291
  topic_number=1,
292
  cards_per_topic=2,
293
  preference_prompt="assume I'm a beginner",
 
294
  ):
295
  logger.info(f"Starting card generation for subject: {subject}")
296
- logger.debug(f"Parameters: topics={topic_number}, cards_per_topic={cards_per_topic}")
 
 
297
 
298
  # Input validation
299
  if not api_key_input:
@@ -305,9 +340,9 @@ def generate_cards(
305
  if not subject.strip():
306
  logger.warning("No subject provided")
307
  raise gr.Error("Subject is required")
308
-
309
  gr.Info("🚀 Starting card generation...")
310
-
311
  try:
312
  logger.debug("Initializing OpenAI client")
313
  client = OpenAI(api_key=api_key_input)
@@ -318,9 +353,9 @@ def generate_cards(
318
  model = model_name
319
  flattened_data = []
320
  total = 0
321
-
322
  progress_tracker = gr.Progress(track_tqdm=True)
323
-
324
  system_prompt = f"""
325
  You are an expert educator in {subject}, creating an optimized learning sequence.
326
  Your goal is to:
@@ -357,30 +392,21 @@ def generate_cards(
357
  try:
358
  logger.info("Generating topics...")
359
  topics_response = structured_output_completion(
360
- client,
361
- model,
362
- {"type": "json_object"},
363
- system_prompt,
364
- topic_prompt
365
  )
366
-
367
  if not topics_response or "topics" not in topics_response:
368
  logger.error("Invalid topics response format")
369
  raise gr.Error("Failed to generate topics. Please try again.")
370
 
371
  topics = topics_response["topics"]
372
-
373
  gr.Info(f"✨ Generated {len(topics)} topics successfully!")
374
-
375
  # Generate cards for each topic
376
- for i, topic in enumerate(progress_tracker.tqdm(topics, desc="Generating cards")):
377
- progress_html = f"""
378
- <div style="text-align: center">
379
- <p>Generating cards for topic {i+1}/{len(topics)}: {topic["name"]}</p>
380
- <p>Cards generated so far: {total}</p>
381
- </div>
382
- """
383
-
384
  try:
385
  cards = generate_cards_batch(
386
  client,
@@ -388,17 +414,19 @@ def generate_cards(
388
  topic["name"],
389
  cards_per_topic,
390
  system_prompt,
391
- batch_size=3
 
392
  )
393
-
394
  if cards:
395
  for card_index, card in enumerate(cards, start=1):
396
- index = f"{i+1}.{card_index}"
397
  metadata = card.metadata or {}
398
-
399
  row = [
400
  index,
401
  topic["name"],
 
402
  card.front.question,
403
  card.back.answer,
404
  card.back.explanation,
@@ -406,15 +434,17 @@ def generate_cards(
406
  metadata.get("prerequisites", []),
407
  metadata.get("learning_outcomes", []),
408
  metadata.get("misconceptions", []),
409
- metadata.get("difficulty", "beginner")
410
  ]
411
  flattened_data.append(row)
412
  total += 1
413
-
414
  gr.Info(f"✅ Generated {len(cards)} cards for {topic['name']}")
415
-
416
  except Exception as e:
417
- logger.error(f"Failed to generate cards for topic {topic['name']}: {str(e)}")
 
 
418
  gr.Warning(f"Failed to generate cards for '{topic['name']}'")
419
  continue
420
 
@@ -424,13 +454,14 @@ def generate_cards(
424
  <p>Total cards generated: {total}</p>
425
  </div>
426
  """
427
-
428
  # Convert to DataFrame with all columns
429
  df = pd.DataFrame(
430
  flattened_data,
431
  columns=[
432
  "Index",
433
  "Topic",
 
434
  "Question",
435
  "Answer",
436
  "Explanation",
@@ -438,10 +469,10 @@ def generate_cards(
438
  "Prerequisites",
439
  "Learning_Outcomes",
440
  "Common_Misconceptions",
441
- "Difficulty"
442
- ]
443
  )
444
-
445
  return df, final_html, total
446
 
447
  except Exception as e:
@@ -452,20 +483,21 @@ def generate_cards(
452
  # Update the BASIC_MODEL definition with enhanced CSS/HTML
453
  BASIC_MODEL = genanki.Model(
454
  random.randrange(1 << 30, 1 << 31),
455
- 'AnkiGen Enhanced',
456
  fields=[
457
- {'name': 'Question'},
458
- {'name': 'Answer'},
459
- {'name': 'Explanation'},
460
- {'name': 'Example'},
461
- {'name': 'Prerequisites'},
462
- {'name': 'Learning_Outcomes'},
463
- {'name': 'Common_Misconceptions'},
464
- {'name': 'Difficulty'}
465
  ],
466
- templates=[{
467
- 'name': 'Card 1',
468
- 'qfmt': '''
 
469
  <div class="card question-side">
470
  <div class="difficulty-indicator {{Difficulty}}"></div>
471
  <div class="content">
@@ -482,8 +514,8 @@ BASIC_MODEL = genanki.Model(
482
  this.parentElement.classList.toggle('show');
483
  });
484
  </script>
485
- ''',
486
- 'afmt': '''
487
  <div class="card answer-side">
488
  <div class="content">
489
  <div class="question-section">
@@ -528,9 +560,10 @@ BASIC_MODEL = genanki.Model(
528
  </div>
529
  </div>
530
  </div>
531
- ''',
532
- }],
533
- css='''
 
534
  /* Base styles */
535
  .card {
536
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
@@ -714,15 +747,69 @@ BASIC_MODEL = genanki.Model(
714
  .tab-content.active {
715
  animation: fadeIn 0.2s ease-in-out;
716
  }
717
- '''
718
  )
719
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
720
  # Split the export functions
721
  def export_csv(data):
722
  """Export the generated cards as a CSV file"""
723
  if data is None:
724
  raise gr.Error("No data to export. Please generate cards first.")
725
-
726
  if len(data) < 2: # Minimum 2 cards
727
  raise gr.Error("Need at least 2 cards to export.")
728
 
@@ -732,188 +819,91 @@ def export_csv(data):
732
  data.to_csv(csv_path, index=False)
733
  gr.Info("✅ CSV export complete!")
734
  return gr.File(value=csv_path, visible=True)
735
-
736
  except Exception as e:
737
  logger.error(f"Failed to export CSV: {str(e)}", exc_info=True)
738
  raise gr.Error(f"Failed to export CSV: {str(e)}")
739
 
 
740
  def export_deck(data, subject):
741
  """Export the generated cards as an Anki deck with pedagogical metadata"""
742
  if data is None:
743
  raise gr.Error("No data to export. Please generate cards first.")
744
-
745
  if len(data) < 2: # Minimum 2 cards
746
  raise gr.Error("Need at least 2 cards to export.")
747
 
748
  try:
749
  gr.Info("💾 Creating Anki deck...")
750
-
751
  deck_id = random.randrange(1 << 30, 1 << 31)
752
  deck = genanki.Deck(deck_id, f"AnkiGen - {subject}")
753
-
754
- records = data.to_dict('records')
755
-
756
- # Update the model to include metadata fields
757
- global BASIC_MODEL
758
- BASIC_MODEL = genanki.Model(
759
- random.randrange(1 << 30, 1 << 31),
760
- 'AnkiGen Enhanced',
761
- fields=[
762
- {'name': 'Question'},
763
- {'name': 'Answer'},
764
- {'name': 'Explanation'},
765
- {'name': 'Example'},
766
- {'name': 'Prerequisites'},
767
- {'name': 'Learning_Outcomes'},
768
- {'name': 'Common_Misconceptions'},
769
- {'name': 'Difficulty'}
770
- ],
771
- templates=[{
772
- 'name': 'Card 1',
773
- 'qfmt': '''
774
- <div class="card question">
775
- <div class="content">{{Question}}</div>
776
- <div class="prerequisites">Prerequisites: {{Prerequisites}}</div>
777
- </div>
778
- ''',
779
- 'afmt': '''
780
- <div class="card answer">
781
- <div class="question">{{Question}}</div>
782
- <hr>
783
- <div class="content">
784
- <div class="answer-section">
785
- <h3>Answer:</h3>
786
- <div>{{Answer}}</div>
787
- </div>
788
-
789
- <div class="explanation-section">
790
- <h3>Explanation:</h3>
791
- <div>{{Explanation}}</div>
792
- </div>
793
-
794
- <div class="example-section">
795
- <h3>Example:</h3>
796
- <pre><code>{{Example}}</code></pre>
797
- </div>
798
-
799
- <div class="metadata-section">
800
- <h3>Prerequisites:</h3>
801
- <div>{{Prerequisites}}</div>
802
-
803
- <h3>Learning Outcomes:</h3>
804
- <div>{{Learning_Outcomes}}</div>
805
-
806
- <h3>Watch out for:</h3>
807
- <div>{{Common_Misconceptions}}</div>
808
-
809
- <h3>Difficulty Level:</h3>
810
- <div>{{Difficulty}}</div>
811
- </div>
812
- </div>
813
- </div>
814
- '''
815
- }],
816
- css='''
817
- .card {
818
- font-family: 'Inter', system-ui, -apple-system, sans-serif;
819
- font-size: 16px;
820
- line-height: 1.6;
821
- color: #1a1a1a;
822
- max-width: 800px;
823
- margin: 0 auto;
824
- padding: 20px;
825
- background: #ffffff;
826
- }
827
-
828
- .question {
829
- font-size: 1.3em;
830
- font-weight: 600;
831
- color: #2563eb;
832
- margin-bottom: 1.5em;
833
- }
834
-
835
- .prerequisites {
836
- font-size: 0.9em;
837
- color: #666;
838
- margin-top: 1em;
839
- font-style: italic;
840
- }
841
-
842
- .answer-section,
843
- .explanation-section,
844
- .example-section {
845
- margin: 1.5em 0;
846
- padding: 1.2em;
847
- border-radius: 8px;
848
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
849
- }
850
-
851
- .answer-section {
852
- background: #f0f9ff;
853
- border-left: 4px solid #2563eb;
854
- }
855
-
856
- .explanation-section {
857
- background: #f0fdf4;
858
- border-left: 4px solid #4ade80;
859
- }
860
-
861
- .example-section {
862
- background: #fff7ed;
863
- border-left: 4px solid #f97316;
864
- }
865
-
866
- .metadata-section {
867
- background: #f8f9fa;
868
- padding: 1em;
869
- border-radius: 6px;
870
- margin: 1em 0;
871
- }
872
-
873
- pre code {
874
- display: block;
875
- padding: 1em;
876
- background: #1e293b;
877
- color: #e2e8f0;
878
- border-radius: 6px;
879
- overflow-x: auto;
880
- font-family: 'Fira Code', 'Consolas', monospace;
881
- font-size: 0.9em;
882
- }
883
- '''
884
- )
885
-
886
  # Add notes to the deck
887
  for record in records:
888
- note = genanki.Note(
889
- model=BASIC_MODEL,
890
- fields=[
891
- str(record['Question']),
892
- str(record['Answer']),
893
- str(record['Explanation']),
894
- str(record['Example']),
895
- str(record['Prerequisites']),
896
- str(record['Learning_Outcomes']),
897
- str(record['Common_Misconceptions']),
898
- str(record['Difficulty'])
899
- ]
900
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
901
  deck.add_note(note)
902
-
903
  # Create a temporary directory for the package
904
  with tempfile.TemporaryDirectory() as temp_dir:
905
  output_path = Path(temp_dir) / "anki_deck.apkg"
906
  package = genanki.Package(deck)
907
  package.write_to_file(output_path)
908
-
909
  # Copy to a more permanent location
910
  final_path = "anki_deck.apkg"
911
- with open(output_path, 'rb') as src, open(final_path, 'wb') as dst:
912
  dst.write(src.read())
913
-
914
  gr.Info("✅ Anki deck export complete!")
915
  return gr.File(value=final_path, visible=True)
916
-
917
  except Exception as e:
918
  logger.error(f"Failed to export Anki deck: {str(e)}", exc_info=True)
919
  raise gr.Error(f"Failed to export Anki deck: {str(e)}")
@@ -951,21 +941,22 @@ custom_theme = gr.themes.Soft().set(
951
  button_primary_text_color="white",
952
  )
953
 
 
954
  def analyze_learning_path(api_key, description, model):
955
  """Analyze a job description or learning goal to create a structured learning path"""
956
-
957
  try:
958
  client = OpenAI(api_key=api_key)
959
  except Exception as e:
960
  logger.error(f"Failed to initialize OpenAI client: {str(e)}")
961
  raise gr.Error(f"Failed to initialize OpenAI client: {str(e)}")
962
-
963
  system_prompt = """You are an expert curriculum designer and educational consultant.
964
  Your task is to analyze learning goals and create structured, achievable learning paths.
965
  Break down complex topics into manageable subjects, identify prerequisites,
966
  and suggest practical projects that reinforce learning.
967
  Focus on creating a logical progression that builds upon previous knowledge."""
968
-
969
  path_prompt = f"""
970
  Analyze this description and create a structured learning path.
971
  Return your analysis as a JSON object with the following structure:
@@ -984,38 +975,96 @@ def analyze_learning_path(api_key, description, model):
984
  Description to analyze:
985
  {description}
986
  """
987
-
988
  try:
989
  response = structured_output_completion(
990
- client,
991
- model,
992
- {"type": "json_object"},
993
- system_prompt,
994
- path_prompt
995
  )
996
-
997
- # Format the response for the UI
 
 
 
 
 
 
 
 
998
  subjects_df = pd.DataFrame(response["subjects"])
999
- learning_order_text = f"### Recommended Learning Order\n{response['learning_order']}"
 
 
1000
  projects_text = f"### Suggested Projects\n{response['projects']}"
1001
-
1002
  return subjects_df, learning_order_text, projects_text
1003
-
1004
  except Exception as e:
1005
  logger.error(f"Failed to analyze learning path: {str(e)}")
1006
  raise gr.Error(f"Failed to analyze learning path: {str(e)}")
1007
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1008
  with gr.Blocks(
1009
  theme=custom_theme,
1010
  title="AnkiGen",
1011
  css="""
1012
  #footer {display:none !important}
1013
- .tall-dataframe {height: 800px !important}
1014
- .contain {max-width: 1200px; margin: auto;}
1015
  .output-cards {border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);}
1016
  .hint-text {font-size: 0.9em; color: #666; margin-top: 4px;}
 
1017
  """,
1018
- js=js_storage, # Add the JavaScript
1019
  ) as ankigen:
1020
  with gr.Column(elem_classes="contain"):
1021
  gr.Markdown("# 📚 AnkiGen - Advanced Anki Card Generator")
@@ -1026,68 +1075,62 @@ with gr.Blocks(
1026
  with gr.Row():
1027
  with gr.Column(scale=1):
1028
  gr.Markdown("### Configuration")
1029
-
1030
  # Add mode selection
1031
  generation_mode = gr.Radio(
1032
- choices=[
1033
- "subject",
1034
- "path"
1035
- ],
1036
  value="subject",
1037
  label="Generation Mode",
1038
- info="Choose how you want to generate content"
1039
  )
1040
-
1041
  # Create containers for different modes
1042
  with gr.Group() as subject_mode:
1043
  subject = gr.Textbox(
1044
  label="Subject",
1045
  placeholder="Enter the subject, e.g., 'Basic SQL Concepts'",
1046
- info="The topic you want to generate flashcards for"
1047
  )
1048
-
1049
  with gr.Group(visible=False) as path_mode:
1050
  description = gr.Textbox(
1051
  label="Learning Goal",
1052
  placeholder="Paste a job description or describe what you want to learn...",
1053
  info="We'll break this down into learnable subjects",
1054
- lines=5
 
 
 
1055
  )
1056
- analyze_button = gr.Button("Analyze & Break Down", variant="secondary")
1057
-
1058
  # Common settings
1059
  api_key_input = gr.Textbox(
1060
  label="OpenAI API Key",
1061
  type="password",
1062
  placeholder="Enter your OpenAI API key",
1063
  value=os.getenv("OPENAI_API_KEY", ""),
1064
- info="Your OpenAI API key starting with 'sk-'"
1065
  )
1066
-
1067
  # Generation Button
1068
  generate_button = gr.Button("Generate Cards", variant="primary")
1069
 
1070
  # Advanced Settings in Accordion
1071
  with gr.Accordion("Advanced Settings", open=False):
1072
  model_choice = gr.Dropdown(
1073
- choices=[
1074
- "gpt-4o-mini",
1075
- "gpt-4o",
1076
- "o1"
1077
- ],
1078
- value="gpt-4o-mini",
1079
- label="Model Selection",
1080
- info="Select the AI model to use for generation"
1081
  )
1082
-
1083
  # Add tooltip/description for models
1084
  model_info = gr.Markdown("""
1085
  **Model Information:**
1086
- - **gpt-4o-mini**: Fastest option, good for most use cases
1087
- - **gpt-4o**: Better quality, takes longer to generate
1088
- - **o1**: Highest quality, longest generation time
1089
  """)
1090
-
1091
  topic_number = gr.Slider(
1092
  label="Number of Topics",
1093
  minimum=2,
@@ -1110,6 +1153,11 @@ with gr.Blocks(
1110
  info="Customize how the content is presented",
1111
  lines=3,
1112
  )
 
 
 
 
 
1113
 
1114
  # Right column - add a new container for learning path results
1115
  with gr.Column(scale=2):
@@ -1118,32 +1166,33 @@ with gr.Blocks(
1118
  subjects_list = gr.Dataframe(
1119
  headers=["Subject", "Prerequisites", "Time Estimate"],
1120
  label="Recommended Subjects",
1121
- interactive=False
1122
  )
1123
  learning_order = gr.Markdown("### Recommended Learning Order")
1124
  projects = gr.Markdown("### Suggested Projects")
1125
-
1126
  # Replace generate_selected with use_subjects
1127
  use_subjects = gr.Button(
1128
  "Use These Subjects ℹ️", # Added info emoji to button text
1129
- variant="primary"
1130
  )
1131
  gr.Markdown(
1132
  "*Click to copy subjects to main input for card generation*",
1133
- elem_classes="hint-text"
1134
  )
1135
-
1136
  # Existing output components
1137
  with gr.Group() as cards_output:
1138
  gr.Markdown("### Generated Cards")
1139
-
1140
  # Output Format Documentation
1141
- with gr.Accordion("Output Format", open=True):
1142
  gr.Markdown("""
1143
  The generated cards include:
1144
 
1145
  * **Index**: Unique identifier for each card
1146
  * **Topic**: The specific subtopic within your subject
 
1147
  * **Question**: Clear, focused question for the flashcard front
1148
  * **Answer**: Concise core answer
1149
  * **Explanation**: Detailed conceptual explanation
@@ -1162,7 +1211,7 @@ with gr.Blocks(
1162
  with gr.Accordion("Example Card Format", open=False):
1163
  gr.Code(
1164
  label="Example Card",
1165
- value='''
1166
  {
1167
  "front": {
1168
  "question": "What is a PRIMARY KEY constraint in SQL?"
@@ -1182,15 +1231,17 @@ with gr.Blocks(
1182
  "difficulty": "beginner"
1183
  }
1184
  }
1185
- ''',
1186
- language="json"
1187
  )
1188
-
1189
  # Dataframe Output
1190
  output = gr.Dataframe(
 
1191
  headers=[
1192
  "Index",
1193
  "Topic",
 
1194
  "Question",
1195
  "Answer",
1196
  "Explanation",
@@ -1198,41 +1249,68 @@ with gr.Blocks(
1198
  "Prerequisites",
1199
  "Learning_Outcomes",
1200
  "Common_Misconceptions",
1201
- "Difficulty"
1202
  ],
1203
  interactive=True,
1204
  elem_classes="tall-dataframe",
1205
  wrap=True,
1206
- column_widths=[50, 100, 200, 200, 250, 200, 150, 150, 150, 100],
 
 
 
 
 
 
 
 
 
 
 
 
1207
  )
1208
 
1209
  # Export Controls
1210
- with gr.Row():
1211
- with gr.Column():
1212
- gr.Markdown("### Export Options")
1213
- with gr.Row():
1214
- export_csv_button = gr.Button("Export to CSV", variant="secondary")
1215
- export_anki_button = gr.Button("Export to Anki Deck", variant="secondary")
1216
- download_csv = gr.File(label="Download CSV", interactive=False, visible=False)
1217
- download_anki = gr.File(label="Download Anki Deck", interactive=False, visible=False)
 
 
 
 
 
 
 
 
 
 
 
1218
 
1219
  # Add near the top of the Blocks
1220
  with gr.Row():
1221
  progress = gr.HTML(visible=False)
1222
- total_cards = gr.Number(label="Total Cards Generated", value=0, visible=False)
 
 
1223
 
1224
  # Add JavaScript to handle mode switching
1225
  def update_mode_visibility(mode):
1226
  """Update component visibility based on selected mode and clear values"""
1227
- is_subject = (mode == "subject")
1228
- is_path = (mode == "path")
1229
-
1230
  # Clear values when switching modes
1231
  if is_path:
1232
  subject.value = "" # Clear subject when switching to path mode
1233
  else:
1234
- description.value = "" # Clear description when switching to subject mode
1235
-
 
 
1236
  return {
1237
  subject_mode: gr.update(visible=is_subject),
1238
  path_mode: gr.update(visible=is_path),
@@ -1242,7 +1320,7 @@ with gr.Blocks(
1242
  description: gr.update(value="") if not is_path else gr.update(),
1243
  output: gr.update(value=None), # Clear previous output
1244
  progress: gr.update(value="", visible=False),
1245
- total_cards: gr.update(value=0, visible=False)
1246
  }
1247
 
1248
  # Update the mode switching handler to include all components that need clearing
@@ -1258,60 +1336,70 @@ with gr.Blocks(
1258
  description,
1259
  output,
1260
  progress,
1261
- total_cards
1262
- ]
1263
  )
1264
-
1265
  # Add handler for path analysis
1266
  analyze_button.click(
1267
  fn=analyze_learning_path,
1268
  inputs=[api_key_input, description, model_choice],
1269
- outputs=[subjects_list, learning_order, projects]
1270
  )
1271
-
1272
  # Add this function to handle copying subjects to main input
1273
- def use_selected_subjects(subjects_df, topic_number):
1274
  """Copy selected subjects to main input and switch to subject mode"""
1275
  if subjects_df is None or subjects_df.empty:
1276
- raise gr.Error("No subjects available to copy")
1277
-
1278
- # Get all subjects and join them
 
 
 
 
 
 
 
 
 
 
 
1279
  subjects = subjects_df["Subject"].tolist()
1280
  combined_subject = ", ".join(subjects)
1281
-
1282
- # Calculate reasonable number of topics based on number of subjects
1283
- suggested_topics = min(len(subjects) + 2, 20) # Add 2 for related concepts, cap at 20
1284
-
1285
- # Return updates for individual components instead of groups
1286
  return (
1287
- "subject", # generation_mode value
1288
- gr.update(visible=True), # subject textbox visibility
1289
- gr.update(visible=False), # description textbox visibility
1290
- gr.update(visible=False), # subjects_list visibility
1291
- gr.update(visible=False), # learning_order visibility
1292
- gr.update(visible=False), # projects visibility
1293
- gr.update(visible=True), # output visibility
1294
- combined_subject, # subject value
1295
- suggested_topics, # topic_number value
1296
- "Focus on connections between these subjects and their practical applications" # preference_prompt
1297
  )
1298
 
1299
- # Update the click handler to match the new outputs
1300
  use_subjects.click(
1301
  fn=use_selected_subjects,
1302
- inputs=[subjects_list, topic_number],
1303
- outputs=[
1304
  generation_mode,
1305
- subject, # Individual components instead of groups
1306
- description,
1307
- subjects_list,
1308
- learning_order,
1309
- projects,
1310
- output,
1311
- subject,
1312
- topic_number,
1313
- preference_prompt
1314
- ]
1315
  )
1316
 
1317
  # Simplified event handlers
@@ -1320,13 +1408,14 @@ with gr.Blocks(
1320
  inputs=[
1321
  api_key_input,
1322
  subject,
1323
- model_choice, # Add model selection
1324
  topic_number,
1325
  cards_per_topic,
1326
  preference_prompt,
 
1327
  ],
1328
  outputs=[output, progress, total_cards],
1329
- show_progress=True,
1330
  )
1331
 
1332
  export_csv_button.click(
 
7
  from logging.handlers import RotatingFileHandler
8
  import sys
9
  from functools import lru_cache
10
+ from tenacity import (
11
+ retry,
12
+ stop_after_attempt,
13
+ wait_exponential,
14
+ retry_if_exception_type,
15
+ )
16
  import hashlib
17
  import genanki
18
  import random
 
50
  front: CardFront
51
  back: CardBack
52
  metadata: Optional[dict] = None
53
+ card_type: str = "basic" # Add card_type, default to basic
54
 
55
 
56
  class CardList(BaseModel):
 
83
 
84
  def setup_logging():
85
  """Configure logging to both file and console"""
86
+ logger = logging.getLogger("ankigen")
87
  logger.setLevel(logging.DEBUG)
88
 
89
  # Create formatters
90
  detailed_formatter = logging.Formatter(
91
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
 
 
 
92
  )
93
+ simple_formatter = logging.Formatter("%(levelname)s: %(message)s")
94
 
95
  # File handler (detailed logging)
96
  file_handler = RotatingFileHandler(
97
+ "ankigen.log",
98
+ maxBytes=1024 * 1024, # 1MB
99
+ backupCount=5,
100
  )
101
  file_handler.setLevel(logging.DEBUG)
102
  file_handler.setFormatter(detailed_formatter)
 
120
  # Replace the caching implementation with a proper cache dictionary
121
  _response_cache = {} # Global cache dictionary
122
 
123
+
124
  @lru_cache(maxsize=100)
125
  def get_cached_response(cache_key: str):
126
  """Get response from cache"""
127
  return _response_cache.get(cache_key)
128
 
129
+
130
  def set_cached_response(cache_key: str, response):
131
  """Set response in cache"""
132
  _response_cache[cache_key] = response
133
 
134
+
135
  def create_cache_key(prompt: str, model: str) -> str:
136
  """Create a unique cache key for the API request"""
137
  return hashlib.md5(f"{model}:{prompt}".encode()).hexdigest()
 
144
  retry=retry_if_exception_type(Exception),
145
  before_sleep=lambda retry_state: logger.warning(
146
  f"Retrying API call (attempt {retry_state.attempt_number})"
147
+ ),
148
  )
149
  def structured_output_completion(
150
  client, model, response_format, system_prompt, user_prompt
 
152
  """Make API call with retry logic and caching"""
153
  cache_key = create_cache_key(f"{system_prompt}:{user_prompt}", model)
154
  cached_response = get_cached_response(cache_key)
155
+
156
  if cached_response is not None:
157
  logger.info("Using cached response")
158
  return cached_response
159
 
160
  try:
161
  logger.debug(f"Making API call with model {model}")
162
+
163
  # Add JSON instruction to system prompt
164
  system_prompt = f"{system_prompt}\nProvide your response as a JSON object matching the specified schema."
165
+
166
  completion = client.chat.completions.create(
167
  model=model,
168
  messages=[
 
170
  {"role": "user", "content": user_prompt.strip()},
171
  ],
172
  response_format={"type": "json_object"},
173
+ temperature=0.7,
174
  )
175
 
176
  if not hasattr(completion, "choices") or not completion.choices:
 
184
 
185
  # Parse the JSON response
186
  result = json.loads(first_choice.message.content)
187
+
188
  # Cache the successful response
189
  set_cached_response(cache_key, result)
190
  return result
 
195
 
196
 
197
  def generate_cards_batch(
198
+ client, model, topic, num_cards, system_prompt, generate_cloze=False, batch_size=3
 
 
 
 
 
199
  ):
200
+ """Generate a batch of cards for a topic, potentially including cloze deletions"""
201
+
202
+ cloze_instruction = ""
203
+ if generate_cloze:
204
+ cloze_instruction = """
205
+ Where appropriate, generate Cloze deletion cards.
206
+ - For Cloze cards, set "card_type" to "cloze".
207
+ - Format the question field using Anki's cloze syntax (e.g., "The capital of France is {{c1::Paris}}.").
208
+ - The "answer" field should contain the full, non-cloze text or specific context for the cloze.
209
+ - For standard question/answer cards, set "card_type" to "basic".
210
+ """
211
+
212
  cards_prompt = f"""
213
  Generate {num_cards} flashcards for the topic: {topic}
214
+ {cloze_instruction}
215
  Return your response as a JSON object with the following structure:
216
  {{
217
  "cards": [
218
  {{
219
+ "card_type": "basic or cloze",
220
  "front": {{
221
+ "question": "question text (potentially with {{c1::cloze syntax}})"
222
  }},
223
  "back": {{
224
+ "answer": "concise answer or full text for cloze",
225
  "explanation": "detailed explanation",
226
  "example": "practical example"
227
  }},
 
232
  "difficulty": "beginner/intermediate/advanced"
233
  }}
234
  }}
235
+ // ... more cards
236
  ]
237
  }}
238
  """
239
 
240
  try:
241
+ logger.info(
242
+ f"Generating card batch for {topic}, Cloze enabled: {generate_cloze}"
243
+ )
244
  response = structured_output_completion(
245
+ client, model, {"type": "json_object"}, system_prompt, cards_prompt
 
 
 
 
246
  )
247
 
248
  if not response or "cards" not in response:
 
252
  # Convert the JSON response into Card objects
253
  cards = []
254
  for card_data in response["cards"]:
255
+ # Ensure required fields are present before creating Card object
256
+ if "front" not in card_data or "back" not in card_data:
257
+ logger.warning(
258
+ f"Skipping card due to missing front/back data: {card_data}"
259
+ )
260
+ continue
261
+ if "question" not in card_data["front"]:
262
+ logger.warning(f"Skipping card due to missing question: {card_data}")
263
+ continue
264
+ if (
265
+ "answer" not in card_data["back"]
266
+ or "explanation" not in card_data["back"]
267
+ or "example" not in card_data["back"]
268
+ ):
269
+ logger.warning(
270
+ f"Skipping card due to missing answer/explanation/example: {card_data}"
271
+ )
272
+ continue
273
+
274
  card = Card(
275
+ card_type=card_data.get("card_type", "basic"),
276
  front=CardFront(**card_data["front"]),
277
  back=CardBack(**card_data["back"]),
278
+ metadata=card_data.get("metadata", {}),
279
  )
280
  cards.append(card)
281
 
282
  return cards
283
 
284
  except Exception as e:
285
+ logger.error(
286
+ f"Failed to generate cards batch for {topic}: {str(e)}", exc_info=True
287
+ )
288
  raise
289
 
290
 
291
  # Add near the top with other constants
292
  AVAILABLE_MODELS = [
293
  {
294
+ "value": "gpt-4.1-mini", # Default model
295
+ "label": "gpt-4.1 Mini (Fastest)",
296
+ "description": "Balanced speed and quality",
297
  },
298
  {
299
+ "value": "gpt-4.1",
300
+ "label": "gpt-4.1 (Better Quality)",
301
+ "description": "Higher quality, slower generation",
302
  },
 
 
 
 
 
303
  ]
304
 
305
  GENERATION_MODES = [
306
  {
307
  "value": "subject",
308
  "label": "Single Subject",
309
+ "description": "Generate cards for a specific topic",
310
  },
311
  {
312
  "value": "path",
313
  "label": "Learning Path",
314
+ "description": "Break down a job description or learning goal into subjects",
315
+ },
316
  ]
317
 
318
+
319
  def generate_cards(
320
  api_key_input,
321
  subject,
322
+ model_name="gpt-4.1-mini",
323
  topic_number=1,
324
  cards_per_topic=2,
325
  preference_prompt="assume I'm a beginner",
326
+ generate_cloze=False,
327
  ):
328
  logger.info(f"Starting card generation for subject: {subject}")
329
+ logger.debug(
330
+ f"Parameters: topics={topic_number}, cards_per_topic={cards_per_topic}, cloze={generate_cloze}"
331
+ )
332
 
333
  # Input validation
334
  if not api_key_input:
 
340
  if not subject.strip():
341
  logger.warning("No subject provided")
342
  raise gr.Error("Subject is required")
343
+
344
  gr.Info("🚀 Starting card generation...")
345
+
346
  try:
347
  logger.debug("Initializing OpenAI client")
348
  client = OpenAI(api_key=api_key_input)
 
353
  model = model_name
354
  flattened_data = []
355
  total = 0
356
+
357
  progress_tracker = gr.Progress(track_tqdm=True)
358
+
359
  system_prompt = f"""
360
  You are an expert educator in {subject}, creating an optimized learning sequence.
361
  Your goal is to:
 
392
  try:
393
  logger.info("Generating topics...")
394
  topics_response = structured_output_completion(
395
+ client, model, {"type": "json_object"}, system_prompt, topic_prompt
 
 
 
 
396
  )
397
+
398
  if not topics_response or "topics" not in topics_response:
399
  logger.error("Invalid topics response format")
400
  raise gr.Error("Failed to generate topics. Please try again.")
401
 
402
  topics = topics_response["topics"]
403
+
404
  gr.Info(f"✨ Generated {len(topics)} topics successfully!")
405
+
406
  # Generate cards for each topic
407
+ for i, topic in enumerate(
408
+ progress_tracker.tqdm(topics, desc="Generating cards")
409
+ ):
 
 
 
 
 
410
  try:
411
  cards = generate_cards_batch(
412
  client,
 
414
  topic["name"],
415
  cards_per_topic,
416
  system_prompt,
417
+ generate_cloze=generate_cloze,
418
+ batch_size=3,
419
  )
420
+
421
  if cards:
422
  for card_index, card in enumerate(cards, start=1):
423
+ index = f"{i + 1}.{card_index}"
424
  metadata = card.metadata or {}
425
+
426
  row = [
427
  index,
428
  topic["name"],
429
+ card.card_type,
430
  card.front.question,
431
  card.back.answer,
432
  card.back.explanation,
 
434
  metadata.get("prerequisites", []),
435
  metadata.get("learning_outcomes", []),
436
  metadata.get("misconceptions", []),
437
+ metadata.get("difficulty", "beginner"),
438
  ]
439
  flattened_data.append(row)
440
  total += 1
441
+
442
  gr.Info(f"✅ Generated {len(cards)} cards for {topic['name']}")
443
+
444
  except Exception as e:
445
+ logger.error(
446
+ f"Failed to generate cards for topic {topic['name']}: {str(e)}"
447
+ )
448
  gr.Warning(f"Failed to generate cards for '{topic['name']}'")
449
  continue
450
 
 
454
  <p>Total cards generated: {total}</p>
455
  </div>
456
  """
457
+
458
  # Convert to DataFrame with all columns
459
  df = pd.DataFrame(
460
  flattened_data,
461
  columns=[
462
  "Index",
463
  "Topic",
464
+ "Card_Type",
465
  "Question",
466
  "Answer",
467
  "Explanation",
 
469
  "Prerequisites",
470
  "Learning_Outcomes",
471
  "Common_Misconceptions",
472
+ "Difficulty",
473
+ ],
474
  )
475
+
476
  return df, final_html, total
477
 
478
  except Exception as e:
 
483
  # Update the BASIC_MODEL definition with enhanced CSS/HTML
484
  BASIC_MODEL = genanki.Model(
485
  random.randrange(1 << 30, 1 << 31),
486
+ "AnkiGen Enhanced",
487
  fields=[
488
+ {"name": "Question"},
489
+ {"name": "Answer"},
490
+ {"name": "Explanation"},
491
+ {"name": "Example"},
492
+ {"name": "Prerequisites"},
493
+ {"name": "Learning_Outcomes"},
494
+ {"name": "Common_Misconceptions"},
495
+ {"name": "Difficulty"},
496
  ],
497
+ templates=[
498
+ {
499
+ "name": "Card 1",
500
+ "qfmt": """
501
  <div class="card question-side">
502
  <div class="difficulty-indicator {{Difficulty}}"></div>
503
  <div class="content">
 
514
  this.parentElement.classList.toggle('show');
515
  });
516
  </script>
517
+ """,
518
+ "afmt": """
519
  <div class="card answer-side">
520
  <div class="content">
521
  <div class="question-section">
 
560
  </div>
561
  </div>
562
  </div>
563
+ """,
564
+ }
565
+ ],
566
+ css="""
567
  /* Base styles */
568
  .card {
569
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
 
747
  .tab-content.active {
748
  animation: fadeIn 0.2s ease-in-out;
749
  }
750
+ """,
751
  )
752
 
753
+
754
+ # Define the Cloze Model (based on Anki's default Cloze type)
755
+ CLOZE_MODEL = genanki.Model(
756
+ random.randrange(1 << 30, 1 << 31), # Needs a unique ID
757
+ "AnkiGen Cloze Enhanced",
758
+ model_type=genanki.Model.CLOZE, # Specify model type as CLOZE
759
+ fields=[
760
+ {"name": "Text"}, # Field for the text containing the cloze deletion
761
+ {"name": "Extra"}, # Field for additional info shown on the back
762
+ {"name": "Difficulty"}, # Keep metadata
763
+ {"name": "SourceTopic"}, # Add topic info
764
+ ],
765
+ templates=[
766
+ {
767
+ "name": "Cloze Card",
768
+ "qfmt": "{{cloze:Text}}",
769
+ "afmt": """
770
+ {{cloze:Text}}
771
+ <hr>
772
+ <div class="extra-info">{{Extra}}</div>
773
+ <div class="metadata-footer">Difficulty: {{Difficulty}} | Topic: {{SourceTopic}}</div>
774
+ """,
775
+ }
776
+ ],
777
+ css="""
778
+ .card {
779
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
780
+ font-size: 16px; line-height: 1.6; color: #1a1a1a;
781
+ max-width: 800px; margin: 0 auto; padding: 20px;
782
+ background: #ffffff;
783
+ }
784
+ .cloze {
785
+ font-weight: bold; color: #2563eb;
786
+ }
787
+ .extra-info {
788
+ margin-top: 1em; padding-top: 1em;
789
+ border-top: 1px solid #e5e7eb;
790
+ font-size: 0.95em; color: #333;
791
+ background: #f8fafc; padding: 1em; border-radius: 6px;
792
+ }
793
+ .extra-info h3 { margin-top: 0.5em; font-size: 1.1em; color: #1e293b; }
794
+ .extra-info pre code {
795
+ display: block; padding: 1em; background: #1e293b;
796
+ color: #e2e8f0; border-radius: 6px; overflow-x: auto;
797
+ font-family: 'Fira Code', 'Consolas', monospace; font-size: 0.9em;
798
+ margin-top: 0.5em;
799
+ }
800
+ .metadata-footer {
801
+ margin-top: 1.5em; font-size: 0.85em; color: #64748b; text-align: right;
802
+ }
803
+ """,
804
+ )
805
+
806
+
807
  # Split the export functions
808
  def export_csv(data):
809
  """Export the generated cards as a CSV file"""
810
  if data is None:
811
  raise gr.Error("No data to export. Please generate cards first.")
812
+
813
  if len(data) < 2: # Minimum 2 cards
814
  raise gr.Error("Need at least 2 cards to export.")
815
 
 
819
  data.to_csv(csv_path, index=False)
820
  gr.Info("✅ CSV export complete!")
821
  return gr.File(value=csv_path, visible=True)
822
+
823
  except Exception as e:
824
  logger.error(f"Failed to export CSV: {str(e)}", exc_info=True)
825
  raise gr.Error(f"Failed to export CSV: {str(e)}")
826
 
827
+
828
  def export_deck(data, subject):
829
  """Export the generated cards as an Anki deck with pedagogical metadata"""
830
  if data is None:
831
  raise gr.Error("No data to export. Please generate cards first.")
832
+
833
  if len(data) < 2: # Minimum 2 cards
834
  raise gr.Error("Need at least 2 cards to export.")
835
 
836
  try:
837
  gr.Info("💾 Creating Anki deck...")
838
+
839
  deck_id = random.randrange(1 << 30, 1 << 31)
840
  deck = genanki.Deck(deck_id, f"AnkiGen - {subject}")
841
+
842
+ records = data.to_dict("records")
843
+
844
+ # Ensure both models are added to the deck package
845
+ deck.add_model(BASIC_MODEL)
846
+ deck.add_model(CLOZE_MODEL)
847
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
848
  # Add notes to the deck
849
  for record in records:
850
+ card_type = record.get("Card_Type", "basic").lower()
851
+
852
+ if card_type == "cloze":
853
+ # Create Cloze note
854
+ extra_content = f"""
855
+ <h3>Explanation:</h3>
856
+ <div>{record["Explanation"]}</div>
857
+ <h3>Example:</h3>
858
+ <pre><code>{record["Example"]}</code></pre>
859
+ <h3>Prerequisites:</h3>
860
+ <div>{record["Prerequisites"]}</div>
861
+ <h3>Learning Outcomes:</h3>
862
+ <div>{record["Learning_Outcomes"]}</div>
863
+ <h3>Watch out for:</h3>
864
+ <div>{record["Common_Misconceptions"]}</div>
865
+ """
866
+ note = genanki.Note(
867
+ model=CLOZE_MODEL,
868
+ fields=[
869
+ str(record["Question"]), # Contains {{c1::...}}
870
+ extra_content, # All other info goes here
871
+ str(record["Difficulty"]),
872
+ str(record["Topic"]),
873
+ ],
874
+ )
875
+ else: # Default to basic card
876
+ # Create Basic note (existing logic)
877
+ note = genanki.Note(
878
+ model=BASIC_MODEL,
879
+ fields=[
880
+ str(record["Question"]),
881
+ str(record["Answer"]),
882
+ str(record["Explanation"]),
883
+ str(record["Example"]),
884
+ str(record["Prerequisites"]),
885
+ str(record["Learning_Outcomes"]),
886
+ str(record["Common_Misconceptions"]),
887
+ str(record["Difficulty"]),
888
+ ],
889
+ )
890
+
891
  deck.add_note(note)
892
+
893
  # Create a temporary directory for the package
894
  with tempfile.TemporaryDirectory() as temp_dir:
895
  output_path = Path(temp_dir) / "anki_deck.apkg"
896
  package = genanki.Package(deck)
897
  package.write_to_file(output_path)
898
+
899
  # Copy to a more permanent location
900
  final_path = "anki_deck.apkg"
901
+ with open(output_path, "rb") as src, open(final_path, "wb") as dst:
902
  dst.write(src.read())
903
+
904
  gr.Info("✅ Anki deck export complete!")
905
  return gr.File(value=final_path, visible=True)
906
+
907
  except Exception as e:
908
  logger.error(f"Failed to export Anki deck: {str(e)}", exc_info=True)
909
  raise gr.Error(f"Failed to export Anki deck: {str(e)}")
 
941
  button_primary_text_color="white",
942
  )
943
 
944
+
945
  def analyze_learning_path(api_key, description, model):
946
  """Analyze a job description or learning goal to create a structured learning path"""
947
+
948
  try:
949
  client = OpenAI(api_key=api_key)
950
  except Exception as e:
951
  logger.error(f"Failed to initialize OpenAI client: {str(e)}")
952
  raise gr.Error(f"Failed to initialize OpenAI client: {str(e)}")
953
+
954
  system_prompt = """You are an expert curriculum designer and educational consultant.
955
  Your task is to analyze learning goals and create structured, achievable learning paths.
956
  Break down complex topics into manageable subjects, identify prerequisites,
957
  and suggest practical projects that reinforce learning.
958
  Focus on creating a logical progression that builds upon previous knowledge."""
959
+
960
  path_prompt = f"""
961
  Analyze this description and create a structured learning path.
962
  Return your analysis as a JSON object with the following structure:
 
975
  Description to analyze:
976
  {description}
977
  """
978
+
979
  try:
980
  response = structured_output_completion(
981
+ client, model, {"type": "json_object"}, system_prompt, path_prompt
 
 
 
 
982
  )
983
+
984
+ if (
985
+ not response
986
+ or "subjects" not in response
987
+ or "learning_order" not in response
988
+ or "projects" not in response
989
+ ):
990
+ logger.error("Invalid response format from API")
991
+ raise gr.Error("Failed to analyze learning path. Please try again.")
992
+
993
  subjects_df = pd.DataFrame(response["subjects"])
994
+ learning_order_text = (
995
+ f"### Recommended Learning Order\n{response['learning_order']}"
996
+ )
997
  projects_text = f"### Suggested Projects\n{response['projects']}"
998
+
999
  return subjects_df, learning_order_text, projects_text
1000
+
1001
  except Exception as e:
1002
  logger.error(f"Failed to analyze learning path: {str(e)}")
1003
  raise gr.Error(f"Failed to analyze learning path: {str(e)}")
1004
 
1005
+
1006
+ # --- Example Data for Initialization ---
1007
+ example_data = pd.DataFrame(
1008
+ [
1009
+ [
1010
+ "1.1",
1011
+ "SQL Basics",
1012
+ "basic",
1013
+ "What is a SELECT statement used for?",
1014
+ "Retrieving data from one or more database tables.",
1015
+ "The SELECT statement is the most common command in SQL. It allows you to specify which columns and rows you want to retrieve from a table based on certain conditions.",
1016
+ "```sql\\nSELECT column1, column2 FROM my_table WHERE condition;\\n```",
1017
+ ["Understanding of database tables"],
1018
+ ["Retrieve specific data", "Filter results"],
1019
+ ["❌ SELECT * is always efficient (Reality: Can be slow for large tables)"],
1020
+ "beginner",
1021
+ ],
1022
+ [
1023
+ "2.1",
1024
+ "Python Fundamentals",
1025
+ "cloze",
1026
+ "The primary keyword to define a function in Python is {{c1::def}}.",
1027
+ "def",
1028
+ "Functions are defined using the `def` keyword, followed by the function name, parentheses for arguments, and a colon. The indented block below defines the function body.",
1029
+ # Use a raw triple-quoted string for the code block to avoid escaping issues
1030
+ r"""```python
1031
+ def greet(name):
1032
+ print(f"Hello, {name}!")
1033
+ ```""",
1034
+ ["Basic programming concepts"],
1035
+ ["Define reusable blocks of code"],
1036
+ ["❌ Forgetting the colon (:) after the definition"],
1037
+ "beginner",
1038
+ ],
1039
+ ],
1040
+ columns=[
1041
+ "Index",
1042
+ "Topic",
1043
+ "Card_Type",
1044
+ "Question",
1045
+ "Answer",
1046
+ "Explanation",
1047
+ "Example",
1048
+ "Prerequisites",
1049
+ "Learning_Outcomes",
1050
+ "Common_Misconceptions",
1051
+ "Difficulty",
1052
+ ],
1053
+ )
1054
+ # -------------------------------------
1055
+
1056
  with gr.Blocks(
1057
  theme=custom_theme,
1058
  title="AnkiGen",
1059
  css="""
1060
  #footer {display:none !important}
1061
+ .tall-dataframe {min-height: 500px !important}
1062
+ .contain {max-width: 95% !important; margin: auto;}
1063
  .output-cards {border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);}
1064
  .hint-text {font-size: 0.9em; color: #666; margin-top: 4px;}
1065
+ .export-group > .gradio-group { margin-bottom: 0 !important; padding-bottom: 5px !important; }
1066
  """,
1067
+ js=js_storage,
1068
  ) as ankigen:
1069
  with gr.Column(elem_classes="contain"):
1070
  gr.Markdown("# 📚 AnkiGen - Advanced Anki Card Generator")
 
1075
  with gr.Row():
1076
  with gr.Column(scale=1):
1077
  gr.Markdown("### Configuration")
1078
+
1079
  # Add mode selection
1080
  generation_mode = gr.Radio(
1081
+ choices=["subject", "path"],
 
 
 
1082
  value="subject",
1083
  label="Generation Mode",
1084
+ info="Choose how you want to generate content",
1085
  )
1086
+
1087
  # Create containers for different modes
1088
  with gr.Group() as subject_mode:
1089
  subject = gr.Textbox(
1090
  label="Subject",
1091
  placeholder="Enter the subject, e.g., 'Basic SQL Concepts'",
1092
+ info="The topic you want to generate flashcards for",
1093
  )
1094
+
1095
  with gr.Group(visible=False) as path_mode:
1096
  description = gr.Textbox(
1097
  label="Learning Goal",
1098
  placeholder="Paste a job description or describe what you want to learn...",
1099
  info="We'll break this down into learnable subjects",
1100
+ lines=5,
1101
+ )
1102
+ analyze_button = gr.Button(
1103
+ "Analyze & Break Down", variant="secondary"
1104
  )
1105
+
 
1106
  # Common settings
1107
  api_key_input = gr.Textbox(
1108
  label="OpenAI API Key",
1109
  type="password",
1110
  placeholder="Enter your OpenAI API key",
1111
  value=os.getenv("OPENAI_API_KEY", ""),
1112
+ info="Your OpenAI API key starting with 'sk-'",
1113
  )
1114
+
1115
  # Generation Button
1116
  generate_button = gr.Button("Generate Cards", variant="primary")
1117
 
1118
  # Advanced Settings in Accordion
1119
  with gr.Accordion("Advanced Settings", open=False):
1120
  model_choice = gr.Dropdown(
1121
+ choices=["gpt-4.1-mini", "gpt-4.1"],
1122
+ value="gpt-4.1-mini",
1123
+ label="Model Selection",
1124
+ info="Select the AI model to use for generation",
 
 
 
 
1125
  )
1126
+
1127
  # Add tooltip/description for models
1128
  model_info = gr.Markdown("""
1129
  **Model Information:**
1130
+ - **gpt-4.1-mini**: Fastest option, good for most use cases
1131
+ - **gpt-4.1**: Better quality, takes longer to generate
 
1132
  """)
1133
+
1134
  topic_number = gr.Slider(
1135
  label="Number of Topics",
1136
  minimum=2,
 
1153
  info="Customize how the content is presented",
1154
  lines=3,
1155
  )
1156
+ generate_cloze_checkbox = gr.Checkbox(
1157
+ label="Generate Cloze Cards (Experimental)",
1158
+ value=False,
1159
+ info="Allow the AI to generate fill-in-the-blank style cards where appropriate.",
1160
+ )
1161
 
1162
  # Right column - add a new container for learning path results
1163
  with gr.Column(scale=2):
 
1166
  subjects_list = gr.Dataframe(
1167
  headers=["Subject", "Prerequisites", "Time Estimate"],
1168
  label="Recommended Subjects",
1169
+ interactive=False,
1170
  )
1171
  learning_order = gr.Markdown("### Recommended Learning Order")
1172
  projects = gr.Markdown("### Suggested Projects")
1173
+
1174
  # Replace generate_selected with use_subjects
1175
  use_subjects = gr.Button(
1176
  "Use These Subjects ℹ️", # Added info emoji to button text
1177
+ variant="primary",
1178
  )
1179
  gr.Markdown(
1180
  "*Click to copy subjects to main input for card generation*",
1181
+ elem_classes="hint-text",
1182
  )
1183
+
1184
  # Existing output components
1185
  with gr.Group() as cards_output:
1186
  gr.Markdown("### Generated Cards")
1187
+
1188
  # Output Format Documentation
1189
+ with gr.Accordion("Output Format", open=False):
1190
  gr.Markdown("""
1191
  The generated cards include:
1192
 
1193
  * **Index**: Unique identifier for each card
1194
  * **Topic**: The specific subtopic within your subject
1195
+ * **Card_Type**: Type of card (basic or cloze)
1196
  * **Question**: Clear, focused question for the flashcard front
1197
  * **Answer**: Concise core answer
1198
  * **Explanation**: Detailed conceptual explanation
 
1211
  with gr.Accordion("Example Card Format", open=False):
1212
  gr.Code(
1213
  label="Example Card",
1214
+ value="""
1215
  {
1216
  "front": {
1217
  "question": "What is a PRIMARY KEY constraint in SQL?"
 
1231
  "difficulty": "beginner"
1232
  }
1233
  }
1234
+ """,
1235
+ language="json",
1236
  )
1237
+
1238
  # Dataframe Output
1239
  output = gr.Dataframe(
1240
+ value=example_data,
1241
  headers=[
1242
  "Index",
1243
  "Topic",
1244
+ "Card_Type",
1245
  "Question",
1246
  "Answer",
1247
  "Explanation",
 
1249
  "Prerequisites",
1250
  "Learning_Outcomes",
1251
  "Common_Misconceptions",
1252
+ "Difficulty",
1253
  ],
1254
  interactive=True,
1255
  elem_classes="tall-dataframe",
1256
  wrap=True,
1257
+ column_widths=[
1258
+ 50,
1259
+ 100,
1260
+ 80,
1261
+ 200,
1262
+ 200,
1263
+ 250,
1264
+ 200,
1265
+ 150,
1266
+ 150,
1267
+ 150,
1268
+ 100,
1269
+ ],
1270
  )
1271
 
1272
  # Export Controls
1273
+ with gr.Group(elem_classes="export-group"):
1274
+ gr.Markdown("#### Export Generated Cards")
1275
+ with gr.Row():
1276
+ export_csv_button = gr.Button(
1277
+ "Export to CSV", variant="secondary"
1278
+ )
1279
+ export_anki_button = gr.Button(
1280
+ "Export to Anki Deck (.apkg)", variant="secondary"
1281
+ )
1282
+ # Re-wrap File components in an invisible Row
1283
+ with gr.Row(visible=False):
1284
+ download_csv = gr.File(
1285
+ label="Download CSV", interactive=False, visible=False
1286
+ )
1287
+ download_anki = gr.File(
1288
+ label="Download Anki Deck",
1289
+ interactive=False,
1290
+ visible=False,
1291
+ )
1292
 
1293
  # Add near the top of the Blocks
1294
  with gr.Row():
1295
  progress = gr.HTML(visible=False)
1296
+ total_cards = gr.Number(
1297
+ label="Total Cards Generated", value=0, visible=False
1298
+ )
1299
 
1300
  # Add JavaScript to handle mode switching
1301
  def update_mode_visibility(mode):
1302
  """Update component visibility based on selected mode and clear values"""
1303
+ is_subject = mode == "subject"
1304
+ is_path = mode == "path"
1305
+
1306
  # Clear values when switching modes
1307
  if is_path:
1308
  subject.value = "" # Clear subject when switching to path mode
1309
  else:
1310
+ description.value = (
1311
+ "" # Clear description when switching to subject mode
1312
+ )
1313
+
1314
  return {
1315
  subject_mode: gr.update(visible=is_subject),
1316
  path_mode: gr.update(visible=is_path),
 
1320
  description: gr.update(value="") if not is_path else gr.update(),
1321
  output: gr.update(value=None), # Clear previous output
1322
  progress: gr.update(value="", visible=False),
1323
+ total_cards: gr.update(value=0, visible=False),
1324
  }
1325
 
1326
  # Update the mode switching handler to include all components that need clearing
 
1336
  description,
1337
  output,
1338
  progress,
1339
+ total_cards,
1340
+ ],
1341
  )
1342
+
1343
  # Add handler for path analysis
1344
  analyze_button.click(
1345
  fn=analyze_learning_path,
1346
  inputs=[api_key_input, description, model_choice],
1347
+ outputs=[subjects_list, learning_order, projects],
1348
  )
1349
+
1350
  # Add this function to handle copying subjects to main input
1351
+ def use_selected_subjects(subjects_df):
1352
  """Copy selected subjects to main input and switch to subject mode"""
1353
  if subjects_df is None or subjects_df.empty:
1354
+ gr.Warning("No subjects available to copy from Learning Path analysis.")
1355
+ # Return updates for all relevant output components to avoid errors
1356
+ return (
1357
+ gr.update(),
1358
+ gr.update(),
1359
+ gr.update(),
1360
+ gr.update(),
1361
+ gr.update(),
1362
+ gr.update(),
1363
+ gr.update(),
1364
+ gr.update(),
1365
+ gr.update(),
1366
+ )
1367
+
1368
  subjects = subjects_df["Subject"].tolist()
1369
  combined_subject = ", ".join(subjects)
1370
+ suggested_topics = min(
1371
+ len(subjects) + 1, 20
1372
+ ) # Suggest topics = num subjects + 1
1373
+
1374
+ # Return updates for relevant components
1375
  return (
1376
+ "subject", # Set mode to subject
1377
+ gr.update(visible=True), # Show subject_mode group
1378
+ gr.update(visible=False), # Hide path_mode group
1379
+ gr.update(visible=False), # Hide path_results group
1380
+ gr.update(visible=True), # Show cards_output group
1381
+ combined_subject, # Update subject textbox value
1382
+ suggested_topics, # Update topic_number slider value
1383
+ # Update preference prompt
1384
+ "Focus on connections between these subjects and their practical applications.",
1385
+ example_data, # Reset output to example data - THIS NOW WORKS
1386
  )
1387
 
1388
+ # Correct the outputs for the use_subjects click handler
1389
  use_subjects.click(
1390
  fn=use_selected_subjects,
1391
+ inputs=[subjects_list], # Only needs the dataframe
1392
+ outputs=[ # Match the return tuple of the function
1393
  generation_mode,
1394
+ subject_mode, # Group visibility
1395
+ path_mode, # Group visibility
1396
+ path_results, # Group visibility
1397
+ cards_output, # Group visibility
1398
+ subject, # Component value
1399
+ topic_number, # Component value
1400
+ preference_prompt, # Component value
1401
+ output, # Component value
1402
+ ],
 
1403
  )
1404
 
1405
  # Simplified event handlers
 
1408
  inputs=[
1409
  api_key_input,
1410
  subject,
1411
+ model_choice,
1412
  topic_number,
1413
  cards_per_topic,
1414
  preference_prompt,
1415
+ generate_cloze_checkbox,
1416
  ],
1417
  outputs=[output, progress, total_cards],
1418
+ show_progress="full",
1419
  )
1420
 
1421
  export_csv_button.click(
pyproject.toml CHANGED
@@ -4,22 +4,23 @@ build-backend = "setuptools.build_meta"
4
 
5
  [project]
6
  name = "ankigen"
7
- version = "0.1.0"
8
  description = ""
9
  authors = [
10
- {name = "Justin", email = "[email protected]"}
11
  ]
12
  readme = "README.md"
13
  requires-python = ">=3.12"
14
  dependencies = [
15
  "openai>=1.35.10",
16
  "gradio>=4.44.1",
 
 
 
17
  ]
18
 
19
  [project.optional-dependencies]
20
- dev = [
21
- "ipykernel>=6.29.5",
22
- ]
23
 
24
  [tool.setuptools]
25
- py-modules = ["app"]
 
4
 
5
  [project]
6
  name = "ankigen"
7
+ version = "0.2.0"
8
  description = ""
9
  authors = [
10
+ { name = "Justin", email = "[email protected]" },
11
  ]
12
  readme = "README.md"
13
  requires-python = ">=3.12"
14
  dependencies = [
15
  "openai>=1.35.10",
16
  "gradio>=4.44.1",
17
+ "tenacity>=9.1.2",
18
+ "genanki>=0.13.1",
19
+ "pydantic==2.10.6",
20
  ]
21
 
22
  [project.optional-dependencies]
23
+ dev = ["ipykernel>=6.29.5"]
 
 
24
 
25
  [tool.setuptools]
26
+ py-modules = ["app"]
requirements.txt CHANGED
@@ -1,5 +1,60 @@
1
- gradio
2
- openai
3
- tenacity>=8.2.3
4
- genanki>=0.13.0
5
- pandas>=2.0.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ aiofiles==23.2.1
2
+ -e file:///home/justin/Documents/Code/ankigen
3
+ annotated-types==0.7.0
4
+ anyio==4.9.0
5
+ cached-property==2.0.1
6
+ certifi==2025.1.31
7
+ charset-normalizer==3.4.1
8
+ chevron==0.14.0
9
+ click==8.1.8
10
+ distro==1.9.0
11
+ fastapi==0.115.12
12
+ ffmpy==0.5.0
13
+ filelock==3.18.0
14
+ frozendict==2.4.6
15
+ fsspec==2025.3.2
16
+ genanki==0.13.1
17
+ gradio==5.3.0
18
+ gradio-client==1.4.2
19
+ h11==0.14.0
20
+ httpcore==1.0.8
21
+ httpx==0.28.1
22
+ huggingface-hub==0.30.2
23
+ idna==3.10
24
+ jinja2==3.1.6
25
+ jiter==0.9.0
26
+ markdown-it-py==3.0.0
27
+ markupsafe==2.1.5
28
+ mdurl==0.1.2
29
+ numpy==2.2.4
30
+ openai==1.75.0
31
+ orjson==3.10.16
32
+ packaging==24.2
33
+ pandas==2.2.3
34
+ pillow==10.4.0
35
+ pydantic==2.10.6
36
+ pydantic-core==2.27.2
37
+ pydub==0.25.1
38
+ pygments==2.19.1
39
+ python-dateutil==2.9.0.post0
40
+ python-multipart==0.0.20
41
+ pytz==2025.2
42
+ pyyaml==6.0.2
43
+ requests==2.32.3
44
+ rich==14.0.0
45
+ ruff==0.11.6
46
+ semantic-version==2.10.0
47
+ shellingham==1.5.4
48
+ six==1.17.0
49
+ sniffio==1.3.1
50
+ starlette==0.46.2
51
+ tenacity==9.1.2
52
+ tomlkit==0.12.0
53
+ tqdm==4.67.1
54
+ typer==0.15.2
55
+ typing-extensions==4.13.2
56
+ typing-inspection==0.4.0
57
+ tzdata==2025.2
58
+ urllib3==2.4.0
59
+ uvicorn==0.34.1
60
+ websockets==12.0
uv.lock ADDED
The diff for this file is too large to render. See raw diff